Quarkus - 用 Panache 简化 Hibernate ORM

Hibernate ORM 是 JPA 实现的事实标准, 提供了完整的对象关系映射。 它支持复杂的映射,但是有点琐碎不够简单. Hibernate ORM with Panache 致力于使你的实体(Entity) 在 Quarkus 中的日常编码有趣。

开始:示例

我们在 Panache 中所做的就是允许您这样编写 Hibernate ORM实体:

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }

    public static void deleteStefs(){
        delete("name", "Stef");
    }
}

您是否注意到代码更紧凑和更易读? 这看起来有趣吗?继续阅读!

一开始 list() 方法可能令人惊讶。它转换成 HQL(JP-QL)查询语句,并用适当的上下文处理。这使得代码非常简洁但可读性强。
上面描述的本质上是 活动记录模式(active record pattern) ,有时也称为实体模式. Hibernate with Panache 也可通过 PanacheRepository 使用更经典的 仓库模式(repository pattern) .

完整代码

我们建议按照以下介绍进行操作,并逐步创建应用程序。 但是,您可以直接转到完成的示例。

克隆 Git 库: git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或下载 存档 .

完整代码位于 hibernate-orm-panache-quickstart 目录.。

配置 Hibernate ORM with Panache

开始:

  • application.properties 中添加设置

  • 使用 @Entity 注解你的实体类(entities)

  • 实体类改为从 PanacheEntity 继承 (如果使用仓库模式则无需继承)

pom.xml 中, 增加下边依赖:

  • Panache JPA extension

  • 你的 JDBC 驱动 extension (quarkus-jdbc-postgresql, quarkus-jdbc-h2, quarkus-jdbc-mariadb, …​)

<dependencies>
    <!-- Hibernate ORM specific dependencies -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>

    <!-- JDBC driver dependencies -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>
</dependencies>

然后在 application.properties 中添加相关的配置项.

# 配置你的数据源(datasource)
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.password = connor
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydatabase

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create

方式 1: 活动记录模式(active record)

定义你的实体(entity)

要定义 Panache 实体(entity), 只需要简单的从 PanacheEntity 继承,用 @Entity 注解,并添加列作为 public 字段:

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;
}

可以将所有 JPA 列注解为 public 字段。如果类中个别字段不需要保存到数据库,使用 @Transient 注解这些字段。如果需要写 accessors, 可以:

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

在你提供了字段 get/set 方法后, 取 person.name 时实际会调用你提供的 getName() 方法,字段 set 也一样。 This allows for proper encapsulation at runtime as all fields calls will be replaced by the corresponding getter/setter calls.

最有用的操作

在你写好 entity 后,你能执行下列最常用的操作:

// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it
person.persist();

// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.

// check if it's persistent
if(person.isPersistent()){
    // delete it
    person.delete();
}

// getting a list of all Person entities
List<Person> allPersons = Person.listAll();

// finding a specific person by ID
person = Person.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = Person.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

// finding all living persons
List<Person> livingPersons = Person.list("status", Status.Alive);

// counting all persons
long countAll = Person.count();

// counting all living persons
long countAlive = Person.count("status", Status.Alive);

// delete all living persons
Person.delete("status", Status.Alive);

// delete all persons
Person.deleteAll();

// update all living persons
Person.update("name = 'Moral' where status = ?1", Status.Alive);

所有 list 方法都有等效的 stream 版本.

try (Stream<Person> persons = Person.streamAll()) {
    List<String> namesButEmmanuels = persons
        .map(p -> p.name.toLowerCase() )
        .filter( n -> ! "emmanuel".equals(n) )
        .collect(Collectors.toList());
}
stream 方法需要事务.
跟执行 I/O 操作一样,它们也需要通过 close() 方法或通过 try-with-resource 方式关闭底层的 ResultSet . 否则,您将看到 Agroal 发出的警告,这些警告将为您关闭底层的 ResultSet

添加实体方法

在实体本身内部添加自定义查询。 这样,您和您的同事可以轻松找到它们,并且查询与它们所操作的对象位于同一位置。 Panache 活动记录(Active Record)的方式将它们作为静态方法添加到实体类中。

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Person findByName(String name){
        return find("name", name).firstResult();
    }

    public static List<Person> findAlive(){
        return list("status", Status.Alive);
    }

    public static void deleteStefs(){
        delete("name", "Stef");
    }
}

方式 2: 使用仓库模式(repository pattern)

定义实体

当使用仓库模式, 你可以将你的实体定义为常规 JPA 实体.

@Entity
public class Person {
    @Id @GeneratedValue private Long id;
    private String name;
    private LocalDate birth;
    private Status status;

    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 LocalDate getBirth() {
        return birth;
    }
    public void setBirth(LocalDate birth) {
        this.birth = birth;
    }
    public Status getStatus() {
        return status;
    }
    public void setStatus(Status status) {
        this.status = status;
    }
}
如果不想定义实体的 getters/setters , 可以继承 PanacheEntityBase 这样 Quarkus 会生成它们. 你甚至可以继承 PanacheEntity 以利用其提供的默认 ID.

