Menu

Quarkus Topic

Part 1: Configuring Datasources

Quarkus uses a unified configuration approach for datasources. You configure everything in application.properties.

Basic PostgreSQL Configuration

properties

# Datasource configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_user
quarkus.datasource.password=quarkus_pass
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_db

# Hibernate configuration
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true

MySQL Configuration

properties

quarkus.datasource.db-kind=mysql
quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/quarkus_db
quarkus.datasource.username=root
quarkus.datasource.password=secret

Multiple Datasources

properties

# Default datasource
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/main_db

# Secondary datasource
quarkus.datasource.users.db-kind=mysql
quarkus.datasource.users.jdbc.url=jdbc:mysql://localhost:3306/users_db

# Hibernate for secondary datasource
quarkus.hibernate-orm.users.database.generation=update

Part 2: Hibernate ORM with Panache

Panache comes in two flavors: Repository Pattern and Active Record Pattern. Both significantly reduce boilerplate.

Active Record Pattern

The entity extends PanacheEntity and contains its own persistence logic.

java

@Entity
@Table(name = "person")
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public String status;
    
    // Custom business methods
    public static Person findByName(String name) {
        return find("name", name).firstResult();
    }
    
    public static List<Person> findAlive() {
        return list("status", "alive");
    }
    
    public static long deleteSteves() {
        return delete("name", "Steve");
    }
}

Repository Pattern

More familiar if you come from Spring Data. Separates persistence logic from the entity.

java

// Entity (pure data model)
@Entity
@Table(name = "person")
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    
    public String name;
    public LocalDate birth;
    public String status;
    
    // Getters and setters...
}

// Repository
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
    public Person findByName(String name) {
        return find("name", name).firstResult();
    }
    
    public List<Person> findAlive() {
        return list("status", "alive");
    }
    
    public List<Person> findBornAfter(LocalDate date) {
        return find("birth > ?1", date).list();
    }
}

Part 3: Entity Relationships, Queries, and Repositories

Entity Relationships

java

@Entity
public class Person extends PanacheEntity {
    public String name;
    
    // One-to-One
    @OneToOne(cascade = CascadeType.ALL)
    public Address address;
    
    // One-to-Many
    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
    public List<Phone> phones = new ArrayList<>();
    
    // Many-to-Many
    @ManyToMany
    @JoinTable(name = "person_team")
    public List<Team> teams = new ArrayList<>();
}

@Entity
public class Phone extends PanacheEntity {
    public String number;
    
    @ManyToOne
    public Person person;
}

Query Methods

Panache provides several ways to query:

1. Simple Queries

java

// Basic operations
Person person = Person.findById(1L);
List<Person> people = Person.listAll();

// Simple where clauses
Person steve = Person.find("name", "Steve").firstResult();
List<Person> alivePeople = Person.list("status", "alive");

// Complex queries
List<Person> adults = Person.find("birth < ?1", 
    LocalDate.now().minusYears(18)).list();

// Named queries
@NamedQuery(name = "Person.getAlive", 
    query = "FROM Person WHERE status = 'alive'")
public class Person extends PanacheEntity { ... }

List<Person> alive = Person.find("#Person.getAlive").list();

2. Advanced Queries with PanacheQuery

java

PanacheQuery<Person> query = Person.find("status", "alive");

// Pagination
List<Person> firstPage = query.page(0, 20).list();
List<Person> secondPage = query.nextPage().list();

// Streaming (for large datasets)
query.stream().forEach(person -> {
    processPerson(person);
});

// Single results
Person person = query.firstResult();

3. Repository Pattern Usage

java

@Inject
PersonRepository personRepository;

public void businessMethod() {
    Person person = personRepository.findById(1L);
    List<Person> people = personRepository.listAll();
    List<Person> alive = personRepository.findAlive();
    
    // Count
    long total = personRepository.count();
    long aliveCount = personRepository.count("status", "alive");
    
    // Update
    personRepository.update("status = 'inactive' where birth < ?1", 
        LocalDate.now().minusYears(65));
}

Part 4: Reactive SQL Clients

