Skip to content

Project Structure

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.

File Structure

├── [Project Name]
│   ├── .gradle
│   ├── build
│   ├── src
│   │   ├── main
│   │   │   ├── java
│   │   │   │   ├── com.thomaswildetech.structure
│   │   │   │   │   ├── config
│   │   │   │   │   │   ├── models
│   │   │   │   │   │   │   └── ErrorResponse.java
│   │   │   │   │   │   ├── security
│   │   │   │   │   │   ├── InitializeDbData.java
│   │   │   │   │   │   ├── JsonConfig.java
│   │   │   │   │   │   ├── RestExceptionHandler.java
│   │   │   │   │   │   └── SchedulingConfig.java
│   │   │   │   │   ├── controllers
│   │   │   │   │   │   └── league
│   │   │   │   │   │       └── PlayerController.java
│   │   │   │   │   ├── entities
│   │   │   │   │   │   ├── awards
│   │   │   │   │   │   │   ├── Award.java
│   │   │   │   │   │   │   └── AwardReceived.java
│   │   │   │   │   │   ├── league
│   │   │   │   │   │   │   ├── Coach.java
│   │   │   │   │   │   │   ├── Player.java
│   │   │   │   │   │   │   ├── Position.java
│   │   │   │   │   │   │   └── Team.java
│   │   │   │   │   │   └── schedule
│   │   │   │   │   │       └── Match.java
│   │   │   │   │   ├── repositories
│   │   │   │   │   │   └── league
│   │   │   │   │   │       └── PlayerRepository.java
│   │   │   │   │   ├── scheduled
│   │   │   │   │   ├── services
│   │   │   │   │   │   └── league
│   │   │   │   │   │       └── PlayerRepository.java
│   │   │   │   │   └── StructureApplication.java
│   │   │   └── resources
│   │   │       ├── application.properties
│   │   │       ├── application-api.properties
│   │   │       ├── application-local.properties
│   │   │       ├── application-scheduling.properties
│   │   │       └── data.sql
│   │   └── test
│   ├── .gitignore
│   ├── build.gradle
│   └── settings.json
└──

Entities

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
    }
Player Entity

Player.java
    @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"),
        }
    )
    public class Player {

        @Id // (3)
        @GeneratedValue(strategy = GenerationType.IDENTITY) // (4)
        private Long id; // (5)

        @Column(length = 50) // (6)
        private String firstName;

        @Column(length = 50)
        private String lastName;

        @ManyToOne(fetch = FetchType.EAGER) // (7)
        @JoinColumn(name = "position_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "FK_PLAYER_TO_POSITION")) // (8)
        private Position position;

        @ManyToOne
        @JoinColumn(name = "team_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "FK_PLAYER_TO_TEAM"))
        @JsonIgnoreProperties("roster")
        private Team team;

        @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")
        private List<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)
        private List<Team> teamsPlayedFor;

        private Boolean franchiseTagged;

        @Transient
        @JsonIgnore
        private String someTempVariable;
        // Getters and Setters
    }

  1. @Entity designates this POJO as a database table mapping
  2. @Table allows us to specify properties of the table, such as the name, indexes, and unique contraints
  3. 🔑 Primary Key for the database table
  4. 🪪 Will use the database auto-increment feature to populate the id value on insertions
  5. 🪪 We use nullable Long instead of long so that it's easy for Spring to know if any entity has been persisted or not
  6. @Column allows us to customize the column, like it's name, length, nullability, as well as a custom definition
  7. @ManyToOne means that this entity references an entity of another table. The other entity may referene "Many" of this entity. Default fetching is "Eager"
  8. 🤲 @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.

Major Topics

  • Primary key and generated value
  • Relationships to other tables (foreign keys)

    • @OneToOne
    • @ManyToOne
    • @OneToMany
    • @ManyToMany
  • Cascading actions taken by Spring

  • @OnDelete foreign key constraint actions

Repositories

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.

Player Repository

@Repository
public interface PlayerRepository extends JpaRepository<Player, Long> {

    @Override
    @EntityGraph(attributePaths = {"team", "position"})  // Can't fetch mutliple "bags" (lists) with EntityGraph
    List<Player> findAll();

    @Override
    @EntityGraph(attributePaths = {"team", "position", "teamsPlayedFor", "receivedAwards", "receivedAwards.award"}) // Can fetch multiple "bags" when returning only 1 result
    Optional<Player> findById(Long id);
}

Major Topics

  • Usage of @EntityGraph to dictate fetching of properties that relate to other tables.
  • Custom methods using method naming convention.
  • Custom methods using @Query.
  • Option to use JDBC instead

Services

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.

Player Service

@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) {

        // If we are cascading/persisting new received awards, we need to fetch the award from the database
        if(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 database
        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);
    }
}