定义仓库(repository)

使用仓库,与活动记录(active record) 模式一样的便利方法,实现 PanacheRepository 来注入到你的 repository

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

   // put your custom logic here as instance methods

   public Person findByName(String name){
       return find("name", name).firstResult();
   }

   public List<Person> findAlive(){
       return list("status", Status.Alive);
   }

   public void deleteStefs(){
       delete("name", "Stef");
  }
}

PanacheEntityBase 中定义的所有方法在你的 repository 中都可以用, 所以跟使用活动记录模式是一样的, 除了你需要注入它:

@Inject
PersonRepository personRepository;

@GET
public long count(){
    return personRepository.count();
}

最有用操作

写好 repository 后,可以执行以下常用操作:

// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it
personRepository.persist(person);

// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.

// check if it's persistent
if(personRepository.isPersistent(person)){
    // delete it
    personRepository.delete(person);
}

// getting a list of all Person entities
List<Person> allPersons = personRepository.listAll();

// finding a specific person by ID
person = personRepository.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = personRepository.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

// finding all living persons
List<Person> livingPersons = personRepository.list("status", Status.Alive);

// counting all persons
long countAll = personRepository.count();

// counting all living persons
long countAlive = personRepository.count("status", Status.Alive);

// delete all living persons
personRepository.delete("status", Status.Alive);

// delete all persons
personRepository.deleteAll();

// update all living persons
personRepository.update("name = 'Moral' where status = ?1", Status.Alive);

所有 list 方法都有等效的 stream 版本.

Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());
stream 方法需要事务.
文档剩余部分仅显示基于活动记录模式的用法, 但请记住,它们也可以与仓库模式一起用。 为简洁起见,已省略了仓库模式示例。

高级查询

分页

如果表包含少量数据,可能只需要使用 liststream 方法。 对于较多数据,可以使用等效的 find 方法,该方法返回一个 PanacheQuery 可以用它进行分页:

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use pages of 25 entries at a time
livingPersons.page(Page.ofSize(25));

// get the first page
List<Person> firstPage = livingPersons.list();

// get the second page
List<Person> secondPage = livingPersons.nextPage().list();

// get page 7
List<Person> page7 = livingPersons.page(Page.of(7, 25)).list();

// get the number of pages
int numberOfPages = livingPersons.pageCount();

// get the total number of entities returned by this query without paging
int count = livingPersons.count();

// and you can chain methods of course
return Person.find("status", Status.Alive)
    .page(Page.ofSize(25))
    .nextPage()
    .stream()

PanacheQuery 类型还有许多其他方法来处理分页和返回流。

排序

所有接受查询字符串的方法还接受以下简化的查询形式:

List<Person> persons = Person.list("order by name,birth");

这些方法还可接受一个可选 Sort 参数,该参数允许您排序:

List<Person> persons = Person.list(Sort.by("name").and("birth"));

// and with more restrictions
List<Person> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);

Sort 类有很多方法来添加列,并指定排序方向。

简化查询

通常,HQL查询具有以下形式:from EntityName [where …​] [order by …​],末尾带有可选元素。

如果您的 select 查询不是以 from 开头,我们支持以下其他形式:

  • order by …​ which will expand to from EntityName order by …​

  • <singleColumnName> (and single parameter) which will expand to from EntityName where <singleColumnName> = ?

  • <query> will expand to from EntityName where <query>

如果您的 update 查询不是以 update 开头,我们支持以下其他形式:

  • from EntityName …​ which will expand to update from EntityName …​

  • set? <singleColumnName> (and single parameter) which will expand to update from EntityName set <singleColumnName> = ?

  • set? <update-query> will expand to update from EntityName set <update-query>

您也可以使用简单的 HQL 编写查询 :
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update from Person set name = 'Moral' where status = ?", Status.Alive);

Query 参数

您可以按索引(从1开始)传递查询参数,如下所示:

Person.find("name = ?1 and status = ?2", "stef", Status.Alive);

或按名称使用 Map:

Map<String, Object> params = new HashMap<>();
params.put("name", "stef");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);

或直接使用 Parameters 类或用其构造一个 Map

// generate a Map
Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive).map());

// use it as-is
Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive));

