让我们从一个典型的实体示例开始
我们长期以来一直被教导编写的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中的所有
persist
、delete
、findById
、findAll
方法。所有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>
-
如果您的查询以
FROM
或SELECT
开头,它将保持不变作为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
进行编码。
通过复制我们的快速入门
克隆Git仓库:git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或下载存档。
快速入门位于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