はじめに
Hibernate ORM Panache は、JPA エンティティとクエリの作成を簡素化する Hibernate ORM 上のレイヤーです。 Panache は、アクティブ レコード (エンティティの自己クエリ) と リポジトリ (エンティティ/リポジトリの分離) の 2 つのパターンをサポートしています。 Dev Services と組み合わせると、PostgreSQL をインストールする必要がなく、Quarkus が自動的にコンテナを起動します。
PostgreSQL を構成する
依存関係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
開発サービス — 構成ゼロ
依存関係を追加するだけで、開発モードでは それ以上の構成は必要ありません。
# application.properties
# Dev Services tự động:
# - Pull postgres:latest container
# - Tạo database
# - Cấu hình JDBC URL
# - Inject vào Hibernate ORM
# Chỉ cần cấu hình cho production
%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=${DB_USERNAME}
%prod.quarkus.datasource.password=${DB_PASSWORD}
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
# Schema management
quarkus.hibernate-orm.database.generation=drop-and-create
# Production dùng Flyway (bài sau)
%prod.quarkus.hibernate-orm.database.generation=none
Dev Services が機能することを確認する
# Start dev mode
quarkus dev
# Kiểm tra container đang chạy
docker ps
# CONTAINER ID IMAGE PORTS NAMES
# abc123 postgres:16 0.0.0.0:55432->5432/tcp ...
# Dev UI → http://localhost:8080/q/dev-ui → Database
アクティブなレコード パターン
エンティティクラス
package com.xdev.ecommerce.product.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "products")
public class Product extends PanacheEntity {
// id được tự động generate bởi PanacheEntity (Long)
@Column(nullable = false, length = 255)
public String name;
@Column(columnDefinition = "TEXT")
public String description;
@Column(nullable = false, precision = 12, scale = 2)
public BigDecimal price;
@Column(name = "stock_quantity", nullable = false)
public int stockQuantity;
@Column(length = 100)
public String category;
@Column(length = 50)
public String status = "ACTIVE";
@Column(name = "created_at", updatable = false)
public LocalDateTime createdAt;
@Column(name = "updated_at")
public LocalDateTime updatedAt;
@PrePersist
void onPersist() {
createdAt = LocalDateTime.now();
updatedAt = createdAt;
}
@PreUpdate
void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
アクティブ レコードを使用する
// CREATE
Product product = new Product();
product.name = "Laptop Dell XPS 15";
product.price = new BigDecimal("32990000");
product.stockQuantity = 50;
product.category = "Electronics";
product.persist();
// READ by ID
Product found = Product.findById(1L);
// READ all
List<Product> all = Product.listAll();
// QUERY
List<Product> electronics = Product.list(
"category = ?1 and status = ?2", "Electronics", "ACTIVE");
// QUERY with named parameters
List<Product> cheap = Product.list(
"price < :maxPrice and category = :cat",
Parameters.with("maxPrice", new BigDecimal("10000000"))
.and("cat", "Electronics"));
// UPDATE
Product.update("price = price * 0.9 where category = ?1",
"Electronics");
// DELETE
Product.deleteById(1L);
Product.delete("status", "INACTIVE");
// COUNT
long total = Product.count();
long active = Product.count("status", "ACTIVE");
リポジトリ パターン
エンティティ (PanacheEntity を拡張しない)
@Entity
@Table(name = "categories")
public class Category extends PanacheEntityBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
@Column(nullable = false, unique = true)
public String name;
public String description;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
public List<Product> products;
}
リポジトリクラス
package com.xdev.ecommerce.product.repository;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class CategoryRepository implements PanacheRepository<Category> {
public Optional<Category> findByName(String name) {
return find("name", name).firstResultOptional();
}
public List<Category> findActive() {
return list("status", "ACTIVE");
}
public long countProducts(Long categoryId) {
return Product.count("category.id", categoryId);
}
}
サービスでリポジトリを使用する
@ApplicationScoped
public class CategoryService {
@Inject
CategoryRepository categoryRepository;
public List<Category> getAll() {
return categoryRepository.listAll();
}
@Transactional
public Category create(String name, String description) {
Category cat = new Category();
cat.name = name;
cat.description = description;
categoryRepository.persist(cat);
return cat;
}
}
PanacheQuery — ページネーションと並べ替え
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
@GET
public Response listProducts(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("sort") @DefaultValue("createdAt") String sortBy,
@QueryParam("dir") @DefaultValue("desc") String direction) {
Sort sort = Sort.by(sortBy,
direction.equalsIgnoreCase("asc")
? Sort.Direction.Ascending
: Sort.Direction.Descending);
PanacheQuery<Product> query = Product
.find("status = ?1", sort, "ACTIVE")
.page(Page.of(page, size));
List<Product> items = query.list();
long total = query.count();
int totalPages = query.pageCount();
return Response.ok(items)
.header("X-Total-Count", total)
.header("X-Total-Pages", totalPages)
.build();
}
カスタム クエリ — JPQL およびネイティブ
JPQL クエリ
@Entity
@Table(name = "products")
@NamedQuery(name = "Product.search",
query = """
SELECT p FROM Product p
WHERE (LOWER(p.name) LIKE LOWER(:keyword)
OR LOWER(p.description) LIKE LOWER(:keyword))
AND p.status = 'ACTIVE'
ORDER BY p.createdAt DESC
""")
public class Product extends PanacheEntity {
// ...
}
// Sử dụng
List<Product> results = Product.find("#Product.search",
Parameters.with("keyword", "%" + keyword + "%")).list();
ネイティブ SQL クエリ
@ApplicationScoped
public class ProductRepository implements PanacheRepository<Product> {
public List<Product> searchFullText(String query) {
return find(
"""
to_tsvector('english', name || ' ' || description)
@@ plainto_tsquery('english', ?1)
""", query).list();
}
public List<Object[]> getTopCategories(int limit) {
return getEntityManager()
.createNativeQuery("""
SELECT category, COUNT(*) as cnt, AVG(price) as avg_price
FROM products
WHERE status = 'ACTIVE'
GROUP BY category
ORDER BY cnt DESC
LIMIT :limit
""")
.setParameter("limit", limit)
.getResultList();
}
}
トランザクション
import jakarta.transaction.Transactional;
@ApplicationScoped
public class OrderService {
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 1. Kiểm tra stock
Product product = Product.findById(request.productId());
if (product == null) {
throw new NotFoundException("Product not found");
}
if (product.stockQuantity < request.quantity()) {
throw new BadRequestException("Insufficient stock");
}
// 2. Giảm stock
product.stockQuantity -= request.quantity();
// Không cần persist() — entity đã managed
// 3. Tạo order
Order order = new Order();
order.productId = product.id;
order.quantity = request.quantity();
order.totalPrice = product.price
.multiply(BigDecimal.valueOf(request.quantity()));
order.status = "PENDING";
order.persist();
return order;
// Transaction auto-commit khi method return
// Auto-rollback nếu throw exception
}
}
Dev Services を使用したテスト
import io.quarkus.test.junit.QuarkusTest;
import jakarta.transaction.Transactional;
@QuarkusTest
class ProductRepositoryTest {
@Test
@Transactional
void testCreateAndFind() {
Product product = new Product();
product.name = "Test Product";
product.price = new BigDecimal("100000");
product.stockQuantity = 10;
product.persist();
assertNotNull(product.id);
Product found = Product.findById(product.id);
assertEquals("Test Product", found.name);
}
@Test
void testPagination() {
PanacheQuery<Product> query = Product.findAll()
.page(Page.of(0, 5));
List<Product> page1 = query.list();
assertTrue(page1.size() <= 5);
}
}
演習
- エンティティの作成
Productライフサイクル コールバックを含むアクティブ レコード パターンを使用 (@PrePersist、@PreUpdate) 2.作成Categoryリポジトリ パターンを持つエンティティ、関係の確立@OneToMany - ページネーションとソートを備えた製品の CRUD エンドポイントを実装する
- キーワードで製品を検索するためのカスタム クエリを作成します (JPQL)
- PostgreSQL による全文検索の追加
tsvector(ネイティブクエリ) - 次のコマンドを使用して Order サービスを作成します
@Transactional在庫の一貫性を確保する
概要
- Dev Services は PostgreSQL コンテナを自動的に実行します。ローカルにインストールする必要はありません
- アクティブな記録 (
extends PanacheEntity) — 自己存在エンティティpersist()、find()、delete() - リポジトリ パターン (
implements PanacheRepository<T>) — 個別の懸念事項 - PanacheQuery はページネーションをサポートします (
page())、カウント(count())、並べ替え(Sort.by()) @Transactionalアトミック性の確保、リターン時の自動コミット、例外時の自動ロールバック- JPQL + ネイティブ SQL: Panache では不十分な複雑なクエリ用
次の記事: Bean の検証とエラー処理 — 専門的なデータとエラー処理。