For truly reactive applications, Quarkus offers reactive SQL clients that work without Hibernate.

Reactive PostgreSQL Client

Dependencies

xml

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>

Configuration

properties

quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_db
quarkus.datasource.username=quarkus_user
quarkus.datasource.password=quarkus_pass

Usage Example

java

@Path("/reactive-users")
@ApplicationScoped
public class ReactiveUserResource {

    @Inject
    io.vertx.mutiny.pgclient.PgPool client;

    // Create table (in production, use Flyway)
    @PostConstruct
    void setup() {
        client.query("DROP TABLE IF EXISTS reactive_users").execute()
            .flatMap(r -> client.query("""
                CREATE TABLE reactive_users (
                    id SERIAL PRIMARY KEY,
                    name VARCHAR(100),
                    email VARCHAR(100)
                )
            """).execute())
            .subscribe().with(
                success -> System.out.println("Table created"),
                failure -> System.out.println("Error: " + failure.getMessage())
            );
    }

    // Create user
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Uni<Long> createUser(User user) {
        return client.preparedQuery("""
                INSERT INTO reactive_users (name, email) 
                VALUES ($1, $2) RETURNING id
            """)
            .execute(Tuple.of(user.name, user.email))
            .map(pgRowSet -> {
                // Extract the generated ID
                return pgRowSet.iterator().next().getLong("id");
            });
    }

    // Get all users
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<List<User>> getAllUsers() {
        return client.query("SELECT id, name, email FROM reactive_users")
            .execute()
            .map(pgRowSet -> {
                List<User> list = new ArrayList<>();
                for (Row row : pgRowSet) {
                    list.add(new User(
                        row.getLong("id"),
                        row.getString("name"),
                        row.getString("email")
                    ));
                }
                return list;
            });
    }

    // Get user by ID
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<User> getUserById(@PathParam("id") Long id) {
        return client.preparedQuery("SELECT id, name, email FROM reactive_users WHERE id = $1")
            .execute(Tuple.of(id))
            .map(pgRowSet -> {
                if (pgRowSet.size() == 0) {
                    return null;
                }
                Row row = pgRowSet.iterator().next();
                return new User(
                    row.getLong("id"),
                    row.getString("name"),
                    row.getString("email")
                );
            });
    }
}

// Simple User DTO
class User {
    public Long id;
    public String name;
    public String email;
    
    public User() {}
    
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

Transaction Management

Imperative Transactions

java

@ApplicationScoped
public class PersonService {
    
    @Inject
    EntityManager em;
    
    @Transactional
    public void createPerson(Person person) {
        // This runs in a transaction
        person.persist();
    }
    
    @Transactional
    public void transferMoney(Long from, Long to, BigDecimal amount) {
        Person fromPerson = Person.findById(from);
        Person toPerson = Person.findById(to);
        
        fromPerson.balance = fromPerson.balance.subtract(amount);
        toPerson.balance = toPerson.balance.add(amount);
        
        // If any exception occurs, transaction rolls back automatically
    }
}

Reactive Transactions

java

@Inject
Mutiny.Session session;

public Uni<Void> createUserReactive(User user) {
    return session.withTransaction(tx -> {
        return session.persist(user);
    });
}

Database Migration with Flyway

properties

# Flyway configuration
quarkus.flyway.migrate-at-start=true
quarkus.flyway.locations=classpath:db/migration

sql

-- src/main/resources/db/migration/V1.0.0__Create_person_table.sql
CREATE TABLE person (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    birth DATE,
    status VARCHAR(50)
);

Key Takeaways

  1. Panache Choice: Use Active Record for simple CRUD, Repository for complex business logic

  2. Query Simplicity: Panache eliminates 90% of boilerplate repository code

  3. Reactive When Needed: Use reactive clients for high-concurrency, non-blocking scenarios

  4. Build-Time Optimization: Quarkus optimizes Hibernate at build time for faster startup

  5. Unified Configuration: All datasource configuration in one place

The combination of Quarkus + Panache makes database interactions in Java feel modern and productive, while still giving you access to full Hibernate power when you need it.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?