Database · Lesson 54 of 95
JPA Entity and @Entity
Learn how a JPA entity maps a Java class to a table in Spring Boot 4: @Entity, @Id, @Column, enums and @Transient, with a runnable bike rental app.
Walk into any bike rental shop and look at the register on the counter. Each row has the same columns: frame number, model, daily rate, and whether the bike is free or out. The shop owner never invents a new layout for every bike. One layout, many rows. In JPA, an entity class is that layout, and the JPA entity is the first thing you learn. It tells the database what a bike looks like, and every object of the class becomes one row.
In this topic you will learn what an entity is, which annotations shape the table, and how Hibernate turns your class into SQL. We will build a bike rental register and watch the database enforce the rules you write.
What is a JPA entity?
An entity is a plain Java class that is linked to a database table. Each object is one row, and each field is one column. You do this with annotations from the jakarta.persistence package.
A valid entity needs only a few things:
- The
@Entityannotation on the class. - A field marked with
@Id, which is the primary key that identifies each row. - A constructor with no arguments, which can be
protected. Hibernate uses it to create objects when it reads rows. - A class that is not
final, and fields that are notfinal, so Hibernate can fill them.
Everything else is optional. Without more annotations, Hibernate uses smart defaults: the table name comes from the class name, and the column names come from the field names.
Why is it used?
Without entities, you would write SQL for every table and copy each column into an object by hand. An entity keeps the table layout and the Java class in one place. When a column changes, you change one class.
The extra annotations let you state rules close to the field: this column cannot be empty, this text may be at most 60 characters, this value must be unique. The database then refuses bad data, even if a bug in your code tries to save it.
How it works
At startup, Hibernate scans your classes and builds a map between them and the tables.
text@Entity classes found | v Read annotations on fields | v Build the table layout | v CREATE TABLE (if ddl allows) | v Objects become rows
Spring Boot finds every class with @Entity under your main package. Hibernate reads the annotations and works out the table name, the columns, their types and their rules. If schema generation is on, it sends a CREATE TABLE statement. From then on, saving an object writes a row, and loading a row builds an object.
Here is how one object relates to one row.
textJava object Table row ----------- --------- Bike object --> 1 row in bikes id id (PK) modelName model_name frameNo frame_no status status
Each field maps to one column, and @Id marks the primary key. Names are changed from camelCase to snakecase, so `modelName` becomes `modelname`.
Real-Life Example
Think about a form that a bike shop uses when a new bike arrives. The form has boxes: model name (at most 60 letters), frame number (must be different for every bike), daily rate (two decimals), and a status tick box with three choices. The staff cannot leave the model or frame box empty. That printed form is the entity. Every bike that arrives fills one copy, and each filled copy goes into the register as one row. The shop trusts the form, because it does not accept a copy that breaks the rules.
Code Example
Let's build the register for a shop called Pedal Point. The bike entity has a table name, column rules, an enum status, a date, and one field that is not stored.
textrentals/ ├─ pom.xml └─ src/main/ ├─ java/com/pedalpoint/rentals/ │ ├─ RentalsApplication.java │ ├─ Bike.java │ ├─ BikeStatus.java │ ├─ BikeRepository.java │ └─ BikeRunner.java └─ resources/ └─ application.properties
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.pedalpoint</groupId> <artifactId>rentals</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: RentalsApplication.java in package com.pedalpoint.rentals
javapackage com.pedalpoint.rentals; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RentalsApplication { public static void main(String[] args) { SpringApplication.run(RentalsApplication.class, args); } }
File: BikeStatus.java in package com.pedalpoint.rentals
javapackage com.pedalpoint.rentals; public enum BikeStatus { AVAILABLE, RENTED, IN_REPAIR }
File: Bike.java in package com.pedalpoint.rentals
javapackage com.pedalpoint.rentals; import java.math.BigDecimal; import java.time.LocalDate; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.Transient; @Entity @Table(name = "bikes") public class Bike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "model_name", nullable = false, length = 60) private String modelName; @Column(name = "frame_no", nullable = false, unique = true, length = 20) private String frameNo; @Column(name = "daily_rate", nullable = false, precision = 8, scale = 2) private BigDecimal dailyRate; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 12) private BikeStatus status; private LocalDate boughtOn; @Transient private String shopNote; protected Bike() { } public Bike(String modelName, String frameNo, String dailyRate, BikeStatus status) { this.modelName = modelName; this.frameNo = frameNo; this.dailyRate = new BigDecimal(dailyRate); this.status = status; this.boughtOn = LocalDate.of(2026, 1, 15); } public Long getId() { return id; } public String getShopNote() { return shopNote; } public void setShopNote(String shopNote) { this.shopNote = shopNote; } @Override public String toString() { return id + " " + modelName + " " + status + " Rs" + dailyRate; } }
File: BikeRepository.java in package com.pedalpoint.rentals
javapackage com.pedalpoint.rentals; import org.springframework.data.jpa.repository.JpaRepository; public interface BikeRepository extends JpaRepository<Bike, Long> { }
File: BikeRunner.java in package com.pedalpoint.rentals
javapackage com.pedalpoint.rentals; import org.springframework.boot.CommandLineRunner; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; @Component public class BikeRunner implements CommandLineRunner { private final BikeRepository repository; public BikeRunner(BikeRepository repository) { this.repository = repository; } @Override public void run(String... args) { Bike trail = new Bike("Trail Rider", "FR-1001", "450.00", BikeStatus.AVAILABLE); trail.setShopNote("fresh paint"); repository.save(trail); repository.save(new Bike("City Glide", "FR-1002", "300.00", BikeStatus.IN_REPAIR)); repository.findAll().forEach(System.out::println); try { repository.save(new Bike("Copy Cat", "FR-1001", "100.00", BikeStatus.RENTED)); } catch (DataIntegrityViolationException e) { System.out.println("Duplicate frame no rejected"); } Bike reloaded = repository.findById(trail.getId()).orElseThrow(); System.out.println("Note after reload: " + reloaded.getShopNote()); } }
File: application.properties in src/main/resources
propertiesspring.application.name=rentals spring.jpa.show-sql=true
Run it with mvn spring-boot:run. Hibernate also writes a warning about the duplicate row to the console. It is expected here, and the output below leaves it out.
Output:
text1 Trail Rider AVAILABLE Rs450.00 2 City Glide IN_REPAIR Rs300.00 Duplicate frame no rejected Note after reload: null
show-sql also prints the table that Hibernate built from the annotations. Compare it with the class above. H2 shows the enum column as a list of the three allowed names.
sqlcreate table bikes (bought_on date, daily_rate numeric(8,2) not null, id bigint generated by default as identity, frame_no varchar(20) not null unique, model_name varchar(60) not null, status enum ('AVAILABLE','IN_REPAIR','RENTED') not null, primary key (id))
Code Explained
@Table(name = "bikes")sets the table name. Without it, the table would be calledbike.@Column(nullable = false, length = 60)makes the columnnot nullwith at most 60 characters.name = "model_name"sets the column name.unique = trueonframe_nocreates a unique constraint. The duplicateFR-1001was rejected by the database, and Spring turned the error into a data-integrity exception, which the runner catches.precision = 8, scale = 2is for money. Eight digits in total, two after the point, which suitsBigDecimal.@Enumerated(EnumType.STRING)stores the enum name, such asAVAILABLE, instead of its position number. Names stay correct even if you reorder the enum.@Transientsays "do not store this field". The note printed asnullafter reload, because it never reached the database.boughtOnhas no annotation, so Hibernate uses the default column namebought_onand a date type.
Common Column Annotations
| Annotation | Purpose |
|---|---|
@Entity | Maps the class to a table |
@Table(name = "...") | Chooses the table name |
@Id | Marks the primary key |
@Column(...) | Sets name, length, null and unique rules |
@Enumerated | Stores an enum as text or number |
@Transient | Leaves a field out of the table |
Common Mistakes
- Making the entity a `record` or a `final` class. Hibernate must create and fill entity objects, so it needs a normal class it can extend.
- Forgetting the no-argument constructor. Reading rows then fails at runtime.
- Using `double` for money. Decimals like 0.10 are not exact in
double. UseBigDecimal. - Trusting `@Column(length)` as the only check. It protects the database, but validate user input earlier too, so people get friendly error messages.
Interview Questions
What is the minimum needed to make a class an entity?
Ans:The @Entity annotation, an @Id field and a no-argument constructor. The class must not be final.
What does `@Transient` do?
Ans:It tells JPA not to store the field. The field lives only in memory.
Why prefer `EnumType.STRING` over `ORDINAL`?
Ans:The string stays correct if the enum order changes. Ordinal stores positions, which break when values move.
Key Points to Remember
- An entity is a class mapped to a table, and its objects are rows.
@Entityand@Idare required, and a no-argument constructor is needed for Hibernate.@Columnstates rules such as not null, length and unique, and the database enforces them.- Use
BigDecimalfor money andEnumType.STRINGfor enums. @Transientkeeps a field out of the table.
Frequently Asked Questions
Do I have to write getters and setters in a JPA entity?
Hibernate can read private fields directly, so it does not need them. You add getters when other code needs the values, for example when Jackson turns the object into JSON.
Can two entities share the same table name?
Not in one database. Each entity needs its own table, so pick different names with @Table if the class names clash.
What is the difference between @Entity and @Table?
@Entity says the class is stored in the database. @Table is optional and only sets details such as the table name.
Can I use Lombok on entities?
Yes, but be careful with @Data, which creates equals and hashCode over all fields and can cause trouble with relations. Plain getters and setters are safer.
Related Topics
- Primary Key Generation Strategies: choose how the
@Idvalue is created. - JpaRepository: save and load these entities without SQL.
- Bean Validation: check input before it reaches the database.
- One-to-One Mapping: connect two entities together.
Practice Problems
Try each problem on your own first. Each project has its own pom.xml, shown below.
Easy: Bakery Pastry Entity with Rules
Golden Crust Bakery wants a Pastry entity stored in the table pastries. It has a name (required, at most 40 characters), a category that is an enum (CAKE, BREAD or SNACK) stored as text, and a price in rupees with two decimals. Save one valid pastry and print it. Then try to save a pastry with no name, and print Name is required when the database refuses it.
Show answerHide answer
not null rule lives in the entity annotation, and the database enforces it. Spring converts the low-level error into a data-integrity exception.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.goldencrust</groupId> <artifactId>bakery</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: BakeryApplication.java in package com.goldencrust.bakery
javapackage com.goldencrust.bakery; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BakeryApplication { public static void main(String[] args) { SpringApplication.run(BakeryApplication.class, args); } }
File: Category.java in package com.goldencrust.bakery
javapackage com.goldencrust.bakery; public enum Category { CAKE, BREAD, SNACK }
File: Pastry.java in package com.goldencrust.bakery
javapackage com.goldencrust.bakery; import java.math.BigDecimal; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "pastries") public class Pastry { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 40) private String name; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 10) private Category category; @Column(precision = 8, scale = 2) private BigDecimal price; protected Pastry() { } public Pastry(String name, Category category, String price) { this.name = name; this.category = category; this.price = new BigDecimal(price); } @Override public String toString() { return id + " " + name + " " + category + " Rs" + price; } }
File: PastryRepository.java in package com.goldencrust.bakery
javapackage com.goldencrust.bakery; import org.springframework.data.jpa.repository.JpaRepository; public interface PastryRepository extends JpaRepository<Pastry, Long> { }
File: PastryRunner.java in package com.goldencrust.bakery
javapackage com.goldencrust.bakery; import org.springframework.boot.CommandLineRunner; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; @Component public class PastryRunner implements CommandLineRunner { private final PastryRepository repository; public PastryRunner(PastryRepository repository) { this.repository = repository; } @Override public void run(String... args) { System.out.println(repository.save(new Pastry("Fruit Cake", Category.CAKE, "550.00"))); try { repository.save(new Pastry(null, Category.BREAD, "40.00")); } catch (DataIntegrityViolationException e) { System.out.println("Name is required"); } } }
The output is:
text1 Fruit Cake CAKE Rs550.00 Name is required
Medium: Gym Member with a Locked Join Date
IronWorks Gym stores members in the table gym_members. A member has a full name, a unique email, a plan (MONTHLY or YEARLY, stored as text) and a join date. The join date must never change after the first save. Save a member, then change both the name and the join date and save again. Reload the member and print it. The name must change, but the join date must stay the same.
Show answerHide answer
@Column(updatable = false) protects a column after the first insert. The setter still changes the Java field, but the change is not written to the database, and the reload shows the stored value.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.ironworks</groupId> <artifactId>gym</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: GymApplication.java in package com.ironworks.gym
javapackage com.ironworks.gym; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GymApplication { public static void main(String[] args) { SpringApplication.run(GymApplication.class, args); } }
File: Plan.java in package com.ironworks.gym
javapackage com.ironworks.gym; public enum Plan { MONTHLY, YEARLY }
File: GymMember.java in package com.ironworks.gym
javapackage com.ironworks.gym; import java.time.LocalDate; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "gym_members") public class GymMember { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String fullName; @Column(nullable = false, unique = true) private String email; @Enumerated(EnumType.STRING) private Plan plan; @Column(nullable = false, updatable = false) private LocalDate joinedOn; protected GymMember() { } public GymMember(String fullName, String email, Plan plan, LocalDate joinedOn) { this.fullName = fullName; this.email = email; this.plan = plan; this.joinedOn = joinedOn; } public Long getId() { return id; } public void setFullName(String fullName) { this.fullName = fullName; } public void setJoinedOn(LocalDate joinedOn) { this.joinedOn = joinedOn; } @Override public String toString() { return fullName + " " + plan + " " + joinedOn; } }
File: GymMemberRepository.java in package com.ironworks.gym
javapackage com.ironworks.gym; import org.springframework.data.jpa.repository.JpaRepository; public interface GymMemberRepository extends JpaRepository<GymMember, Long> { }
File: MemberRunner.java in package com.ironworks.gym
javapackage com.ironworks.gym; import java.time.LocalDate; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class MemberRunner implements CommandLineRunner { private final GymMemberRepository repository; public MemberRunner(GymMemberRepository repository) { this.repository = repository; } @Override public void run(String... args) { GymMember member = repository.save(new GymMember( "Kabir Nair", "kabir@example.com", Plan.MONTHLY, LocalDate.of(2026, 2, 1))); member.setFullName("Kabir K. Nair"); member.setJoinedOn(LocalDate.of(2020, 1, 1)); repository.save(member); GymMember reloaded = repository.findById(member.getId()).orElseThrow(); System.out.println(reloaded); } }
The output is:
textKabir K. Nair MONTHLY 2026-02-01