Major Topics

  • Utilizing services for database transactions.
  • Handling of cascading relationships
  • Usage of transient data transfer objects (DTOs)

Controllers

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.

Player Controller

@RestController
@RequestMapping("/player")
@Profile("api")
public class PlayerController{

    private static final Logger log = LoggerFactory.getLogger(PlayerController.class);

    private final PlayerService playerService;

    @Autowired
    public PlayerController(PlayerService playerService) {
        this.playerService = playerService;
    }

    @GetMapping
    public ResponseEntity<List<Player>> findAll() {
        return ResponseEntity.ok(playerService.findAll());
    }

    @GetMapping("/{id}")
    public ResponseEntity<Player> findById(@PathVariable("id") Long id) {
        log.debug("Finding player by id: {}", id);
        return playerService.findById(id)
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND,
                        "Player not found with id: " + id
                ));
    }

    @PostMapping
    public ResponseEntity<Player> create(@RequestBody Player player) {
        return ResponseEntity.ok(playerService.save(player));
    }

    @PutMapping("/{id}")
    public ResponseEntity<Player> update(@PathVariable("id") Long id, @RequestBody Player player) {
        player.setId(id);
        return ResponseEntity.ok(playerService.save(player));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<Player> patchPlayer(Long id, Map<String, Object> updates) {
        Player existingPlayer = playerService.findById(id)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND,
                        "Player not found with id: " + id
                ));

        return ResponseEntity.ok(playerService.patch(existingPlayer, updates));

    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable("id") Long id) {
        playerService.deleteById(id);
        return ResponseEntity.ok(null);
    }

}

Major Topics

  • Implementing HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Error handling

Scheduled

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.

Major Topics

  • Enabling
  • Scheduling options

Config

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.

Major Topics

  • Json Configuration
  • Customization from application.properties
  • Security
  • Exception Handling
  • Data Initialization

Application Properties

Major Topics

  • application.properties

    • Important Properties to define
    • Profiles
  • build.gradle

  • settings.gradle
Property Files

# 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

  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
  2. Used if you want to execute a sql script at the start of the program.
  3. This is a custom property for enabling the conditional config SchedulingConfig
  4. The default setting is servlet, but we may want a microservice config that isn't a web application.

1
2
3
4
5
6
7
8
# 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 

  1. These settings configure the application using the profile to be a web server running on local host, port 8080, with a path of /api.
  2. If we configure a global Executor Service for background threads it will be initialized with this number.

# Database Configuration (1)
spring.datasource.url= jdbc:mysql://localhost:3306/tutorials?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=tutorial_master
spring.datasource.password=tutorial

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

# SQL Printing
spring.jpa.show-sql=true # (2)
spring.jpa.properties.hibernate.format_sql=true # (3)
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE # (4)

# Max database connections (5)
spring.datasource.maxActive=10

# Allow for batch inserts (6)
spring.jpa.properties.hibernate.jdbc.batch_size=500
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

# DDL: none, validate, update, create, create-drop (7)
spring.jpa.hibernate.ddl-auto=create-drop

# Logging Levels (8)
logging.level.com.thomaswildetech=TRACE

# Logging Pattern
logging.pattern.console=%clr(%d %-7level %-60logger{36} Line %-5L %-40M - %msg%n)

