跳到主要内容

4. Micronaut Data JPA Hibernate

Micronaut Data JPA 增加了对具有编译时生成的查询、JPA 标准和事务管理的存储库的支持。

4.1 JPA 注解

Micronaut Data JPA 4.0.0 版本支持 Hibernate 6,而早期版本支持Hibernate 5。您可以使用 javax.persistenceannotations(如 javax.persistence.Entity)来映射您的实体。

对于 Hibernate 6 Micronaut Data JPA 支持 jakarta.persistence 注解,如 jakarta.persistence.Entity 来映射你的实体。

4.2 快速入门

最快速的入门方法是使用 Micronaut Launch 创建一个新的 Micronaut 应用程序,并选择 data-jpa、数据库驱动程序、池化和数据库迁移框架功能。

提示

你还可以在 Micronaut 指南中找到关于构建 Micronaut Data JPA 应用程序的精彩指南,包括各种语言的示例代码:使用 Micronaut Data JPA 访问数据库

点击下表中的一个链接,您将进入 Micronaut Launch,其中的相应选项已根据您选择的语言和构建工具进行了预配置:

表 1. 使用 Micronaut Launch 创建应用程序

GradleMaven
Java打开打开]
Kotlin打开打开
Groovy打开打开

使用 CLI 创建应用程序

# For Maven add: --build maven
$ mn create-app --lang java example --features data-jpa,flyway,mysql,jdbc-hikari

或通过 curl

使用 curl 创建应用程序

提示

使用 JDBC 驱动时,需要从 Micronaut SQL 项目中添加 JDBC 连接池模块(Hikari、Tomcat JDBC 或 DBCP)。

有关配置 HibernateJDBC 和池的更多信息,使用 Micronaut SQL 项目文档。

您需要在应用程序配置文件中配置数据源。例如,对于 H2:

datasources.default.url=jdbc:h2:mem:devDb
datasources.default.driverClassName=org.h2.Driver
datasources.default.username=sa
datasources.default.password=
datasources.default.schema-generate=CREATE_DROP
datasources.default.dialect=H2

并在应用程序配置文件中添加以下配置。

jpa.default.entity-scan.packages=example.domain

其中,jpa.default.entity-scan.packages 引用了 @Entity 类所在的根包。

并确保正确配置了实现。

然后,您就可以定义一个 @Entity

package example;

import io.micronaut.serde.annotation.Serdeable;

import jakarta.persistence.*;

@Serdeable
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private int pages;

public Book(String title, int pages) {
this.title = title;
this.pages = pages;
}

public Book() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public int getPages() {
return pages;
}

public void setPages(int pages) {
this.pages = pages;
}
}

随后是一个从 CrudRepository 扩展而来的接口:

package example

import io.micronaut.context.annotation.Executable
import io.micronaut.context.annotation.Parameter
import io.micronaut.data.annotation.*
import io.micronaut.data.model.*
import io.micronaut.data.repository.CrudRepository

@Repository // (1)
interface BookRepository extends CrudRepository<Book, Long> { // (2)
@Executable
Book find(String title)
}
  1. 接口使用 @Repository 进行注解
  2. CrudRepository 接口有两个通用参数,即实体类型(本例中为 Book)和 ID 类型(本例中为 Long)。

现在,您可以对实体执行 CRUD(创建、读取、更新、删除)操作。example.BookRepository 的实现是在编译时创建的。要获得对它的引用,只需注入 Bean:

@Inject
BookRepository bookRepository;

保存实例(创建)

要保存实例,请使用 CrudRepository 接口的 save 方法:

Book book = new Book();
book.setTitle("The Stand");
book.setPages(1000);
bookRepository.save(book);

检索实例(读取)

要读取一本 book,请使用 findById

book = bookRepository.findById(id).orElse(null);

更新实例(更新)

要更新实例,请再次使用 save

book.setTitle("Changed");
bookRepository.save(book);

对于部分实体更新,可以使用类似这样的自定义更新方法:

@QueryHint(name = "jakarta.persistence.FlushModeType", value = "AUTO")
void updatePages(@Id Long id, @Parameter("pages") int pages);

在本例中,为了让更新在当前会话中传播,可以添加 QueryHint 注解来强制会话刷新。

对于 Hibernate 6,需要使用 jakarta.persistence.FlushModeType 代替 javax.persistence.FlushModeType

删除实例(删除)

要删除一个实例,请使用 deleteById

bookRepository.deleteById(id)

4.3 使用 Micronaut 数据 JPA Hibernate 记录 SQL

在应用程序的配置中,将 jpa.default.properties.hibernate.show_sqljpa.default.properties.hibernate.format_sql 设置为 true,就可以记录查询。

4.4 连接查询

为了优化你的查询,你可能需要改变连接,以便在结果集中准确获取你需要的数据。

提示

如果出现 LazyInitializationException,这并不是 Micronaut Data 或 Hibernate 中的错误,而是表明你应该改变你的查询连接,以获取实现用例所需的相关数据。

