When you're building a Java backend with Spring, sooner or later you'll talk to a relational database. Writing raw SQL for every read and write doesn't scale very well, and that's exactly the problem JPA — the Java Persistence API — was designed to solve.
JPA provides a set of concepts and guidelines that describe how to manage relational data inside Java applications. Annotations are the everyday surface of it: they let you tell JPA, in a declarative way, how your Java objects map to database tables.
Class-level annotations
These describe the table itself.
@Entity
Tells JPA that a class corresponds to a database table. Without it, JPA simply doesn't know the class exists.
src/main/java/com/example/User.java1@Entity
2public class User {
3 // fields
4}
@Table
Customizes the table's name (and optionally schema/catalog) when you don't want it to match the class name.
1@Entity
2@Table(name = "app_users")
3public class User {
4 // ...
5}
Field-level annotations
These describe individual columns.
@Id
Marks the primary key of the entity. Every @Entity class needs one.
@GeneratedValue
Tells JPA how to generate primary key values — usually IDENTITY (database auto-increment) or SEQUENCE (database sequence).
@Column
Configures the column the field maps to: its name, nullability, length, uniqueness, and so on.
1@Entity
2@Table(name = "app_users")
3public class User {
4 @Id
5 @GeneratedValue(strategy = GenerationType.IDENTITY)
6 private Long id;
7
8 @Column(name = "email", nullable = false, unique = true, length = 255)
9 private String email;
10
11 @Column(name = "full_name")
12 private String fullName;
13}
Relationship annotations
This is where JPA really pays for itself. Relational databases describe relationships through foreign keys; JPA lets you describe them as object references.
@OneToMany
One entity owns a collection of another. A User has many Orders:
1@Entity
2public class User {
3 @Id @GeneratedValue
4 private Long id;
5
6 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
7 private List<Order> orders;
8}
@ManyToOne
The flip side of the relationship — many Orders belong to one User. This is the side that actually holds the foreign key column.
1@Entity
2public class Order {
3 @Id @GeneratedValue
4 private Long id;
5
6 @ManyToOne
7 @JoinColumn(name = "user_id")
8 private User user;
9}
@OneToOne
Exactly one record on each side. Useful for profile-style data you want to keep in a separate table.
1@Entity
2public class User {
3 @OneToOne(cascade = CascadeType.ALL)
4 @JoinColumn(name = "profile_id", referencedColumnName = "id")
5 private Profile profile;
6}
@ManyToMany
Both sides hold many of the other — wired up through a join table.
1@Entity
2public class Student {
3 @ManyToMany
4 @JoinTable(
5 name = "student_course",
6 joinColumns = @JoinColumn(name = "student_id"),
7 inverseJoinColumns = @JoinColumn(name = "course_id")
8 )
9 private List<Course> courses;
10}
A worked example
Here's a small domain with a User who places many Orders, with all the annotations in one place:
src/main/java/com/example/model/User.java 1@Entity
2@Table(name = "app_users")
3public class User {
4
5 @Id
6 @GeneratedValue(strategy = GenerationType.IDENTITY)
7 private Long id;
8
9 @Column(nullable = false, unique = true)
10 private String email;
11
12 @Column(name = "full_name")
13 private String fullName;
14
15 @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
16 private List<Order> orders = new ArrayList<>();
17
18 // getters and setters
19}
src/main/java/com/example/model/Order.java 1@Entity
2@Table(name = "orders")
3public class Order {
4
5 @Id
6 @GeneratedValue(strategy = GenerationType.IDENTITY)
7 private Long id;
8
9 @Column(nullable = false)
10 private BigDecimal amount;
11
12 @ManyToOne(fetch = FetchType.LAZY)
13 @JoinColumn(name = "user_id", nullable = false)
14 private User user;
15}
Two annotations are doing the heavy lifting:
mappedBy = "user"on theUserside tells JPA thatUseris the inverse side of the relationship —Order.userowns the foreign key.@JoinColumn(name = "user_id")on theOrderside names the foreign key column.
Common pitfalls
- Forgetting to make collections lazy. Eager fetching of
@OneToManycollections will quietly load the whole graph on every query. Default toFetchType.LAZYand reach forFetchType.EAGERonly when you actually need it. - Missing
equals/hashCodeon entities. Hibernate-managed entities should have these defined based on stable business keys, not the generated id. Otherwise sets and maps misbehave once the entity gets persisted. - Cascading more than you mean to.
CascadeType.ALLis convenient — and dangerous. Removing a parent will delete every child, which may or may not be what you want.
Wrap-up
JPA annotations turn the database schema into an object graph that Java can reason about. The five class- and field-level annotations (@Entity, @Table, @Id, @GeneratedValue, @Column) cover the everyday mapping, and the four relationship annotations (@OneToMany, @ManyToOne, @OneToOne, @ManyToMany) cover the rest.
Once you've internalized them, building a Spring backend feels less like writing SQL plumbing and more like describing your domain — which is the whole point.