# Allows for color logging
spring.output.ansi.enabled=always

  1. Database URL, username, and password are defined here. Note, for production, we can simply use a different profile.
  2. Shows hibernates SQL calls. Great for ensuring hibernate is performing as expected.
  3. Formats the SQL ouput in a readable format.
  4. Shows the inputs into the SQL prepared statements.
  5. Max database connections depending on frequency of user calls.
  6. Mostly for doing batch inserts through a job.
    • create-drop: Drops all tables before recreating them
    • create: Create new tables based on new entities created
    • update: Update statements based on new properties within entities
    • none: Fastest, no DDL executed
  7. Set logging levels for your any packages.

scheduling.enabled=true
spring.task.scheduling.pool.size=5

Sample Code

Entity/Repository/Service/Controller Code

Player.java
    @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"),
        }
    )
    public class Player {

        @Id // (3)
        @GeneratedValue(strategy = GenerationType.IDENTITY) // (4)
        private Long id; // (5)

        @Column(length = 50) // (6)
        private String firstName;

        @Column(length = 50)
        private String lastName;

        @ManyToOne(fetch = FetchType.EAGER) // (7)
        @JoinColumn(name = "position_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "FK_PLAYER_TO_POSITION")) // (8)
        private Position position;

        @ManyToOne
        @JoinColumn(name = "team_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "FK_PLAYER_TO_TEAM"))
        @JsonIgnoreProperties("roster")
        private Team team;

        @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")
        private List<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)
        private List<Team> teamsPlayedFor;

        private Boolean franchiseTagged;

        @Transient
        @JsonIgnore
        private String someTempVariable;
        // Getters and Setters
    }

  1. @Entity designates this POJO as a database table mapping
  2. @Table allows us to specify properties of the table, such as the name, indexes, and unique contraints
  3. 🔑 Primary Key for the database table
  4. 🪪 Will use the database auto-increment feature to populate the id value on insertions
  5. 🪪 We use nullable Long instead of long so that it's easy for Spring to know if any entity has been persisted or not
  6. @Column allows us to customize the column, like it's name, length, nullability, as well as a custom definition
  7. @ManyToOne means that this entity references an entity of another table. The other entity may referene "Many" of this entity. Default fetching is "Eager"
  8. 🤲 @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.

@Repository
public interface PlayerRepository extends JpaRepository<Player, Long> {

    @Override
    @EntityGraph(attributePaths = {"team", "position"})  // Can't fetch mutliple "bags" (lists) with EntityGraph
    List<Player> findAll();

    @Override
    @EntityGraph(attributePaths = {"team", "position", "teamsPlayedFor", "receivedAwards", "receivedAwards.award"}) // Can fetch multiple "bags" when returning only 1 result
    Optional<Player> findById(Long id);
}

@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) {

        // If we are cascading/persisting new received awards, we need to fetch the award from the database
        if(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 database
        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);
    }
}

@RestController
@RequestMapping("/player")
@Profile("api")
public class PlayerController{

    private static final Logger log = LoggerFactory.getLogger(PlayerController.class);

    private final PlayerService playerService;

    @Autowired
    public PlayerController(PlayerService playerService) {
        this.playerService = playerService;
    }

    @GetMapping
    public ResponseEntity<List<Player>> findAll() {
        return ResponseEntity.ok(playerService.findAll());
    }

    @GetMapping("/{id}")
    public ResponseEntity<Player> findById(@PathVariable("id") Long id) {
        log.debug("Finding player by id: {}", id);
        return playerService.findById(id)
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND,
                        "Player not found with id: " + id
                ));
    }

    @PostMapping
    public ResponseEntity<Player> create(@RequestBody Player player) {
        return ResponseEntity.ok(playerService.save(player));
    }

    @PutMapping("/{id}")
    public ResponseEntity<Player> update(@PathVariable("id") Long id, @RequestBody Player player) {
        player.setId(id);
        return ResponseEntity.ok(playerService.save(player));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<Player> patchPlayer(Long id, Map<String, Object> updates) {
        Player existingPlayer = playerService.findById(id)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND,
                        "Player not found with id: " + id
                ));

        return ResponseEntity.ok(playerService.patch(existingPlayer, updates));

    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable("id") Long id) {
        playerService.deleteById(id);
        return ResponseEntity.ok(null);
    }

}

Database Structure

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
  }

Comments