每个查询操作都接受通过索引 (Object…​) 或名称((Map<String,Object> or Parameters) 传递参数。

事务

确保在事务内使用修改数据库的方法(例如 entity.persist() )。 用 @Transactional 注解 CDI bean 方法可以做到这一点,并使该方法成为事务边界。 我们建议您在应用程序入口点边界(例如REST端点控制器)这样做。

JPA批量处理您对实体所做的更改,并在事务结束时或在查询之前发送更改(称为“刷新”)。 这通常是一件好事,因为它效率更高。 但是,如果您要检查乐观的锁定失败,立即进行对象验证或通常希望立即获得反馈,则可以通过调用 entity.flush() 来强制执行刷新操作,甚至可以使用 entity.persistAndFlush() 调用单个方法。 这将允许您捕获 PersistenceException 一般JPA将这些更改发送到数据库时可能发生。

请记住,这效率较低,所以不要滥用它。 而且事务仍必须提交。

这是使用 flush 方法在特定情况下触发 PersistenceException 的例子:

@Transactional
public void create(Parameter parameter){
    try {
        //Here I use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
        return parameterRepository.persistAndFlush(parameter);
    }
    catch(PersistenceException pe){
        LOG.error("Unable to create the parameter", pe);
        //in case of error, I save it to disk
        diskPersister.save(parameter);
    }
}

Lock management

Panache provides direct support for database locking with your entity/repository, using findById(Object, LockModeType) or find().withLock(LockModeType).

The following examples are for the entity pattern, but the same can be used with repositories.

First: Locking using findById().

public class PersonEndpoint {

    @GET
    @Transactional
    public Person findByIdForUpdate(Long id){
        Person p = Person.findById(id, LockModeType.PESSIMISTIC_WRITE);
        //do something useful, the lock will be released when the transaction ends.
        return person;
    }

}

Second: Locking in a find().

public class PersonEndpoint {

    @GET
    @Transactional
    public Person findByNameForUpdate(String name){
        Person p = Person.find("name", name).withLock(LockModeType.PESSIMISTIC_WRITE).findOne();
        //do something useful, the lock will be released when the transaction ends.
        return person;
    }

}

Be careful that locks are released when the transaction ends, so the method that invokes the lock query must be annotated with the @Transactional annotation.

Custom IDs

IDs are often a touchy subject, and not everyone’s up for letting them handled by the framework, once again we have you covered.

You can specify your own ID strategy by extending PanacheEntityBase instead of PanacheEntity. Then you just declare whatever ID you want as a public field:

@Entity
public class Person extends PanacheEntityBase {

    @Id
    @SequenceGenerator(
            name = "personSequence",
            sequenceName = "person_id_seq",
            allocationSize = 1,
            initialValue = 4)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
    public Integer id;

    //...
}

If you’re using repositories, then you will want to extend PanacheRepositoryBase instead of PanacheRepository and specify your ID type as an extra type parameter:

@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
    //...
}

How and why we simplify Hibernate ORM mappings

When it comes to writing Hibernate ORM entities, there are a number of annoying things that users have grown used to reluctantly deal with, such as:

  • Duplicating ID logic: most entities need an ID, most people don’t care how it’s set, because it’s not really relevant to your model.

  • Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields, then generate getters and setters for those fields, even if they don’t actually do anything more than read/write the fields.

  • Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them (DAOs, Repositories), but really that requires an unnatural split between the state and its operations even though we would never do something like that for regular objects in the Object Oriented architecture, where state and methods are in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repository where you need to do entity operations, which breaks your edit flow and requires you to get out of the code you’re writing to set up an injection point before coming back to use it.

  • Hibernate queries are super powerful, but overly verbose for common operations, requiring you to write queries even when you don’t need all the parts.

  • Hibernate is very general-purpose, but does not make it trivial to do trivial operations that make up 90% of our model usage.

With Panache, we took an opinionated approach to tackle all these problems:

  • Make your entities extend PanacheEntity: it has an ID field that is auto-generated. If you require a custom ID strategy, you can extend PanacheEntityBase instead and handle the ID yourself.

  • Use public fields. Get rid of dumb getter and setters. Under the hood, we will generate all getters and setters that are missing, and rewrite every access to these fields to use the accessor methods. This way you can still write useful accessors when you need them, which will be used even though your entity users still use field accesses.

  • With the active record pattern: put all your entity logic in static methods in your entity class and don’t create DAOs. Your entity superclass comes with lots of super useful static methods, and you can add your own in your entity class. Users can just start using your entity Person by typing Person. and getting completion for all the operations in a single place.

  • Don’t write parts of the query that you don’t need: write Person.find("order by name") or Person.find("name = ?1 and status = ?2", "stef", Status.Alive) or even better Person.find("name", "stef").

That’s all there is to it: with Panache, Hibernate ORM has never looked so trim and neat.

Defining entities in external projects or jars

Hibernate ORM with Panache relies on compile-time bytecode enhancements to your entities. If you define your entities in the same project where you build your Quarkus application, everything will work fine. If the entities come from external projects or jars, you can make sure that your jar is treated like a Quarkus application library by indexing it via Jandex, see How to Generate a Jandex Index in the CDI guide. This will allow Quarkus to index and enhance your entities as if they were inside the current project.

quarkus.pro 是基于 quarkus.io 的非官方中文翻译站 ,最后更新 2020/04 。
沪ICP备19006215号-8
QQ交流群:1055930959
微信群: