我们最近在这篇博客中谈到了这个Quarkus “Panache”事情,但也许我们应该更详细地了解一下它是什么,以及为什么它很重要。

首先,为了澄清,我们说的是“具有Panache的Hibernate ORM”,这大概意味着我们正在谈论一种Hibernate ORM的华丽而大胆风味。

这意味着两件重要的事情

  • 它是完整的Hibernate ORM,不是任何更少的东西

  • 但它看起来与您所习惯的不同

  • 您可以在任何时候回退,甚至混合纯JPA和Panache

让我们从一个典型的实体示例开始

我们长期以来一直被教导编写的JPA实体样式看起来是这样的

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String firstName;
    private String lastName;
    private Date birth;

    public Long getId() {
        return id;
    }

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

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }
}

然后我们通常会在这样的数据访问对象(DAO)bean中定义模型逻辑

@Singleton
public class PersonDao {

    @Inject
    private EntityManager entityManager;

    public void persist(Person person) {
        entityManager.persist(person);
    }

    public void delete(Person person) {
        entityManager.remove(person);
    }

    public Person findById(Long id) {
        return entityManager.find(Person.class, id);
    }

    public List<Person> findAll() {
        return entityManager.createQuery("FROM Person", Person.class).getResultList();
    }

    public List<Person> findByName(String lastName) {
        return entityManager.createQuery("FROM Person WHERE lastName = :lastName", Person.class).setParameter("lastName", lastName).getResultList();
    }

    public List<Person> findBornAfter(Date date) {
        return entityManager.createQuery("FROM Person WHERE birth > :date", Person.class).setParameter("date", date).getResultList();
    }
}

然后在JAX-RS REST端点中这样使用它

@Path("/")
public class PersonEndpoint {
    @Inject
    private PersonDao personDao;

    @GET
    @Path("people")
    public List<Person> all() {
        return personDao.findAll();
    }

    @GET
    @Path("people/by-name")
    public List<Person> findByName(@PathParam String name) {
        return personDao.findByName(name);
    }

    @GET
    @Path("people/born-after")
    public List<Person> findBornAfter(@PathParam Date date) {
        return personDao.findBornAfter(date);
    }

    @GET
    @Path("person/{id}")
    public Person findById(@PathParam Long id) {
        Person p = personDao.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        return p;
    }

    @PUT
    @Path("person/{id}")
    public void updatePerson(@PathParam Long id, Person newPerson) {
        Person p = personDao.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        p.setBirth(newPerson.getBirth());
        p.setFirstName(newPerson.getFirstName());
        p.setLastName(newPerson.getLastName());
    }

    @DELETE
    @Path("person/{id}")
    public void deletePerson(@PathParam Long id) {
        Person p = personDao.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        personDao.delete(p);
    }

    @POST
    @Path("people")
    public Response newPerson(@Context UriInfo uriInfo, Person newPerson) {
        Person p = new Person();
        p.setBirth(newPerson.getBirth());
        p.setFirstName(newPerson.getFirstName());
        p.setLastName(newPerson.getLastName());
        personDao.persist(p);

        URI uri = uriInfo.getAbsolutePathBuilder()
                .path(PersonEndpoint.class)
                .path(PersonEndpoint.class, "findById")
                .build(p.getId());
        return Response.created(uri).build();
    }
}
在我们的REST服务中我们没有使用任何数据传输对象,只是为了使示例简短并直接。我们不建议您在实际代码中将实体用作DTO。

关于传统JPA方式的几点观察

我们都见过成百上千个这样编写的实体和DAO。它们没有什么出奇之处。

但另一方面,它们有很多样板代码

  • 生成的ID字段。通常,您的所有实体都会使用相同的自动生成的ID类型。

  • 所有在实体中不起作用的属性访问器。它们是封装所必需的,因为Java不支持语言中的第一级属性。大多数人要么从他们的IDE中生成它们,要么使用Lombok

  • 每个DAO中的所有persistdeletefindByIdfindAll方法。所有DAO都具有这些方法。

  • 这些DAO查询都以FROM Person开始,并必须在各个地方重复使用Person.class

初探Hibernate ORM with Panache能为我们做什么

让我们跳到Quarkus,特别是Hibernate ORM with Panache能为我们做什么。结果是相当多的。

