Getting a preview of the structure of a typical spring boot is a great way to get a head start on developing a project. Before you begin your first project, it's helpful to know where you will be creating classes, and a workflow for your development.
Entity classes which have the are POJOs (plain old java objects) that map to database tables and whose properties map to the columns of that table. It is the basis for what is considered an ORM (object relational mapping), in which programmatic instances of these classes map directly to and entry in the table and whose changes to their properties get reflected in the database.
@Entity// @Table(name="lu_player",// indexes={@Index(columnList="firstName, lastName",name="idx_lu_player_first_name"),@Index(columnList="lastName, firstName",name="idx_lu_player_last_name"),@Index(columnList="position_id",name="idx_lu_player_position"),@Index(columnList="team_id",name="idx_lu_player_team"),})publicclassPlayer{@Id// @GeneratedValue(strategy=GenerationType.IDENTITY)// privateLongid;// @Column(length=50)// privateStringfirstName;@Column(length=50)privateStringlastName;@ManyToOne(fetch=FetchType.EAGER)// @JoinColumn(name="position_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TO_POSITION"))// privatePositionposition;@ManyToOne@JoinColumn(name="team_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TO_TEAM"))@JsonIgnoreProperties("roster")privateTeamteam;@OneToMany(mappedBy="player",cascade={CascadeType.PERSIST,CascadeType.MERGE})// Ensure at the database level that if a player is deleted, all of their awards are also deleted@OnDelete(action=OnDeleteAction.CASCADE)@JsonIgnoreProperties("player")privateList<AwardReceived>receivedAwards;@ManyToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE})@JoinTable(name="j_player_team",joinColumns=@JoinColumn(name="player_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TEAM_TO_PLAYER")),inverseJoinColumns=@JoinColumn(name="team_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TEAM_TO_TEAM")))// Spring knows to delete the join table records when a player is deleted// However, without an OnDelete Cascade, you cannot delete a player from the database because you will get a FK constrain violation@OnDelete(action=OnDeleteAction.CASCADE)privateList<Team>teamsPlayedFor;privateBooleanfranchiseTagged;@Transient@JsonIgnoreprivateStringsomeTempVariable;// Getters and Setters}
Repositories are typed interfaces which are responsible for all database transactions for a particular entity. So if you want to be able to make changes to a Player entity, you would want to have a repository dedicated to Player, likely called PlayerRepository. Spring Boot already have default implementations for methods such as findById(ID id), findAll()save(T entity), and delete(). SpringBoot also makes it very easy to create custom repository methods.
@RepositorypublicinterfacePlayerRepositoryextendsJpaRepository<Player,Long>{@Override@EntityGraph(attributePaths={"team","position"})// Can't fetch mutliple "bags" (lists) with EntityGraphList<Player>findAll();@Override@EntityGraph(attributePaths={"team","position","teamsPlayedFor","receivedAwards","receivedAwards.award"})// Can fetch multiple "bags" when returning only 1 resultOptional<Player>findById(Longid);}
Major Topics
Usage of @EntityGraph to dictate fetching of properties that relate to other tables.
Services are where all "business logic" will reside. This is where data should be extracted from repositories and any further data manipulation should be made. Generally, you will likely repeat any methods created in a repository here in Services, and allowing you perform additional logic which is usually necessary in JPA transactions as it is.
@ServicepublicclassPlayerService{privatefinalPlayerRepositoryplayerRepository;privatefinalAwardRepositoryawardRepository;privatefinalTeamRepositoryteamRepository;@AutowiredpublicPlayerService(PlayerRepositoryplayerRepository,AwardRepositoryawardRepository,TeamRepositoryteamRepository){this.playerRepository=playerRepository;this.awardRepository=awardRepository;this.teamRepository=teamRepository;}publicList<Player>findAll(){returnplayerRepository.findAll();}publicOptional<Player>findById(Longid){returnplayerRepository.findById(id);}publicPlayersave(Playerplayer){// If we are cascading/persisting new received awards, we need to fetch the award from the databaseif(player.getReceivedAwards()!=null){player.getReceivedAwards().forEach(awardReceived->{Optional<Award>award=awardRepository.findById(awardReceived.getAward().getId());award.ifPresent(awardReceived::setAward);awardReceived.setPlayer(player);});}// If we are cascading/persisting teams played for, we need to fetch the teams from the databaseif(player.getTeamsPlayedFor()!=null){for(inti=0;i<player.getTeamsPlayedFor().size();i++){Teamteam=player.getTeamsPlayedFor().get(i);Optional<Team>teamFromDb=teamRepository.findById(team.getId());intfinalI=i;teamFromDb.ifPresent(fetchedTeam->player.getTeamsPlayedFor().set(finalI,fetchedTeam));}}returnplayerRepository.save(player);}publicPlayerpatch(PlayerexistingPlayer,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;}}returnplayerRepository.save(existingPlayer);}publicvoiddeleteById(Longid){playerRepository.deleteById(id);}}
Controllers are responsible for handling REST requests and responding to them. Methods should be relatively simple and should invoke service calls rather than implementing any business logic themselves.
@RestController@RequestMapping("/player")@Profile("api")publicclassPlayerController{privatestaticfinalLoggerlog=LoggerFactory.getLogger(PlayerController.class);privatefinalPlayerServiceplayerService;@AutowiredpublicPlayerController(PlayerServiceplayerService){this.playerService=playerService;}@GetMappingpublicResponseEntity<List<Player>>findAll(){returnResponseEntity.ok(playerService.findAll());}@GetMapping("/{id}")publicResponseEntity<Player>findById(@PathVariable("id")Longid){log.debug("Finding player by id: {}",id);returnplayerService.findById(id).map(ResponseEntity::ok).orElseThrow(()->newResponseStatusException(HttpStatus.NOT_FOUND,"Player not found with id: "+id));}@PostMappingpublicResponseEntity<Player>create(@RequestBodyPlayerplayer){returnResponseEntity.ok(playerService.save(player));}@PutMapping("/{id}")publicResponseEntity<Player>update(@PathVariable("id")Longid,@RequestBodyPlayerplayer){player.setId(id);returnResponseEntity.ok(playerService.save(player));}@PatchMapping("/{id}")publicResponseEntity<Player>patchPlayer(Longid,Map<String,Object>updates){PlayerexistingPlayer=playerService.findById(id).orElseThrow(()->newResponseStatusException(HttpStatus.NOT_FOUND,"Player not found with id: "+id));returnResponseEntity.ok(playerService.patch(existingPlayer,updates));}@DeleteMapping("/{id}")publicResponseEntity<Void>delete(@PathVariable("id")Longid){playerService.deleteById(id);returnResponseEntity.ok(null);}}
Major Topics
Implementing HTTP methods (GET, POST, PUT, PATCH, DELETE)
If your application has scheduled tasks, I like having a dedicated package for classes with scheduled methods. Similar to controllers, all the logic should reside in services and the methods here should be very simple.
This is where we would see json serialization configuation, scheduling configuration, and most importantly, security configuration, where we apply custom request filters and parse JWTs for example.
# Profiles spring.profiles.active=local,api
# Initialize Sql Statements spring.datasource.initialization-mode=NEVER
#spring.datasource.data=data.sql# Scheduling Config (default setting: will be overridden by scheduling profile) scheduling.enabled=false
# Web Application Type (default setting: will be overridden by api profile) spring.main.web-application-type=none
server.port=-1
# API Details server.address=127.0.0.1
server.port=8080
server.servlet.context-path=/api
spring.main.web-application-type=servlet
# Used By Executor Service Config thread.pool.size=5
@Entity// @Table(name="lu_player",// indexes={@Index(columnList="firstName, lastName",name="idx_lu_player_first_name"),@Index(columnList="lastName, firstName",name="idx_lu_player_last_name"),@Index(columnList="position_id",name="idx_lu_player_position"),@Index(columnList="team_id",name="idx_lu_player_team"),})publicclassPlayer{@Id// @GeneratedValue(strategy=GenerationType.IDENTITY)// privateLongid;// @Column(length=50)// privateStringfirstName;@Column(length=50)privateStringlastName;@ManyToOne(fetch=FetchType.EAGER)// @JoinColumn(name="position_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TO_POSITION"))// privatePositionposition;@ManyToOne@JoinColumn(name="team_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TO_TEAM"))@JsonIgnoreProperties("roster")privateTeamteam;@OneToMany(mappedBy="player",cascade={CascadeType.PERSIST,CascadeType.MERGE})// Ensure at the database level that if a player is deleted, all of their awards are also deleted@OnDelete(action=OnDeleteAction.CASCADE)@JsonIgnoreProperties("player")privateList<AwardReceived>receivedAwards;@ManyToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE})@JoinTable(name="j_player_team",joinColumns=@JoinColumn(name="player_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TEAM_TO_PLAYER")),inverseJoinColumns=@JoinColumn(name="team_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TEAM_TO_TEAM")))// Spring knows to delete the join table records when a player is deleted// However, without an OnDelete Cascade, you cannot delete a player from the database because you will get a FK constrain violation@OnDelete(action=OnDeleteAction.CASCADE)privateList<Team>teamsPlayedFor;privateBooleanfranchiseTagged;@Transient@JsonIgnoreprivateStringsomeTempVariable;// Getters and Setters}
@RepositorypublicinterfacePlayerRepositoryextendsJpaRepository<Player,Long>{@Override@EntityGraph(attributePaths={"team","position"})// Can't fetch mutliple "bags" (lists) with EntityGraphList<Player>findAll();@Override@EntityGraph(attributePaths={"team","position","teamsPlayedFor","receivedAwards","receivedAwards.award"})// Can fetch multiple "bags" when returning only 1 resultOptional<Player>findById(Longid);}
@ServicepublicclassPlayerService{privatefinalPlayerRepositoryplayerRepository;privatefinalAwardRepositoryawardRepository;privatefinalTeamRepositoryteamRepository;@AutowiredpublicPlayerService(PlayerRepositoryplayerRepository,AwardRepositoryawardRepository,TeamRepositoryteamRepository){this.playerRepository=playerRepository;this.awardRepository=awardRepository;this.teamRepository=teamRepository;}publicList<Player>findAll(){returnplayerRepository.findAll();}publicOptional<Player>findById(Longid){returnplayerRepository.findById(id);}publicPlayersave(Playerplayer){// If we are cascading/persisting new received awards, we need to fetch the award from the databaseif(player.getReceivedAwards()!=null){player.getReceivedAwards().forEach(awardReceived->{Optional<Award>award=awardRepository.findById(awardReceived.getAward().getId());award.ifPresent(awardReceived::setAward);awardReceived.setPlayer(player);});}// If we are cascading/persisting teams played for, we need to fetch the teams from the databaseif(player.getTeamsPlayedFor()!=null){for(inti=0;i<player.getTeamsPlayedFor().size();i++){Teamteam=player.getTeamsPlayedFor().get(i);Optional<Team>teamFromDb=teamRepository.findById(team.getId());intfinalI=i;teamFromDb.ifPresent(fetchedTeam->player.getTeamsPlayedFor().set(finalI,fetchedTeam));}}returnplayerRepository.save(player);}publicPlayerpatch(PlayerexistingPlayer,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;}}returnplayerRepository.save(existingPlayer);}publicvoiddeleteById(Longid){playerRepository.deleteById(id);}}
@RestController@RequestMapping("/player")@Profile("api")publicclassPlayerController{privatestaticfinalLoggerlog=LoggerFactory.getLogger(PlayerController.class);privatefinalPlayerServiceplayerService;@AutowiredpublicPlayerController(PlayerServiceplayerService){this.playerService=playerService;}@GetMappingpublicResponseEntity<List<Player>>findAll(){returnResponseEntity.ok(playerService.findAll());}@GetMapping("/{id}")publicResponseEntity<Player>findById(@PathVariable("id")Longid){log.debug("Finding player by id: {}",id);returnplayerService.findById(id).map(ResponseEntity::ok).orElseThrow(()->newResponseStatusException(HttpStatus.NOT_FOUND,"Player not found with id: "+id));}@PostMappingpublicResponseEntity<Player>create(@RequestBodyPlayerplayer){returnResponseEntity.ok(playerService.save(player));}@PutMapping("/{id}")publicResponseEntity<Player>update(@PathVariable("id")Longid,@RequestBodyPlayerplayer){player.setId(id);returnResponseEntity.ok(playerService.save(player));}@PatchMapping("/{id}")publicResponseEntity<Player>patchPlayer(Longid,Map<String,Object>updates){PlayerexistingPlayer=playerService.findById(id).orElseThrow(()->newResponseStatusException(HttpStatus.NOT_FOUND,"Player not found with id: "+id));returnResponseEntity.ok(playerService.patch(existingPlayer,updates));}@DeleteMapping("/{id}")publicResponseEntity<Void>delete(@PathVariable("id")Longid){playerService.deleteById(id);returnResponseEntity.ok(null);}}