This tutorial continues from Your first Vidocq application and gives the app a real database: a Product entity, a Jakarta Data repository generated at compile time by Mansart, a virtual-thread-native connection pool, and @Transactional methods — first on in-memory H2, then a look at named datasources and PostgreSQL Dev Services.

What you will build

  • GET /products and POST /products backed by an H2 database;

  • a ProductRepository interface — its implementation is generated at compile time, no runtime proxies;

  • transactions handled by Mansart’s Jakarta Transactions manager through Vauban interceptors.

The reference for everything here is the runnable example shipped in the runtime repo: vidocq-runtime-examples/vidocq-runtime-mansart-h2-example (Vidocq/vidocq).

Prerequisites

The todo project from the first tutorial (or any scaffolded cassini-rest project).

Step 1 — Add the Mansart extensions

Add the three Mansart extensions, the H2 dialect and the H2 driver to your pom.xml dependencies:

<dependency>
    <groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
    <artifactId>vidocq-runtime-mansart-pool-extension</artifactId>
    <version>0.3.0</version>
</dependency>
<dependency>
    <groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
    <artifactId>vidocq-runtime-mansart-data-extension</artifactId>
    <version>0.3.0</version>
</dependency>
<dependency>
    <groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
    <artifactId>vidocq-runtime-mansart-transactions-extension</artifactId>
    <version>0.3.0</version>
</dependency>
<dependency>
    <groupId>io.vidocq.mansart</groupId>
    <artifactId>mansart-data-dialect-h2</artifactId>
    <version>0.3.0</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.3.232</version>
</dependency>

Then let the Mansart code generators run during compilation — append their bundles to the annotationProcessorPaths of the compiler plugin (the combine.children="append" attribute is already on the scaffolded block; keep it, it preserves the inherited Vauban indexer):

<annotationProcessorPaths combine.children="append">
    <!-- (scaffolded) cassini-rest codegen path is already here -->
    <path>
        <groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
        <artifactId>vidocq-runtime-mansart-data-extension-codegen</artifactId>
        <version>0.3.0</version>
        <type>pom</type>
    </path>
    <path>
        <groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
        <artifactId>vidocq-runtime-mansart-transactions-extension-codegen</artifactId>
        <version>0.3.0</version>
        <type>pom</type>
    </path>
</annotationProcessorPaths>

Finally declare the new modules in module-info.java:

    requires java.sql;
    requires jakarta.persistence;
    requires jakarta.data;
    requires jakarta.transaction;

    requires io.vidocq.runtime.extensions.jakartaee.web.mansart.pool;
    requires io.vidocq.runtime.extensions.jakartaee.web.mansart.data;
    requires io.vidocq.runtime.extensions.jakartaee.web.mansart.transactions;
    requires io.vidocq.mansart.data.core;

Step 2 — Configure the pool

In src/main/resources/vidocq.properties:

# Mansart pool -- @Default datasource, H2 in-memory.
# DB_CLOSE_DELAY=-1 keeps the in-mem DB alive for the whole JVM lifetime.
vidocq.pool.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1
vidocq.pool.username=sa
vidocq.pool.maxSize=8
vidocq.pool.acquireTimeout=PT5S

The pool is Mansart’s own, built for virtual threads (no synchronized around I/O). All keys live under vidocq.pool.* — see Mansart usage for sizing, validation and leak detection.

Step 3 — The entity

Create src/main/java/com/acme/todo/Product.java:

package com.acme.todo;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private double price;

    public Product() {}

    public Product(String name, double price) {
        this.name  = name;
        this.price = price;
    }

    public Long   getId()            { return id;      }
    public void   setId(Long id)     { this.id = id;   }
    public String getName()          { return name;    }
    public void   setName(String n)  { this.name = n;  }
    public double getPrice()         { return price;   }
    public void   setPrice(double p) { this.price = p; }
}

Mansart implements Jakarta Data 1.0 and reads the standard Jakarta Persistence annotations for its entity model — at compile time, through APT.

Step 4 — The repository

Create src/main/java/com/acme/todo/ProductRepository.java:

package com.acme.todo;

import jakarta.data.repository.BasicRepository;
import jakarta.data.repository.Repository;
import jakarta.transaction.Transactional;

import java.util.List;

@Transactional
@Repository
public interface ProductRepository extends BasicRepository<Product, Long> {

    long count();

    List<Product> findByNameLike(String pattern);
}

You write the interface; the Mansart annotation processor generates ProductRepositoryImpl during compilation and wires it into Vauban as a bean. findByNameLike is a derived query — the SQL comes from the method name. @Transactional on the interface makes every method run in a transaction, through the Vauban-generated interceptor chain.

