Skip to content
CampusEduX

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.

9 min read

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 @Entity annotation 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 not final, 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.

text
Java 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.

text
rentals/ ├─ 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

java
package 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

java
package com.pedalpoint.rentals; public enum BikeStatus { AVAILABLE, RENTED, IN_REPAIR }

File: Bike.java in package com.pedalpoint.rentals

java
package 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

java
package 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

java
package 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

properties
spring.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:

text
1 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.

sql
create 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 called bike.
  • @Column(nullable = false, length = 60) makes the column not null with at most 60 characters. name = "model_name" sets the column name.
  • unique = true on frame_no creates a unique constraint. The duplicate FR-1001 was rejected by the database, and Spring turned the error into a data-integrity exception, which the runner catches.
  • precision = 8, scale = 2 is for money. Eight digits in total, two after the point, which suits BigDecimal.
  • @Enumerated(EnumType.STRING) stores the enum name, such as AVAILABLE, instead of its position number. Names stay correct even if you reorder the enum.
  • @Transient says "do not store this field". The note printed as null after reload, because it never reached the database.
  • boughtOn has no annotation, so Hibernate uses the default column name bought_on and a date type.

Common Column Annotations

AnnotationPurpose
@EntityMaps the class to a table
@Table(name = "...")Chooses the table name
@IdMarks the primary key
@Column(...)Sets name, length, null and unique rules
@EnumeratedStores an enum as text or number
@TransientLeaves 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. Use BigDecimal.
  • 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.
  • @Entity and @Id are required, and a no-argument constructor is needed for Hibernate.
  • @Column states rules such as not null, length and unique, and the database enforces them.
  • Use BigDecimal for money and EnumType.STRING for enums.
  • @Transient keeps 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.

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 answer
The 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

java
package 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

java
package com.goldencrust.bakery; public enum Category { CAKE, BREAD, SNACK }

File: Pastry.java in package com.goldencrust.bakery

java
package 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

java
package 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

java
package 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:

text
1 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 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

java
package 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

java
package com.ironworks.gym; public enum Plan { MONTHLY, YEARLY }

File: GymMember.java in package com.ironworks.gym

java
package 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

java
package 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

java
package 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:

text
Kabir K. Nair MONTHLY 2026-02-01

Mock Test

  • JPA Entity and @Entity - Quick Test

    5 questions to check what you learned in JPA Entity and @Entity.

    5 questions · 5 min · Medium
    Start Mock Test