From adb536e135ac230a26dd32972d069b8a54ebbcc0 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 7 Dec 2025 23:03:14 -0300 Subject: [PATCH] Enhance Album entity with bidirectional relationships and partial update support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces significant improvements to the Album entity and its update functionality, adding bidirectional relationship management with AlbumHasArtist and implementing proper partial update semantics using protobuf wrapper types. Key enhancements: Model Layer Improvements: - Added OneToMany relationship from Album to AlbumHasArtist * Bidirectional mapping with cascade ALL and orphan removal * Maintains collection of album-artist associations - Implemented helper methods for artist management: * addArtist(): creates and adds AlbumHasArtist association * removeArtist(): removes association by artist ID * getArtists(): convenience method to extract Artist list from associations - Added getters/setters for albumArtists collection - Improved entity relationship management Service Layer Enhancements: - Refactored updateAlbum() to support partial updates - Added boolean flags to control field updates: * updateYear: controls whether year field should be modified * updateNumberOfDiscs: controls disc count updates * updateAlbumType: controls AlbumType relationship updates * updateAlbumArt: controls AlbumArt relationship updates - Only updates fields when explicitly provided by client - Maintains existing values for non-provided fields - Allows explicit null/removal of optional relationships - Improved update logic clarity and maintainability Handler Layer Improvements (UpdateAlbumHandler): - Integrated protobuf wrapper type handling - Uses hasYear(), hasNumberOfDiscs(), etc. to detect field presence - Extracts values from Int32Value wrappers - Passes presence flags to service layer - Properly distinguishes between "not provided" and "null/0" - Improved code formatting and readability Protocol Buffer Enhancements: - Added import for google.protobuf.wrappers - Changed UpdateAlbumRequest optional fields to wrapper types: * year: int32 → google.protobuf.Int32Value * number_of_discs: int32 → google.protobuf.Int32Value * fk_albumtype_id: int32 → google.protobuf.Int32Value * fk_albumart_id: int32 → google.protobuf.Int32Value - Enables proper optional field semantics - Allows clients to omit fields from update requests - Supports explicit null to remove relationships Benefits of these changes: - Proper partial update support: clients can update only specific fields - Prevents unintended field overwrites with default values - Bidirectional navigation from Album to Artists - Cascade operations for album-artist associations - Cleaner API semantics with explicit field presence detection - Better alignment with REST PATCH semantics - Maintains backward compatibility for required fields Example usage: - Update only name: provide name, omit other fields - Remove AlbumType: provide fk_albumtype_id with value 0 or null - Keep existing year: omit year field from request - Update multiple fields: provide only the fields to change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../java/com/mediamanager/model/Album.java | 36 ++++++++++ .../service/album/AlbumService.java | 71 ++++++++++++------- .../handler/album/UpdateAlbumHandler.java | 48 +++++++++---- src/main/proto/album.proto | 14 ++-- 4 files changed, 126 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/mediamanager/model/Album.java b/src/main/java/com/mediamanager/model/Album.java index 6a740fc..6bedc3a 100644 --- a/src/main/java/com/mediamanager/model/Album.java +++ b/src/main/java/com/mediamanager/model/Album.java @@ -2,6 +2,9 @@ package com.mediamanager.model; import jakarta.persistence.*; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @Entity @Table(name = "album") @@ -34,6 +37,10 @@ public class Album { @JoinColumn(name = "fk_albumart_id") private AlbumArt albumArt; + // Relacionamento ManyToMany com Artist através da tabela de junção + @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, orphanRemoval = true) + private List albumArtists = new ArrayList<>(); + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @@ -53,6 +60,27 @@ public class Album { public Album() {} + // Métodos helper para Artist (ManyToMany) + public void addArtist(Artist artist) { + AlbumHasArtist albumHasArtist = new AlbumHasArtist(); + albumHasArtist.setAlbum(this); + albumHasArtist.setArtist(artist); + albumArtists.add(albumHasArtist); + } + + public void removeArtist(Artist artist) { + albumArtists.removeIf(aa -> + aa.getArtist() != null && aa.getArtist().getId().equals(artist.getId()) + ); + } + + // Método conveniente para pegar só os artistas + public List getArtists() { + return albumArtists.stream() + .map(AlbumHasArtist::getArtist) + .collect(Collectors.toList()); + } + // Getters and Setters public Integer getId() { return id; @@ -118,6 +146,14 @@ public class Album { this.albumArt = albumArt; } + public List getAlbumArtists() { + return albumArtists; + } + + public void setAlbumArtists(List albumArtists) { + this.albumArtists = albumArtists; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/com/mediamanager/service/album/AlbumService.java b/src/main/java/com/mediamanager/service/album/AlbumService.java index 82d1664..bf41a46 100644 --- a/src/main/java/com/mediamanager/service/album/AlbumService.java +++ b/src/main/java/com/mediamanager/service/album/AlbumService.java @@ -71,50 +71,73 @@ public class AlbumService { return repository.findById(id); } - public Optional updateAlbum(Integer id, String name, Integer year, Integer numberOfDiscs, String code, Boolean isCompilation, Integer albumTypeId, Integer albumArtId) { + public Optional updateAlbum(Integer id, String name, Integer year, + Integer numberOfDiscs, String code, + Boolean isCompilation, Integer albumTypeId, + Integer albumArtId, + boolean updateYear, // ← Novo! + boolean updateNumberOfDiscs, // ← Novo! + boolean updateAlbumType, // ← Novo! + boolean updateAlbumArt) { // ← Novo! if (id == null) { throw new IllegalArgumentException("ID cannot be null"); } - logger.info("Updating album:{}", name); + logger.info("Updating album: {}", name); if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("Album name cannot be null or empty"); } Optional existingAlbum = repository.findById(id); - if(existingAlbum.isEmpty()) { - logger.warn("Album not found with id:{}", id); + if (existingAlbum.isEmpty()) { + logger.warn("Album not found with id: {}", id); return Optional.empty(); } Album album = existingAlbum.get(); album.setName(name); - album.setYear(year); - album.setNumberOfDiscs(numberOfDiscs); album.setCode(code); album.setIsCompilation(isCompilation); - // Update AlbumType if provided - if (albumTypeId != null && albumTypeId > 0) { - Optional albumType = albumTypeRepository.findById(albumTypeId); - if (albumType.isEmpty()) { - throw new IllegalArgumentException("AlbumType not found with id: " + albumTypeId); - } - album.setAlbumType(albumType.get()); - } else { - album.setAlbumType(null); + // Atualiza year SOMENTE se o campo foi fornecido + if (updateYear) { + album.setYear(year); } - // Update AlbumArt if provided - if (albumArtId != null && albumArtId > 0) { - Optional albumArt = albumArtRepository.findById(albumArtId); - if (albumArt.isEmpty()) { - throw new IllegalArgumentException("AlbumArt not found with id: " + albumArtId); - } - album.setAlbumArt(albumArt.get()); - } else { - album.setAlbumArt(null); + // Atualiza numberOfDiscs SOMENTE se o campo foi fornecido + if (updateNumberOfDiscs) { + album.setNumberOfDiscs(numberOfDiscs); } + // Update AlbumType SOMENTE se o campo foi fornecido + if (updateAlbumType) { + if (albumTypeId != null && albumTypeId > 0) { + Optional albumType = albumTypeRepository.findById(albumTypeId); + if (albumType.isEmpty()) { + throw new IllegalArgumentException("AlbumType not found with id: " + albumTypeId); + } + album.setAlbumType(albumType.get()); + } else { + // Explicitamente passado como 0 ou null = remover a relação + album.setAlbumType(null); + } + } + // Se não foi fornecido, mantém o existente + + // Update AlbumArt SOMENTE se o campo foi fornecido + if (updateAlbumArt) { + if (albumArtId != null && albumArtId > 0) { + Optional albumArt = albumArtRepository.findById(albumArtId); + if (albumArt.isEmpty()) { + throw new IllegalArgumentException("AlbumArt not found with id: " + albumArtId); + } + album.setAlbumArt(albumArt.get()); + } else { + // Explicitamente passado como 0 ou null = remover a relação + album.setAlbumArt(null); + } + } + // Se não foi fornecido, mantém o existente + Album updatedAlbum = repository.update(album); return Optional.of(updatedAlbum); } diff --git a/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java b/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java index ac10f55..d9f81b4 100644 --- a/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java +++ b/src/main/java/com/mediamanager/service/delegate/handler/album/UpdateAlbumHandler.java @@ -24,24 +24,47 @@ public class UpdateAlbumHandler implements ActionHandler { } @Override - public TransportProtocol.Response.Builder handle(ByteString requestPayload) throws InvalidProtocolBufferException { - try{ + public TransportProtocol.Response.Builder handle(ByteString requestPayload) + throws InvalidProtocolBufferException { + try { AlbumMessages.UpdateAlbumRequest updateRequest = AlbumMessages.UpdateAlbumRequest.parseFrom(requestPayload); int id = updateRequest.getId(); + + // Extrai valores dos wrappers - null se não foi fornecido + Integer year = updateRequest.hasYear() + ? updateRequest.getYear().getValue() + : null; + + Integer numberOfDiscs = updateRequest.hasNumberOfDiscs() + ? updateRequest.getNumberOfDiscs().getValue() + : null; + + Integer albumTypeId = updateRequest.hasFkAlbumtypeId() + ? updateRequest.getFkAlbumtypeId().getValue() + : null; + + Integer albumArtId = updateRequest.hasFkAlbumartId() + ? updateRequest.getFkAlbumartId().getValue() + : null; + Optional albumOpt = albumService.updateAlbum( id, updateRequest.getName(), - updateRequest.getYear() > 0 ? updateRequest.getYear() : null, - updateRequest.getNumberOfDiscs() > 0 ? updateRequest.getNumberOfDiscs() : null, + year, + numberOfDiscs, updateRequest.getCode().isEmpty() ? null : updateRequest.getCode(), updateRequest.getIsCompilation(), - updateRequest.getFkAlbumtypeId() > 0 ? updateRequest.getFkAlbumtypeId() : null, - updateRequest.getFkAlbumartId() > 0 ? updateRequest.getFkAlbumartId() : null + albumTypeId, + albumArtId, + updateRequest.hasYear(), // ← Novo! + updateRequest.hasNumberOfDiscs(), // ← Novo! + updateRequest.hasFkAlbumtypeId(), // ← Novo! + updateRequest.hasFkAlbumartId() // ← Novo! ); - if(albumOpt.isEmpty()){ + if (albumOpt.isEmpty()) { logger.warn("Album not found with ID: {}", id); return TransportProtocol.Response.newBuilder() .setStatusCode(404) @@ -50,14 +73,15 @@ public class UpdateAlbumHandler implements ActionHandler { AlbumMessages.Album albumProto = AlbumMapper.toProtobuf(albumOpt.get()); - AlbumMessages.UpdateAlbumResponse updateResponse = AlbumMessages.UpdateAlbumResponse.newBuilder() - .setAlbum(albumProto) - .build(); + AlbumMessages.UpdateAlbumResponse updateResponse = + AlbumMessages.UpdateAlbumResponse.newBuilder() + .setAlbum(albumProto) + .build(); return TransportProtocol.Response.newBuilder() .setPayload(updateResponse.toByteString()); - } catch (IllegalArgumentException e){ + } catch (IllegalArgumentException e) { logger.error("Validation error", e); return TransportProtocol.Response.newBuilder() .setStatusCode(400) @@ -69,4 +93,4 @@ public class UpdateAlbumHandler implements ActionHandler { .setPayload(ByteString.copyFromUtf8("Error: " + e.getMessage())); } } -} +} \ No newline at end of file diff --git a/src/main/proto/album.proto b/src/main/proto/album.proto index 44e876e..e7d350a 100644 --- a/src/main/proto/album.proto +++ b/src/main/proto/album.proto @@ -1,5 +1,7 @@ syntax = "proto3"; +import "google/protobuf/wrappers.proto"; // ← Adicione esse import! + option java_package = "com.mediamanager.protocol.messages"; option java_outer_classname = "AlbumMessages"; @@ -13,11 +15,9 @@ message Album { string code = 5; bool is_compilation = 6; - int32 fk_albumtype_id = 7; int32 fk_albumart_id = 8; - int64 created_at = 9; int64 updated_at = 10; } @@ -55,12 +55,12 @@ message GetAlbumByIdResponse { message UpdateAlbumRequest { int32 id = 1; string name = 2; - int32 year = 3; - int32 number_of_discs = 4; + google.protobuf.Int32Value year = 3; // ← Mudou! + google.protobuf.Int32Value number_of_discs = 4; // ← Mudou! string code = 5; bool is_compilation = 6; - int32 fk_albumtype_id = 7; - int32 fk_albumart_id = 8; + google.protobuf.Int32Value fk_albumtype_id = 7; // ← Mudou! + google.protobuf.Int32Value fk_albumart_id = 8; // ← Mudou! } message UpdateAlbumResponse { @@ -74,4 +74,4 @@ message DeleteAlbumRequest { message DeleteAlbumResponse { bool success = 1; string message = 2; -} +} \ No newline at end of file