Step 5 — Create the schema

For this tutorial, plain DDL at startup is enough. Create src/main/java/com/acme/todo/SchemaInitializer.java:

package com.acme.todo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

@ApplicationScoped
public class SchemaInitializer {

    @Inject
    DataSource dataSource;

    void onStart(@Observes @Initialized(ApplicationScoped.class) Object event) {
        try (Connection c = dataSource.getConnection(); Statement s = c.createStatement()) {
            s.execute("""
                CREATE TABLE IF NOT EXISTS products (
                    id    BIGINT AUTO_INCREMENT PRIMARY KEY,
                    name  VARCHAR(255) NOT NULL,
                    price DOUBLE PRECISION NOT NULL
                )""");
        } catch (SQLException e) {
            throw new IllegalStateException("Schema initialization failed", e);
        }
    }
}

@Inject DataSource hands you the pooled @Default datasource configured in Step 2.

For real applications, prefer versioned migrations: the runtime ships a Flyway extension (vidocq-runtime-flyway-migration-extension) that migrates the @Default datasource from db/migration/*.sql at boot — that is what the reference example uses.

Step 6 — The resource

Create src/main/java/com/acme/todo/ProductResource.java:

package com.acme.todo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import java.util.List;

@Path("/products")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {

    @Inject
    ProductRepository products;

    @GET
    public List<Product> list() {
        return products.findAll().toList();
    }

    @POST
    public Product add(Product product) {
        return products.save(product);
    }
}

Step 7 — Run it

mvn package
sh target/todo-0.3.0/bin/todo.sh &

curl -X POST http://localhost:8080/products \
     -H 'Content-Type: application/json' \
     -d '{"name":"Optical telegraph","price":1794.0}'

curl http://localhost:8080/products
[{"id":1,"name":"Optical telegraph","price":1794.0}]

Insert, query, transaction commit — all through code generated at build time. Check the boot log: the pool, the transaction manager and the repositories are reported as they come up, still well under a second.

Step 8 — Named datasources

Version note. On the released 0.3.0 artifacts this tutorial uses, @Repository(dataStore = …​) does not route — the repository silently targets the default datasource (known bug MANSART-005, fixed on the dev line). @Inject @Named datasource injection works fine. For real routing, switch this page to the dev version with the selector in the header and follow its instructions.

One database is rarely the whole story. Mansart routes repositories to named datasources: declare a second pool and point a repository at it.

vidocq.pool.audit.url=jdbc:h2:mem:todo-audit;DB_CLOSE_DELAY=-1
vidocq.pool.audit.username=sa
vidocq.pool.audit.maxSize=4
@Repository(dataStore = "audit")
public interface AuditEntryRepository extends BasicRepository<AuditEntry, Long> { }

The datasource itself can be injected anywhere with @Inject @Named("audit") DataSource. Named datasources need one extra codegen path (vidocq-runtime-mansart-pool-datasources-codegen, plain jar) in annotationProcessorPaths — see Mansart usage for the full walkthrough.

Step 9 — PostgreSQL and Dev Services

Swapping H2 for PostgreSQL is a dependency change (mansart-data-dialect-postgresql + the PostgreSQL driver) and a URL change. In dev mode, you do not even need a database: the runtime ships a PostgreSQL Dev Service that starts a disposable container (Testcontainers, default image postgres:16-alpine) and injects the connection coordinates into the application — nothing to configure.

# Optional tuning -- everything has sensible defaults:
vidocq.dev.postgres.image=postgres:16-alpine
vidocq.dev.reuse=true              # reuse the container across restarts
vidocq.dev.devServices=false       # global opt-out

Dev Services run from the Maven plugin (mvn vidocq:dev), never from your packaged application: Testcontainers is not on your module path and nothing container-related ships to production. An explicitly configured URL (-D or environment) always wins over the Dev Service. The full behaviour — multi-datasource containers, connection-info resolution, Docker daemon detection — is documented in DEV_SERVICES.md at the root of the runtime repo.

Where to next

  • Mansart usage — derived queries, JDQL, pagination, pool sizing, named datasources.

  • Mansart concepts — how the repository implementations are generated.

  • Runtime usage — packaging (jlink, Docker), configuration profiles.

  • The complete runnable example: vidocq-runtime-examples/vidocq-runtime-mansart-h2-example in Vidocq/vidocq — adds Flyway migrations, XA transactions, OpenAPI and a small UI on top of what you just built.