Services¶
Services hold the business logic between controllers and repositories. Two patterns from the ESPN project I kept: re-fetching related entities before cascading saves, and applying partial PATCH updates field by field.
Pattern¶
@Service
public class PlayerService {
private final PlayerRepository playerRepository;
private final AwardRepository awardRepository;
private final TeamRepository teamRepository;
@Autowired
public PlayerService(PlayerRepository playerRepository,
AwardRepository awardRepository,
TeamRepository teamRepository) {
this.playerRepository = playerRepository;
this.awardRepository = awardRepository;
this.teamRepository = teamRepository;
}
public List<Player> findAll() {
return playerRepository.findAll();
}
public Optional<Player> findById(Long id) {
return playerRepository.findById(id);
}
public Player save(Player player) {
// Cascading new received awards: re-fetch the award from the DB
if (player.getReceivedAwards() != null) {
player.getReceivedAwards().forEach(awardReceived -> {
Optional<Award> award = awardRepository.findById(awardReceived.getAward().getId());
award.ifPresent(awardReceived::setAward);
awardReceived.setPlayer(player);
});
}
// Cascading teams played for: replace detached entities with managed ones
if (player.getTeamsPlayedFor() != null) {
for (int i = 0; i < player.getTeamsPlayedFor().size(); i++) {
Team team = player.getTeamsPlayedFor().get(i);
Optional<Team> teamFromDb = teamRepository.findById(team.getId());
int finalI = i;
teamFromDb.ifPresent(fetchedTeam -> player.getTeamsPlayedFor().set(finalI, fetchedTeam));
}
}
return playerRepository.save(player);
}
public Player patch(Player existingPlayer, Map<String, Object> updates) {
for (Map.Entry<String, Object> entry : updates.entrySet()) {
switch (entry.getKey()) {
case "firstName": existingPlayer.setFirstName((String) entry.getValue()); break;
case "lastName": existingPlayer.setLastName((String) entry.getValue()); break;
case "position": existingPlayer.setPosition((Position) entry.getValue()); break;
case "team": existingPlayer.setTeam((Team) entry.getValue()); break;
case "franchiseTagged": existingPlayer.setFranchiseTagged((Boolean) entry.getValue()); break;
}
}
return playerRepository.save(existingPlayer);
}
public void deleteById(Long id) {
playerRepository.deleteById(id);
}
}
Why the re-fetch matters¶
Incoming JSON carries detached references (an award id, a team id). JPA cascades save whatever object graph it's handed — so resolve each reference to a managed entity from its repository first, then save. Otherwise you risk inserting duplicates or stale rows.
The patch method is the service-side half of the PATCH endpoint in Controllers — the controller parses the map, the service applies it.