Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

CallWhat 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.

Thanks for reading!

Donate to the site — we work hard for you.