Ugrás a fő tartalomhoz

Spring tanfolyam - 3. alkalom

A mostani alkalmon a ticketing alkalmazást fogunk befejezni.


5. Fejezet - Bővítés címkével

Szeretnénk címkékkel (Label) bővíteni az alkalmazásunkat, amiket jegyekre (Ticket) illeszthetünk. A címkéknek legyen nevük, színük, és több-több kapcsolatban álljanak a jegyekkel.

Hozzunk létre egy új mappát label néven és a továbbiakban oda dolgozzunk.

Ugyanazokat a lépéseket fogjuk követni, mint az eddigi entitásainknál, viszont másképp kell megvalósítanunk a kapcsolatot, mivel ez egy több-több kapcsolat lesz. Ez azt jelenti, hogy egy címke több jegyhez is tartozhat, és egy jegyhez több címke is tartozhat.

LabelEntity

LabelEntity.kt

@Entity
@Table(name = "label")
data class LabelEntity(

@Id
@GeneratedValue
@Column(nullable = false)
val id: Int = 0,

@Column(nullable = false)
var name: String = "",

@Column(nullable = false)
var color: String = "",

@ManyToMany(mappedBy = "labels")
var tickets: MutableList<TicketEntity> = mutableListOf()

){
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is LabelEntity) return false
if (id != other.id) return false
return true
}

override fun hashCode(): Int = javaClass.hashCode()

override fun toString(): String {
return this::class.simpleName + "(id = $id)"
}
}

LabelRepository

LabelRepository.kt

@Repository
interface LabelRepository: CrudRepository<LabelEntity, Int> {
}

LabelDTOk

LabelDtos.kt

data class CreateLabelDto(
val name: String,
val color: String
)

data class UpdateLabelDto(
val name: String,
val color: String
)

data class LabelDto(
val id: Int,
val name: String,
val color: String
){
constructor(label: LabelEntity): this(
id = label.id,
name = label.name,
color = label.color
)
}

data class DetailedLabelDto(
val id: Int,
val name: String,
val color: String,
val tickets: List<TicketDto>
){
constructor(label: LabelEntity): this(
id = label.id,
name = label.name,
color = label.color,
tickets = label.tickets.map { TicketDto(it) }
)
}

LabelService

LabelService.kt

@Service
class LabelService(
private val labelRepository: LabelRepository
) {

fun createLabel(label: CreateLabelDto): DetailedLabelDto {
return labelRepository.save(
LabelEntity(
name = label.name,
color = label.color
)
).let { DetailedLabelDto(it) }
}

fun getLabel(id: Int): DetailedLabelDto {
return labelRepository.findById(id)
.orElseThrow{ ResponseStatusException(HttpStatus.NOT_FOUND, "Label not found") }
.let { DetailedLabelDto(it) }
}

fun getAllLabels(): List<DetailedLabelDto> {
return labelRepository.findAll().map { DetailedLabelDto(it) }
}

fun updateLabel(id: Int, label: UpdateLabelDto): DetailedLabelDto {
return labelRepository.findById(id).map{
val toUpdate = it
toUpdate.name = label.name
toUpdate.color = label.color
labelRepository.save(toUpdate)
}.orElseThrow{ ResponseStatusException(HttpStatus.NOT_FOUND, "Label not found") }
.let { DetailedLabelDto(it) }
}

fun deleteLabel(id: Int) {
labelRepository.deleteById(id)
}

}

LabelController

LabelController.kt

@RestController
@RequestMapping("/label")
class LabelController(
private val labelService: LabelService
) {

@Operation(summary = "Create a new label")
@ApiResponses(
value = [
ApiResponse(
responseCode = "201",
description = "Label created",
content = [Content(schema = Schema(implementation = DetailedLabelDto::class))]
),
]
)
@PostMapping
fun createLabel(@RequestBody label: CreateLabelDto): ResponseEntity<DetailedLabelDto> {
val created = labelService.createLabel(label)
return ResponseEntity.status(HttpStatus.CREATED).body(created)
}


@Operation(summary = "List all labels")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Labels found",
content = [Content(schema = Schema(implementation = DetailedLabelDto::class))]
)
]
)
@GetMapping
fun getAllLabels(): ResponseEntity<List<DetailedLabelDto>> {
val labels = labelService.getAllLabels()
return ResponseEntity.status(HttpStatus.OK).body(labels)
}


@Operation(summary = "Get a label by its ID")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Label found",
content = [Content(schema = Schema(implementation = DetailedLabelDto::class))]
),
ApiResponse(responseCode = "404", description = "Label not found"),
]
)
@GetMapping("/{id}")
fun getLabel(@PathVariable id: Int): ResponseEntity<DetailedLabelDto> {
val label = labelService.getLabel(id)
return ResponseEntity.status(HttpStatus.OK).body(label)
}


@Operation(summary = "Update a label")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Label updated",
content = [Content(schema = Schema(implementation = DetailedLabelDto::class))]
),
ApiResponse(responseCode = "404", description = "Label not found"),
]
)
@PatchMapping("/{id}")
fun updateLabel(@PathVariable id: Int, @RequestBody label: UpdateLabelDto): ResponseEntity<DetailedLabelDto> {
val updated = labelService.updateLabel(id, label)
return ResponseEntity.status(HttpStatus.OK).body(updated)
}


