Skip to content

Commit fec46ea

Browse files
nanotaboadaclaude
andcommitted
feat(api): use UUID as PK, squadNumber as route identifier (#268)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 09afb08 commit fec46ea

File tree

14 files changed

+380
-473
lines changed

14 files changed

+380
-473
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ Release names follow the **historic football clubs** naming convention (A–Z):
4040

4141
## [Unreleased]
4242

43+
### Changed
44+
45+
- `Player` entity: `id` demoted from `@Id Long` to surrogate `UUID` (non-PK,
46+
`updatable=false`, `unique`, generated via `@PrePersist`); `squadNumber`
47+
promoted to `@Id Integer` (natural key) (#268)
48+
- `PlayerDTO`: `id` type changed from `Long` to `UUID` (#268)
49+
- `PlayersRepository`: keyed on `Integer` (squad number as PK); added
50+
`findById(UUID)` overload for admin/internal lookup (#268)
51+
- `PlayersService`: `retrieveById` now accepts `UUID`; `update` keyed on
52+
`Integer squadNumber`; `delete` renamed to `deleteBySquadNumber(Integer)` (#268)
53+
- `PUT /players/{squadNumber}` and `DELETE /players/{squadNumber}`: path
54+
variable changed from `Long id` to `Integer squadNumber` (#268)
55+
- `GET /players/{id}`: path variable changed from `Long` to `UUID` (admin use) (#268)
56+
- `storage/players-sqlite3.db`: schema migrated to `id VARCHAR(36) NOT NULL UNIQUE`,
57+
`squadNumber INTEGER PRIMARY KEY`; 25 players preserved with generated UUIDs (#268)
58+
- `ddl.sql` and `dml.sql`: test schema and seed data updated for new structure (#268)
59+
4360
### Added
4461

4562
- `.sonarcloud.properties`: SonarCloud Automatic Analysis configuration —

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.net.URI;
66
import java.util.List;
7+
import java.util.UUID;
78

89
import org.springframework.http.HttpStatus;
910
import org.springframework.http.ResponseEntity;
@@ -37,12 +38,12 @@
3738
* <h3>Base Path:</h3>
3839
* <ul>
3940
* <li><b>GET</b> {@code /players} - Retrieve all players</li>
40-
* <li><b>GET</b> {@code /players/{id}} - Retrieve player by ID</li>
41+
* <li><b>GET</b> {@code /players/{id}} - Retrieve player by UUID (admin/internal use)</li>
4142
* <li><b>GET</b> {@code /players/search/league/{league}} - Search players by league name</li>
42-
* <li><b>GET</b> {@code /players/squadnumber/{number}} - Retrieve player by squad number</li>
43+
* <li><b>GET</b> {@code /players/squadnumber/{squadNumber}} - Retrieve player by squad number</li>
4344
* <li><b>POST</b> {@code /players} - Create a new player</li>
44-
* <li><b>PUT</b> {@code /players/{id}} - Update an existing player</li>
45-
* <li><b>DELETE</b> {@code /players/{id}} - Delete a player by ID</li>
45+
* <li><b>PUT</b> {@code /players/{squadNumber}} - Update an existing player by squad number</li>
46+
* <li><b>DELETE</b> {@code /players/{squadNumber}} - Delete a player by squad number</li>
4647
* </ul>
4748
*
4849
* <h3>Response Codes:</h3>
@@ -74,12 +75,8 @@ public class PlayersController {
7475
/**
7576
* Creates a new player resource.
7677
* <p>
77-
* Validates the request body and creates a new player in the database. Returns a 201 Created response with a Location
78-
* header pointing to the new resource.
79-
* </p>
80-
* <p>
81-
* <b>Conflict Detection:</b> If a player with the same squad number already exists, returns 409 Conflict.
82-
* Squad numbers must be unique (jersey numbers like Messi's #10).
78+
* Validates the request body and creates a new player. Returns 201 Created with a Location
79+
* header pointing to the new resource (UUID-based path).
8380
* </p>
8481
*
8582
* @param playerDTO the player data to create (validated with JSR-380 constraints)
@@ -115,9 +112,6 @@ public ResponseEntity<Void> post(@RequestBody @Valid PlayerDTO playerDTO) {
115112

116113
/**
117114
* Retrieves all players in the squad.
118-
* <p>
119-
* Returns the complete Argentina 2022 FIFA World Cup squad (26 players).
120-
* </p>
121115
*
122116
* @return 200 OK with array of all players (empty array if none found)
123117
*/
@@ -132,28 +126,27 @@ public ResponseEntity<List<PlayerDTO>> getAll() {
132126
}
133127

134128
/**
135-
* Retrieves a single player by their unique identifier.
129+
* Retrieves a single player by their surrogate UUID (admin/internal use only).
136130
*
137-
* @param id the unique identifier of the player
131+
* @param id the UUID surrogate key of the player
138132
* @return 200 OK with player data, or 404 Not Found if player doesn't exist
139133
*/
140134
@GetMapping("/players/{id}")
141-
@Operation(summary = "Retrieves a player by ID")
135+
@Operation(summary = "Retrieves a player by UUID (admin/internal use)")
142136
@ApiResponses(value = {
143137
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation = PlayerDTO.class))),
144138
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
145139
})
146-
public ResponseEntity<PlayerDTO> getById(@PathVariable Long id) {
140+
public ResponseEntity<PlayerDTO> getById(@PathVariable UUID id) {
147141
PlayerDTO playerDTO = playersService.retrieveById(id);
148142
return (playerDTO != null)
149143
? ResponseEntity.status(HttpStatus.OK).body(playerDTO)
150144
: ResponseEntity.status(HttpStatus.NOT_FOUND).build();
151145
}
152146

153147
/**
154-
* Retrieves a player by their squad number (unique identifier).
148+
* Retrieves a player by their squad number.
155149
* <p>
156-
* Squad numbers are unique jersey numbers (e.g., Messi is #10). This is a direct lookup similar to getById().
157150
* Example: {@code /players/squadnumber/10} returns Lionel Messi
158151
* </p>
159152
*
@@ -199,30 +192,29 @@ public ResponseEntity<List<PlayerDTO>> searchByLeague(@PathVariable String leagu
199192
*/
200193

201194
/**
202-
* Updates an existing player resource (full update).
195+
* Updates an existing player resource (full update) identified by squad number.
203196
* <p>
204-
* Performs a complete replacement of the player entity. The ID in the path must match the ID in the request body.
197+
* Performs a complete replacement of the player entity. The squad number in the path
198+
* must match the squad number in the request body (if provided).
205199
* </p>
206200
*
207-
* @param id the unique identifier of the player to update
201+
* @param squadNumber the squad number (natural key) of the player to update
208202
* @param playerDTO the complete player data (must pass validation)
209-
* @return 204 No Content if successful, 404 Not Found if player doesn't exist, or 400 Bad Request if validation fails or
210-
* ID mismatch
203+
* @return 204 No Content if successful, 404 Not Found if player doesn't exist, or 400 Bad Request if validation fails
211204
*/
212-
@PutMapping("/players/{id}")
213-
@Operation(summary = "Updates (entirely) a player by ID")
205+
@PutMapping("/players/{squadNumber}")
206+
@Operation(summary = "Updates (entirely) a player by squad number")
214207
@ApiResponses(value = {
215208
@ApiResponse(responseCode = "204", description = "No Content", content = @Content),
216209
@ApiResponse(responseCode = "400", description = "Bad Request", content = @Content),
217210
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
218211
})
219-
public ResponseEntity<Void> put(@PathVariable Long id, @RequestBody @Valid PlayerDTO playerDTO) {
220-
// Ensure path ID matches body ID
221-
if (playerDTO.getId() != null && !playerDTO.getId().equals(id)) {
212+
public ResponseEntity<Void> put(@PathVariable Integer squadNumber, @RequestBody @Valid PlayerDTO playerDTO) {
213+
if (playerDTO.getSquadNumber() != null && !playerDTO.getSquadNumber().equals(squadNumber)) {
222214
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
223215
}
224-
playerDTO.setId(id); // Set ID from path to ensure consistency
225-
boolean updated = playersService.update(playerDTO);
216+
playerDTO.setSquadNumber(squadNumber);
217+
boolean updated = playersService.update(squadNumber, playerDTO);
226218
return (updated)
227219
? ResponseEntity.status(HttpStatus.NO_CONTENT).build()
228220
: ResponseEntity.status(HttpStatus.NOT_FOUND).build();
@@ -235,19 +227,19 @@ public ResponseEntity<Void> put(@PathVariable Long id, @RequestBody @Valid Playe
235227
*/
236228

237229
/**
238-
* Deletes a player resource by their unique identifier.
230+
* Deletes a player resource by their squad number.
239231
*
240-
* @param id the unique identifier of the player to delete
232+
* @param squadNumber the squad number of the player to delete
241233
* @return 204 No Content if successful, or 404 Not Found if player doesn't exist
242234
*/
243-
@DeleteMapping("/players/{id}")
244-
@Operation(summary = "Deletes a player by ID")
235+
@DeleteMapping("/players/{squadNumber}")
236+
@Operation(summary = "Deletes a player by squad number")
245237
@ApiResponses(value = {
246238
@ApiResponse(responseCode = "204", description = "No Content", content = @Content),
247239
@ApiResponse(responseCode = "404", description = "Not Found", content = @Content)
248240
})
249-
public ResponseEntity<Void> delete(@PathVariable Long id) {
250-
boolean deleted = playersService.delete(id);
241+
public ResponseEntity<Void> delete(@PathVariable Integer squadNumber) {
242+
boolean deleted = playersService.deleteBySquadNumber(squadNumber);
251243
return (deleted)
252244
? ResponseEntity.status(HttpStatus.NO_CONTENT).build()
253245
: ResponseEntity.status(HttpStatus.NOT_FOUND).build();

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/Player.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ar.com.nanotaboada.java.samples.spring.boot.models;
22

33
import java.time.LocalDate;
4+
import java.util.UUID;
45

56
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
67
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -28,9 +29,9 @@
2829
*
2930
* <h3>Key Features:</h3>
3031
* <ul>
31-
* <li>Auto-generated ID using IDENTITY strategy</li>
32-
* <li>ISO-8601 date storage for SQLite compatibility
33-
* ({@link IsoDateConverter})</li>
32+
* <li>UUID primary key — generated at application level via {@code GenerationType.UUID}</li>
33+
* <li>Squad number natural key — unique domain identifier, used as path variable for mutations</li>
34+
* <li>ISO-8601 date storage for SQLite compatibility ({@link IsoDateConverter})</li>
3435
* <li>JSON serialization support for LocalDate fields</li>
3536
* </ul>
3637
*
@@ -44,9 +45,22 @@
4445
@NoArgsConstructor
4546
@AllArgsConstructor
4647
public class Player {
48+
49+
/**
50+
* Primary key — UUID generated at application level.
51+
*/
4752
@Id
48-
@GeneratedValue(strategy = GenerationType.IDENTITY)
49-
private Long id;
53+
@GeneratedValue(strategy = GenerationType.UUID)
54+
@Column(name = "id", nullable = false, updatable = false, columnDefinition = "VARCHAR(36)")
55+
private UUID id;
56+
57+
/**
58+
* Natural key — unique domain identifier, path variable for PUT and DELETE.
59+
* Squad number (jersey number) is unique per team and stable.
60+
*/
61+
@Column(name = "squadNumber", nullable = false, unique = true, updatable = false)
62+
private Integer squadNumber;
63+
5064
private String firstName;
5165
private String middleName;
5266
private String lastName;
@@ -59,16 +73,6 @@ public class Player {
5973
@Convert(converter = IsoDateConverter.class)
6074
private LocalDate dateOfBirth;
6175

62-
/**
63-
* Squad number (jersey number) - unique natural key.
64-
* <p>
65-
* Used for player lookups via /players/search/squadnumber/{squadNumber}.
66-
* Database constraint enforces uniqueness.
67-
* </p>
68-
*/
69-
@Column(unique = true)
70-
private Integer squadNumber;
71-
7276
private String position;
7377
private String abbrPosition;
7478
private String team;

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerDTO.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
88
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
99

10+
import java.util.UUID;
11+
1012
import jakarta.validation.constraints.NotBlank;
1113
import jakarta.validation.constraints.NotNull;
1214
import jakarta.validation.constraints.Past;
@@ -46,7 +48,7 @@
4648
*/
4749
@Data
4850
public class PlayerDTO {
49-
private Long id;
51+
private UUID id;
5052
@NotBlank
5153
private String firstName;
5254
private String middleName;

src/main/java/ar/com/nanotaboada/java/samples/spring/boot/repositories/PlayersRepository.java

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.List;
44
import java.util.Optional;
5+
import java.util.UUID;
56

67
import org.springframework.data.jpa.repository.JpaRepository;
78
import org.springframework.stereotype.Repository;
@@ -12,56 +13,39 @@
1213
* Spring Data JPA Repository for {@link Player} entities.
1314
* <p>
1415
* Provides data access methods for the {@code players} table using Spring Data's repository abstraction.
15-
* Extends {@link JpaRepository} for CRUD operations, batch operations, and query methods.
16+
* Extends {@link JpaRepository} for CRUD operations keyed on the UUID primary key.
1617
* </p>
1718
*
1819
* <h3>Provided Methods:</h3>
1920
* <ul>
20-
* <li><b>Inherited from JpaRepository:</b> save, findAll (returns List), findById, delete, flush, etc.</li>
21-
* <li><b>Custom Query Methods:</b> League search with case-insensitive wildcard matching</li>
22-
* <li><b>Derived Queries:</b> findBySquadNumber (method name conventions)</li>
23-
* </ul>
24-
*
25-
* <h3>Query Strategies:</h3>
26-
* <ul>
27-
* <li><b>Derived Queries:</b> Spring Data derives queries from method names (findBySquadNumber,
28-
* findByLeagueContainingIgnoreCase)</li>
21+
* <li><b>Inherited from JpaRepository:</b> save, findAll, findById(UUID), existsById, deleteById, etc.</li>
22+
* <li><b>Derived Queries:</b> findBySquadNumber, findByLeagueContainingIgnoreCase</li>
2923
* </ul>
3024
*
3125
* @see Player
3226
* @see org.springframework.data.jpa.repository.JpaRepository
33-
* @see <a href=
34-
* "https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation">Query
35-
* Creation from Method Names</a>
3627
* @since 4.0.2025
3728
*/
3829
@Repository
39-
public interface PlayersRepository extends JpaRepository<Player, Long> {
30+
public interface PlayersRepository extends JpaRepository<Player, UUID> {
4031

4132
/**
4233
* Finds a player by their squad number (exact match).
4334
* <p>
44-
* This is a derived query method - Spring Data JPA generates the query automatically.
45-
* Squad numbers are jersey numbers that users recognize (e.g., Messi is #10).
46-
* This demonstrates Spring Data's method name query derivation with a natural key.
35+
* Squad numbers are unique jersey numbers (e.g., Messi is #10).
36+
* Used as the natural key for mutation endpoints (PUT, DELETE).
4737
* </p>
4838
*
49-
* @param squadNumber the squad number to search for (jersey number, typically
50-
* 1-99)
39+
* @param squadNumber the squad number to search for (jersey number, typically 1-99)
5140
* @return an Optional containing the player if found, empty Optional otherwise
5241
*/
5342
Optional<Player> findBySquadNumber(Integer squadNumber);
5443

5544
/**
5645
* Finds players by league name using case-insensitive wildcard matching.
57-
* <p>
58-
* This method uses Spring Data's derived query mechanism to perform partial matching.
59-
* For example, searching for "Premier" will match "Premier League".
60-
* </p>
6146
*
6247
* @param league the league name to search for (partial matches allowed)
63-
* @return a list of players whose league name contains the search term (empty
64-
* list if none found)
48+
* @return a list of players whose league name contains the search term
6549
*/
6650
List<Player> findByLeagueContainingIgnoreCase(String league);
6751
}

0 commit comments

Comments
 (0)