Quarkus允许我们在构建时进行大量的字节码修改,这(在许多好处中)使我们能够通过以下方式绕过Java对一等属性的支持不足:

  • 编写公共字段而不是私有+getter+setter

  • Hibernate ORM with Panache将实际上为公共字段生成任何缺失的getter+setter,并且

  • 它将所有字段访问替换为对getter和setter的访问。

这个系统允许我们像使用公共字段一样编写代码,但幕后我们仍然能够得到封装和向前兼容性,如果我们后来添加了执行更多操作的getter或setter。

此外,Hibernate ORM with Panache还提供了对已经具有许多常见方法的DAO的支持。

因此,我们可以通过扩展包含预定义自动生成的ID字段的PanacheEntity来重新编写之前的实体类

@Entity
public class Person extends PanacheEntity {

    public String firstName;
    public String lastName;
    public Date birth;
}

我们还可以通过扩展PanacheRepository来重新编写我们的DAO,以获取所有常见方法

@Singleton
public class PersonDao implements PanacheRepository<Person> {

    public List<Person> findByName(String lastName) {
        return find("lastName", lastName).list();
    }

    public List<Person> findBornAfter(Date date) {
        return find("birth > :date", Parameters.with("date", date)).list();
    }
}

这已经是在减少样板代码方面走了很长的路,不是吗?

请注意,find便利方法允许HQL,但也允许简化 HQL(可以将其视为上下文化的HQL)

  • 如果您的查询为空,它将扩展为FROM <entityType>

  • 如果您的查询以FROMSELECT开头,它将保持不变作为HQL

  • 如果您的查询以ORDER BY…​开头,它将扩展为FROM <entityType> ORDER BY…​

  • 如果您的查询只有一个属性和一个参数,它将扩展为FROM <entityType> WHERE <property> = <argument>

  • 否则,您的查询被视为WHERE…​子句,并扩展为FROM <entityType> WHERE…​

这允许许多简单查询简化到最简,同时允许复杂查询保持不变。

现在,我们的REST端点没有太大变化,但为了以防万一,让我们包括它

@Path("/")
public class PersonEndpoint {
    @Inject
    private PersonDao personDao;

    @GET
    @Path("people")
    public List<Person> all() {
        return personDao.findAll().list();
    }

    @GET
    @Path("people/by-name")
    public List<Person> findByName(@PathParam String name) {
        return personDao.findByName(name);
    }

    @GET
    @Path("people/born-after")
    public List<Person> findBornAfter(@PathParam Date date) {
        return personDao.findBornAfter(date);
    }

    @GET
    @Path("person/{id}")
    public Person findById(@PathParam Long id) {
        Person p = personDao.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        return p;
    }

    @PUT
    @Path("person/{id}")
    public void updatePerson(@PathParam Long id, Person newPerson) {
        Person p = personDao.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        p.birth = newPerson.birth;
        p.firstName = newPerson.firstName;
        p.lastName = newPerson.lastName;
    }

    @DELETE
    @Path("person/{id}")
    public void deletePerson(@PathParam Long id) {
        Person p = personDao.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        personDao.delete(p);
    }

    @POST
    @Path("people")
    public Response newPerson(@Context UriInfo uriInfo, Person newPerson) {
        Person p = new Person();
        p.birth = newPerson.birth;
        p.firstName = newPerson.firstName;
        p.lastName = newPerson.lastName;
        personDao.persist(p);

        URI uri = uriInfo.getAbsolutePathBuilder()
                .path(PersonEndpoint.class)
                .path(PersonEndpoint.class, "findById")
                .build(p.id);
        return Response.created(uri).build();
    }
}

走得更远,并去掉DAO

数据访问对象在以下情况下最有用:

  • 实体类型在针对不同堆栈编写的项目中共享。一个项目将使用为WildFly编写的DAO,另一个项目将使用Spring。

  • 实体类型在针对不同用例编写的项目中共享。一个项目将以一种方式处理实体,而另一个项目则完全不同。

  • 您需要在测试中模拟DAO。

  • 您的实体类型充满了getters和setters,以至于添加任何模型方法都将超过最大方法数。

虽然最后一个原因最初是一个笑话,但我们人类有在东西变得太大时将其分割的倾向。一个具有大量getter/setter的类让我们不愿意添加更多方法。

如果您绝对不需要DAO,它们会带来一些缺点

  • 您需要为每个实体有一个额外的类。

  • 您需要在所有使用DAO的地方注入它们。

  • 您不能在不添加字段的情况下将DAO注入到方法中,这使得在编辑流程中成本很高。

  • 您不能在没有注入并尝试自动完成的情况下发现DAO方法。如果不是您想要的DAO,您需要回到注入的字段,更改其类型和名称,然后再次尝试。

  • 您的集成开发环境(IDE)无法帮助您解决任何这些缺点。

  • 任何模型重构都需要您检查与您修改的实体对应的DAO中的查询,这使得封装性很差。

在Hibernate ORM与Panache中,我们支持DAO用例,正如我们所看到的,但我们建议用户完全跳过DAO,并将模型方法作为静态方法放在实体类中。它们可以直接通过添加static修饰符从PanacheRepository复制到实体类。

这允许您

  • 为每个实体少创建一个类

  • 将实体模型重构限制在单个文件中

  • 不需要注入来操作它们(不会打断编辑流程)

  • 具有极高的可发现性:只需输入实体类型即可自动补全所有方法

让我们回顾一下我们新的实体类

@Entity
public class Person extends PanacheEntity {

    public String firstName;
    public String lastName;
    public Date birth;

    public static List<Person> findByName(String lastName) {
        return find("lastName", lastName).list();
    }

    public static List<Person> findBornAfter(Date date) {
        return find("birth > :date", Parameters.with("date", date)).list();
    }
}

现在这是我们的REST端点

@Path("/")
public class PersonEndpoint {

    @GET
    @Path("people")
    public List<Person> all() {
        return Person.findAll().list();
    }

    @GET
    @Path("people/by-name")
    public List<Person> findByName(@PathParam String name) {
        return Person.findByName(name);
    }

    @GET
    @Path("people/born-after")
    public List<Person> findBornAfter(@PathParam Date date) {
        return Person.findBornAfter(date);
    }

    @GET
    @Path("person/{id}")
    public Person findById(@PathParam Long id) {
        Person p = Person.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        return p;
    }

    @PUT
    @Path("person/{id}")
    public void updatePerson(@PathParam Long id, Person newPerson) {
        Person p = Person.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        p.birth = newPerson.birth;
        p.firstName = newPerson.firstName;
        p.lastName = newPerson.lastName;
    }

    @DELETE
    @Path("person/{id}")
    public void deletePerson(@PathParam Long id) {
        Person p = Person.findById(id);
        if(p == null)
            throw new WebApplicationException(Status.NOT_FOUND);
        p.delete();
    }

    @POST
    @Path("people")
    public Response newPerson(@Context UriInfo uriInfo, Person newPerson) {
        Person p = new Person();
        p.birth = newPerson.birth;
        p.firstName = newPerson.firstName;
        p.lastName = newPerson.lastName;
        Person.persist(p);

        URI uri = uriInfo.getAbsolutePathBuilder()
                .path(PersonEndpoint.class)
                .path(PersonEndpoint.class, "findById")
                .build(p.id);
        return Response.created(uri).build();
    }
}

这看起来怎么样?

这是我们一直热爱的Hibernate ORM,它具有坚实的核心和大量功能,并且只需足够的光泽就可以使编写数据层更加简单。这就是我们所说的“带Panache”。

使用Hibernate ORM with Panache创建您的第一个Quarkus应用程序

使用在线项目生成器

今天就开始构建您的第一个Quarkus应用程序,选择以下扩展

  • Hibernate ORM with Panache

  • JDBC驱动程序 - PostgreSQL

  • JSON-B

并创建您的项目,以使用Hibernate ORM with Panache进行编码。

通过复制我们的快速入门

快速入门位于hibernate-orm-panache-quickstart目录中。

使用我们的命令行工具

或者,您可以使用以下命令生成一个骨架项目

mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=hibernate-orm-panache-quickstart \
    -DclassName="org.acme.rest.json.PersonEndpoint" \
    -Dpath="/" \
    -Dextensions="resteasy-jsonb, hibernate-orm-panache, jdbc-postgresql"
cd hibernate-orm-panache-quickstart

回到顶部