WL
Java Full Stack Developer
Wassim Lagnaoui

Tutorial 02: Entities and JPA

Build the core entities and relationships using JPA.

← Back to Tutorials

Tutorial 02 — Entities and JPA

A practical, step-by-step tutorial to build the domain model. Each step shows a small code snippet and a short, English explanation.


Step-by-step: Build entities and explain as they are created

0. Prerequisites

Required dependencies include:

Dev config (application.yml), example:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/restaurant
    username: postgres
    password: postgres
  jpa:
    hibernate:
      ddl-auto: update  # dev only; prefer Flyway/Liquibase in production
    show-sql: true

Explanation:


1. Create the model package

Create a package:

com.wassimlagnaoui.RestaurantOrder.model

Purpose:
Keep all entities and enums together so component scanning and repository wiring stay predictable. Grouping the domain model under one package also makes it easy to apply consistent conventions and to find related code (entities, enums, and later their DTOs and repositories in neighboring packages).


2. MenuItem — a flat entity

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "menu_item")
public class MenuItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, updatable = false)
    private Long id;

    @Column(name = "name", nullable = false, length = 100)
    private String name; // name shown in the menu

    @Column(name = "description", length = 500)
    private String description;

    @Column(name = "price", nullable = false)
    private Double price;

    @Column(name = "image_url", length = 255)
    private String imageUrl;

    @Column(name = "category", length = 50)
    private String category;

    @Column(name = "available", nullable = false)
    private boolean available;
}

Explanation:


3. Order — parent aggregate with relationships

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Double total;
    private LocalDateTime orderDate;

    // Option A: store as String; Option B: use enum with @Enumerated
    private String status;
    // @Enumerated(EnumType.STRING)
    // private OrderStatus status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> items;

    @ManyToOne
    @JoinColumn(name = "table_session_id")
    private TableSession tableSession;
}

Explanation:


4. OrderItem — owning side of the Order relationship

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Integer quantity;

    @Column(name = "served")
    private Boolean served = false;

    @ManyToOne
    private MenuItem menuItem;

    @ManyToOne
    private Order order; // owning side: holds the FK used by mappedBy in Order
}

Explanation:


5. TableSession — grouping orders by table/time

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TableSession {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime sessionStart;
    private LocalDateTime sessionEnd;
    private String tableNumber;

    @OneToMany(mappedBy = "tableSession")
    private List<Order> orders;
}

Explanation:


6. OrderStatus — domain enum for lifecycle

public enum OrderStatus { PLACED, PREPARING, READY, SERVED, CANCELLED }

Explanation:


7. User — integrate with Spring Security

@Entity
@Table(name = "users")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User implements UserDetails {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    @Email(message = "Valid Email Required")
    private String email;

    private String password;
    private String phone;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(role); }
    @Override public String getUsername() { return this.email; }
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

Explanation:


8. Staff — reference data for access checks and metadata

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Staff {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, updatable = false)
    private Long id;

    @Column(name = "first_name", nullable = false, length = 50)
    private String firstName;

    @Column(name = "last_name", nullable = false, length = 50)
    private String lastName;

    @Column(name = "email", nullable = false, unique = true, length = 120)
    private String email;

    @Column(name = "employee_id", nullable = false, unique = true)
    private Long employeeId;

    @Column(name = "role", nullable = false, length = 40)
    private String role; // e.g., waiter, chef, manager
}

Explanation:


9. Key Takeaways: Naming, validation, and migrations

Tips:


Annotation Summary