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.
erDiagram
PLAYER ||--|| PlayerTable : maps
PLAYER {
Long id
String firstName
String lastName
Team team
Position position
Boolean frachiseTagged
}
PlayerTable {
bigint id
String first_name
String last_name
bigint team_id
String position_id
bit franchise_tagged
}
@Entity// (1)@Table(name="lu_player",// (2)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// (3)@GeneratedValue(strategy=GenerationType.IDENTITY)// (4)privateLongid;// (5)@Column(length=50)// (6)privateStringfirstName;@Column(length=50)privateStringlastName;@ManyToOne(fetch=FetchType.EAGER)// (7)@JoinColumn(name="position_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TO_POSITION"))// (8)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}
@Entity designates this POJO as a database table mapping
@Table allows us to specify properties of the table, such as the name, indexes, and unique contraints
Primary Key for the database table
Will use the database auto-increment feature to populate the id value on insertions
We use nullable Long instead of long so that it's easy for Spring to know if any entity has been persisted or not
@Column allows us to customize the column, like it's name, length, nullability, as well as a custom definition
@ManyToOne means that this entity references an entity of another table. The other entity may referene "Many" of this entity. Default fetching is "Eager"
@JoinColumn or @JoinColumns specifies the name of the column(s) in this table, and which columns they reference from the other entity. A foreign key constrain will automatically be applied and can be named.
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 (1)spring.profiles.active=local,api
# Initialize Sql Statements (2)spring.datasource.initialization-mode=NEVER
#spring.datasource.data=data.sql# Scheduling Config (default setting: will be overridden by scheduling profile) (3)scheduling.enabled=false
# Web Application Type (default setting: will be overridden by api profile) (4)spring.main.web-application-type=none
server.port=-1
Profiles can be used to trigger additional property files and can be used in Beans with @Profile() so that Spring only loads the Bean when the profile is activated. This can be very useful if you want multiple builds for different microservices. local and api will load both application-api.properties and application-local.properties
Used if you want to execute a sql script at the start of the program.
This is a custom property for enabling the conditional config SchedulingConfig
The default setting is servlet, but we may want a microservice config that isn't a web application.
# API Details (1)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 (2)thread.pool.size=5
These settings configure the application using the profile to be a web server running on local host, port 8080, with a path of /api.
If we configure a global Executor Service for background threads it will be initialized with this number.
@Entity// (1)@Table(name="lu_player",// (2)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// (3)@GeneratedValue(strategy=GenerationType.IDENTITY)// (4)privateLongid;// (5)@Column(length=50)// (6)privateStringfirstName;@Column(length=50)privateStringlastName;@ManyToOne(fetch=FetchType.EAGER)// (7)@JoinColumn(name="position_id",referencedColumnName="id",foreignKey=@ForeignKey(name="FK_PLAYER_TO_POSITION"))// (8)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}
@Entity designates this POJO as a database table mapping
@Table allows us to specify properties of the table, such as the name, indexes, and unique contraints
Primary Key for the database table
Will use the database auto-increment feature to populate the id value on insertions
We use nullable Long instead of long so that it's easy for Spring to know if any entity has been persisted or not
@Column allows us to customize the column, like it's name, length, nullability, as well as a custom definition
@ManyToOne means that this entity references an entity of another table. The other entity may referene "Many" of this entity. Default fetching is "Eager"
@JoinColumn or @JoinColumns specifies the name of the column(s) in this table, and which columns they reference from the other entity. A foreign key constrain will automatically be applied and can be named.
@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);}}
classDiagram
lu_player "1" -- "N" j_award_received : has
lu_player "1" -- "N" j_player_team : has
lu_player "1" -- "N" j_coach_favorite_player : has
lu_award "1" -- "N" j_award_received : has
lu_team "1" -- "N" d_match : has
lu_team "1" -- "N" lu_coach : has
lu_position "1" -- "N" lu_player : has
lu_team "1" -- "N" lu_player : belongs to
lu_player "0..1" -- "1" lu_coach : has
lu_team "1" -- "N" j_player_team : has
lu_player "1" -- "N" j_player_team : has
lu_coach "1" -- "N" j_coach_favorite_player : has
lu_player "1" -- "N" j_coach_favorite_player : has
d_match "1" -- "N" j_player_team : involves
d_match "1" -- "N" lu_team : home team
d_match "1" -- "N" lu_team : away team
class lu_award{
+bigint id
+String name
+String description
}
class j_award_received{
+int season
+bigint award_id
+bigint player_id
+String notes
}
class lu_coach{
+bigint id
+bigint team_id
+String first_name
+String last_name
}
class lu_player{
+bit franchise_tagged
+bigint id
+bigint team_id
+String first_name
+String last_name
+String position_id
}
class lu_position{
String id
String name
}
class lu_team{
+BOOLEAN active
+String abbreviation
+bigint id
+String color
+String city
+String display_name
+String name
}
class d_match{
+int away_score
+int home_score
+int season
+int week
+bigint away_team_id
+bigint home_team_id
+bigint id
}
class j_player_team{
+bigint player_id
+bigint team_id
}
class j_coach_favorite_player{
+bigint coach_id
+bigint player_id
}