考虑一个 Product 实体:

package example;

import jakarta.persistence.*;

@Entity
public class Product {

@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Manufacturer manufacturer;

public Product(String name, Manufacturer manufacturer) {
this.name = name;
this.manufacturer = manufacturer;
}

public Product() {
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Manufacturer getManufacturer() {
return manufacturer;
}

public void setManufacturer(Manufacturer manufacturer) {
this.manufacturer = manufacturer;
}
}

这与 Manufacturer 实体有关联:

package example;

import io.micronaut.configuration.hibernate.jpa.proxy.GenerateProxy;
import org.hibernate.annotations.BatchSize;

import jakarta.persistence.*;

@Entity
@GenerateProxy
@BatchSize(size = 10)
public class Manufacturer {
@Id
@GeneratedValue
private Long id;
private String name;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

在这种情况下,当您从数据库读取每个 Product 时,还需要额外的选择来检索每个 ProductManufacturer。这将导致 N + 1 次查询。

要解决这个问题,您可以在仓库接口上使用 @Join 注解来指定执行 JOIN FETCH 以检索相关的 Manufacturer

@Repository
public interface ProductRepository extends CrudRepository<Product, Long>, JpaSpecificationExecutor<Product> {
@Join(value = "manufacturer", type = Join.Type.FETCH) // (1)
List<Product> list();
}
  1. @Join 用于表示应包含 JOIN FETCH 子句。

请注意,@Join 注解是可重复的,因此可以为不同的关联指定多次。此外,注解的类型成员可用于指定连接类型,例如 LEFT、INNER 或 RIGHT。

JPA 2.1 实体图

使用 JPA 2.1 实体图是指定查询连接的 JPA 特定替代方法。使用实体图时,您可以由 JPA 实现来选择要使用的适当连接类型:

@EntityGraph(attributePaths = {"manufacturer", "title"}) // (1)
List<Product> findAll();
  1. attributePaths 成员用于指定实体图中要包含的路径。

4.5 明确查询

如果想对编译时生成的查询进行更多控制,可以使用 @Query 注解指定一个显式查询:

@Query("FROM Book b WHERE b.title = :t ORDER BY b.title")
List<Book> listBooks(String t);

您可以使用冒号(:)指定命名参数,冒号后跟名称,这些参数必须与指定给方法的参数相匹配,否则会出现编译错误,请使用反斜杠 \: 转义冒号,因为它不是参数指定。

请注意,如果该方法返回一个用于分页的 Page,则必须使用 @Query 注解中的 countQuery 成员额外指定一个执行等价计数的查询。

4.6 本地查询

在 JPA 中使用 Micronaut Data 时,可以通过在 @Query 注解中将 nativeQuery 设置为 true 来执行本地 SQL 查询:

@Query(value = "select * from books b where b.title like :title limit 5",
nativeQuery = true)
List<Book> findNativeBooks(String title);

上述示例将针对数据库执行原始 SQL。

提示

对于返回 PagePagination 查询,您还需要指定一个本地 countQuery

4.7 JPA 规范

基于与 Spring Data 相同的概念,当您需要通过组合 JPA 标准动态创建查询时,您可以实现 JpaSpecificationExecutor 接口,该接口提供了多个接收 Specification 实例的方法,可与现有的仓库接口结合使用。

Specification 接口代表了一个简单的基于标准的 API 入口点:

public interface Specification<T> {

@Nullable
Predicate toPredicate(@NonNull Root<T> root,
@NonNull CriteriaQuery<?> query,
@NonNull CriteriaBuilder criteriaBuilder);

}

下面的示例演示了使用规范进行自定义实体过滤:

class Specifications {

public static Specification<Product> nameEquals(String name) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.equal(root.get("name"), name);
}

public static Specification<Product> nameEqualsCaseInsensitive(String name) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.equal(criteriaBuilder.lower(root.get("name")), name.toLowerCase());
}
}

您可以在仓库类中创建默认方法,并结合多种规范提供动态实现:

@Repository
public interface ProductRepository extends CrudRepository<Product, Long>, JpaSpecificationExecutor<Product> {

@Transactional
default List<Product> findByName(String name, boolean caseInsensitive, boolean includeBlank) {
Specification<Product> specification;
if (caseInsensitive) {
specification = Specifications.nameEqualsCaseInsensitive(name);
} else {
specification = Specifications.nameEquals(name);
}
if (includeBlank) {
specification = specification.or(Specifications.nameEquals(""));
}
return findAll(specification);
}

class Specifications {

public static Specification<Product> nameEquals(String name) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.equal(root.get("name"), name);
}

public static Specification<Product> nameEqualsCaseInsensitive(String name) {
return (root, query, criteriaBuilder)
-> criteriaBuilder.equal(criteriaBuilder.lower(root.get("name")), name.toLowerCase());
}
}

}
注意

在 Micronaut Data 中,首选方式是在构建时生成查询。建议仅对需要在运行时动态生成的查询使用基于标准的 API。

英文链接