Panache entities: active record
An entity is a Java class whose objects are stored in a database table: the class is the table, a field is a column, an object is a row. Entities are managed by Hibernate ORM. Panache is a Quarkus layer over Hibernate that removes boilerplate code. Its first style is called active record: the methods for working with the database belong to the entity class itself — Post.listAll(), post.persist().
The post entity
We replace the Post record from the previous chapter with an entity class. Delete Post.java and PostService.java and create a new Post.java:
// src/main/java/com/example/blog/Post.java
package com.example.blog;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import java.time.LocalDateTime;
@Entity
public class Post extends PanacheEntity {
public String title;
@Column(length = 5000)
public String body;
public LocalDateTime createdAt = LocalDateTime.now();
}@Entity makes the class an entity; the table is named Post. From PanacheEntity the class inherits an id field with automatic numbering and the database methods. The fields are public — that is the Panache convention: Quarkus generates getters and setters itself at build time. @Column(length = 5000) allows long text; by default a string is limited to 255 characters.
Active record methods
| Call | What it does |
|---|---|
Post.listAll() | All rows of the table |
Post.findById(id) | One row, or null |
Post.findByIdOptional(id) | The same, wrapped in Optional |
Post.count() | The number of rows |
post.persist() | Save a new object |
post.delete() | Delete an object |
Post.deleteById(id) | Delete by id, return true if the row existed |
A resource backed by the database
// src/main/java/com/example/blog/PostResource.java
@Path("/posts")
public class PostResource {
@GET
public List<Post> list() {
return Post.listAll();
}
@GET
@Path("/{id}")
public Post get(@PathParam("id") long id) {
return Post.<Post>findByIdOptional(id)
.orElseThrow(() -> new PostNotFoundException(id));
}
@POST
@Transactional
public Response create(@Valid NewPost newPost) {
Post post = new Post();
post.title = newPost.title();
post.body = newPost.body();
post.persist();
return Response.created(URI.create("/posts/" + post.id)).entity(post).build();
}
@DELETE
@Path("/{id}")
@Transactional
public Response delete(@PathParam("id") long id) {
if (!Post.deleteById(id)) {
throw new PostNotFoundException(id);
}
return Response.noContent().build();
}
}The methods that change data are marked with @Transactional (import jakarta.transaction.Transactional); transactions get their own lesson. Post.<Post>findByIdOptional tells the compiler the result type. The NewPost record with its checks stays the same.
Initial data
In dev mode the tables are recreated on every start. To keep the database from being empty, we add a bean that listens to the application startup event:
// src/main/java/com/example/blog/Startup.java
package com.example.blog;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.transaction.Transactional;
@ApplicationScoped
public class Startup {
@Transactional
void onStart(@Observes StartupEvent event) {
if (Post.count() > 0) {
return;
}
Post first = new Post();
first.title = "Hello, Quarkus";
first.body = "The first post of the blog.";
first.persist();
Post second = new Post();
second.title = "Dev mode";
second.body = "Change the code and refresh the page.";
second.persist();
}
}@Observes StartupEvent means the method is called once at startup. The count() > 0 check prevents duplicate posts in a database that keeps data between runs.
curl http://localhost:8080/posts[{"id":1,"title":"Hello, Quarkus","body":"The first post of the blog.","createdAt":"2026-10-04T11:55:10.403195"},
{"id":2,"title":"Dev mode","body":"Change the code and refresh the page.","createdAt":"2026-10-04T11:55:10.430728"}]
Creating with POST /posts and deleting with DELETE /posts/1 work as before, only now the data lives in a table. In the Dev UI the Hibernate ORM card shows the Post entity and its table.
Common mistake
If you remove @Transactional from the create method, saving fails with a 500 response, and the log says:
jakarta.persistence.TransactionRequiredException: Transaction is not active, consider adding @Transactional to your method to automatically activate one.
Reading works without a transaction, but any change — persist, delete, editing fields — needs one. The message itself suggests the fix.