@Operation(summary = "Delete a label")
@ApiResponses(
value = [
ApiResponse(
responseCode = "204",
description = "Label deleted"
),
]
)
@DeleteMapping("/{id}")
fun deleteLabel(@PathVariable id: Int): ResponseEntity<Void> {
labelService.deleteLabel(id)
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
}

}

Ticket DTO-k frissítése

data class CreateTicketDto(
val name: String,
val description: String?,
var boardId: Int,
var labelIds: List<Int>? // <--
)

data class UpdateTicketDto(
val name: String,
val description: String?,
var status: TicketStatus,
var boardId: Int,
var labelIds: List<Int>? // <--
)

data class TicketDto(
val id: Int,
val name: String,
val description: String,
val status: TicketStatus,
val createdAt: Date,
val updatedAt: Date,
){
constructor(ticket: TicketEntity): this(
id = ticket.id,
name = ticket.name,
description = ticket.description,
status = ticket.status,
createdAt = ticket.createdAt,
updatedAt = ticket.updatedAt,
)
}

data class DetailedTicketDto(
val id: Int,
val name: String,
val description: String,
val status: TicketStatus,
val board: BoardDto,
val labels: List<LabelDto>, // <--
val createdAt: Date,
val updatedAt: Date,
){
constructor(ticket: TicketEntity): this(
id = ticket.id,
name = ticket.name,
description = ticket.description,
status = ticket.status,
board = BoardDto(ticket.board),
labels = ticket.labels.map { LabelDto(it) }, // <--
createdAt = ticket.createdAt,
updatedAt = ticket.updatedAt,
)
}

TicketEntity frissítése

@Entity
@Table(name = "ticket")
data class TicketEntity(
@Id
@GeneratedValue
@Column(nullable = false)
val id: Int = 0,

@Column(nullable = false)
var name: String = "",

@Column(nullable = true)
var description: String = "",

@Column(nullable = false)
var status: TicketStatus = TicketStatus.CREATED,

@Column(nullable = false)
val createdAt: Date = Date(),

@Column(nullable = false)
var updatedAt: Date = Date(),

@ManyToOne
@JoinColumn(name = "board_id", nullable = false)
var board: BoardEntity = BoardEntity(),

@ManyToMany
@JoinTable(
name = "ticket_label",
joinColumns = [JoinColumn(name = "ticket_id")],
inverseJoinColumns = [JoinColumn(name = "label_id")]
)
var labels: MutableList<LabelEntity> = mutableListOf() // <--

) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TicketEntity) return false
if (id != other.id) return false
return true
}

override fun hashCode(): Int = javaClass.hashCode()

override fun toString(): String {
return this::class.simpleName + "(id = $id)"
}

@PreUpdate
fun onUpdate() {
updatedAt = Date()
}
}

TicketService frissítése

@Service
class TicketService(
private val ticketRepository: TicketRepository,
private val labelRepository: LabelRepository, // <--
private val boardRepository: BoardRepository
) {

fun createTicket(ticket: CreateTicketDto): DetailedTicketDto {
val board = boardRepository.findById(ticket.boardId)
.orElseThrow{ ResponseStatusException(HttpStatus.NOT_FOUND, "Board not found") }

val labels = ticket.labelIds?.map { // <--
labelRepository.findById(it)
.orElseThrow{ ResponseStatusException(HttpStatus.BAD_REQUEST, "Label not found") }
}?.toMutableList() ?: mutableListOf()

return ticketRepository.save(TicketEntity(
name = ticket.name,
description = ticket.description?:"",
board = board,
labels = labels // <--
)).let { DetailedTicketDto(it) }
}

fun getTicket(id: Int): DetailedTicketDto {
return ticketRepository.findById(id)
.orElseThrow{ ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found") }
.let { DetailedTicketDto(it) }
}

fun getAllTickets(): List<DetailedTicketDto> {
return ticketRepository.findAll().map { DetailedTicketDto(it) }
}

fun updateTicket(id: Int, ticket: UpdateTicketDto): DetailedTicketDto {
val board = boardRepository.findById(ticket.boardId)
.orElseThrow{ ResponseStatusException(HttpStatus.BAD_REQUEST, "Board not found") }

val labels = ticket.labelIds?.map { // <--
labelRepository.findById(it)
.orElseThrow{ ResponseStatusException(HttpStatus.BAD_REQUEST, "Label not found") }
}?.toMutableList() ?: mutableListOf()

return ticketRepository.findById(id).map{
it.name = ticket.name
it.description = ticket.description?:""
it.status = ticket.status
it.board = board
it.labels = labels // <--
ticketRepository.save(it)
}.orElseThrow{ ResponseStatusException(HttpStatus.NOT_FOUND, "Ticket not found") }
.let { DetailedTicketDto(it) }
}

fun deleteTicket(id: Int) {
ticketRepository.deleteById(id)
}

}

Címkék kipróbálása

Próbáljuk ki címkéket, amihez most már tudjuk használni a Swaggert is!

localhost:8080/swagger-ui/index.html

Szorgalmi feladat

Valósítsuk meg, hogy egy címkéhez tartozó jegyeket is lekérdezhessük egy új végponton keresztül: GET /label/{id}/tickets. Ez a végpont adja vissza az adott címkéhez tartozó jegyeket.


Feladatot készítette: Szabó Benedek, Leírást készítette: Bácsi Miklós