摘要: 本文参考官方文档 Managing relationships,详细介绍如何通过 JHipster
来创建 Entity
之间的关系。通过本文的学习,能够熟练使用 JHipster Domain Language
创建对象间的关系。
概述
首先我们需要知道,JHipster
维护的这种关系只适用于关系型数据库,也就是使用 JPA
。其他的 NoSQL
不适用该文档。
该工具的强大之处在于,一次定义,生成附加的所有内容,包括如下:
- 带有详细
JPA
注解的Java POJO
- 生成
Spring Data
对应的Repository
- 生成正确的
Liquibase
变化文档,用于帮助建立数据库 - 生成测试
- 生成前端的对象 (
Angular
和React
)
工具
有若干种生成方式:
- 使用
JHipster UML
,首先定义使用工具定义 UML,然后使用JHipster UML
生成 - 使用
JDL
,首先定义JDL
,然后在使用import-jdl
生成,JDL
可以使用JDL Studio
,或者其他插件编辑 - 使用
JHipster
交互式命令行。
本文主要介绍第二种方式生成。
对象间关系及生成方式
目前有如下几种关系
- A bidirectional one-to-many relationship
- A unidirectional many-to-one relationship
- A unidirectional one-to-many relationship
- Two one-to-many relationships on the same two entities
- A many-to-many relationship
- A one-to-one relationship
- A unidirectional one-to-one relationship
JDL
对每一种关系都有直白的名字来表示,然后通过表达的顺序 (A to B)来表示先后关系,其中左侧成为 Owner
,最后使用大括号来表示关系的名称。
双向 one-to-many
关系
我们使用汽车(Car
)和拥有者(Owner
)来举例,一个 Owner
可以有拥有多辆 Car
,一辆 Car
只能属于一个 Owner
数据库层面,数据表 Car
会有一个 Foreign Key
指向一个 Owner
。
1 | Owner (1) <-----> (*) Car |
JDL
表达,这里我们使用 OneToMany
关键词来表达该关系,然后我们需要把 Owner
放在 to
的前面,而 Car
放在后面,表示前者是one
后者是many
。另外,因为我们的关系是双向的,所以我们在每个实体后面都加上 {}
,并使用字符串来表示其在该实体中的名称。
双向 (bidirectional
)关系意味着,两个方向都可以进行查找,通过一个 Owner
能够找到其所有拥有的 Car
,同样通过 Car
可以找到其 Owner
。
1 | entity Owner |
单向 many-to-one
关系
这个关系是仅仅关注第一个双向的 one-to-many
反过来的关系。也就是说我们只关注 Car
的拥有者是谁,而不能搜索 Owner
有多少 Car
数据库层面,数据表 Car
会有一个 Foreign Key
指向一个 Owner
。
单向关系的设计目的有两点:
- 从业务逻辑出发,关系确实如此。
- 轻微性能的提升,因为
Owner
不需要维护其Car
的列表。
1 | Owner (1) <----- (*) Car |
JDL
表达,这里上面有三点不同:一是关系的名字变成了 ManyToOne
;二是 Car
放在了前面,表示 Many
;三是 Owner
后面没有了关系名称的大括号。
1 | entity Owner |
单向 one-to-many
关系
单向(unidirectional
)意味着,只能从前者向后者搜索,反过来是无法进行搜索的。也就是说,只能通过 Owner
找到其所拥有的 Car
,但是反过来,不能从 Car
查找其拥有者。
数据库层面,数据表 Car
会有一个 Foreign Key
指向一个 Owner
。
1 | Owner (1) -----> (*) Car |
这种关系不被支持,可能是因为使用场景很少,参考该 issuer #1569。
推荐做法就是使用前面的双向关系。
1 | entity Owner |
或者修改生成的代码:(不推荐)
- 移除
@OneToMany
注解中的mappedBy
属相 - 同时需要处理
mvn liquibase:diff
两个 one-to-many
关系应用于相同的两个实体
一个人可以拥有多辆车,同时一个人可以驾驶多辆车:
1 | Person (1) <---owns-----> (*) Car |
对应的 JDL
表达方法如下:
1 | entity Person |
我们可以发现两个关系的实体是一样的,但是其关系名称不同,通过这样的表达,生成的实体会有两个字段,虽然类型一样,但是名称和含义不同。在数据库表中的定义也不同。
many-to-many
关系
一个驾驶者可以驾驶多辆车,一辆车可以被多个驾驶者驾驶。
数据库层面,两个表会有一个 Join Table
。
1 | Driver (*) <-----> (*) Car |
对应的 JDL
表达方法如下:
1 | entity Driver |
one-to-one
关系
两个关系是唯一的,其关系如下
1 | Driver (1) <-----> (1) Car |
JDL
如下
1 | entity Driver |
单向 one-to-one
关系
两个关系是唯一的,但是只能从左侧查找右侧,其关系如下
1 | Citizen (1) -----> (1) Passport |
JDL
如下
1 | entity Citizen |
命令行工具生成对象
我们通过一个例子来说明命令行工具生成实体的问题的含义,以及对应的 JDL
比如我们要生成一个双向的 one-to-many
关系:
1 | jhipster entity Owner |
- 首先询问是否添加关系,当前的实体就是在左侧,是关系的拥有者
- 关系的另一方实体名称
- 为该关系命名,决定了该关系在该实体的对象字段名称
- 指定关系名称
- 前端显示关系实体的字段
同时为了生成一个双向的关系,需要生成两个关系。如果是单向则无需这样,比如生成单向的 many-to-one
关系
1 | jhipster entity Owner |
关于 User 实体的特殊性
Tip: the User entity
Please note that the User entity, which is handled by JHipster, is specific. You can do:
many-to-one relationships to this entity (a Car can have a many-to-one relationship to a User). This will generate a specific query in your new entity repository, so you can filter your entity on the current security user, which is a common requirement. On the generated Angular/React client UI you will have a dropdown in Car to select a User.
many-to-many and one-to-one relationships to the User entity, but the other entity must be the owner of the relationship (a Team can have a many-to-many relationship to User, but only the team can add/remove users, and a user cannot add/remove a team). On the Angular/React client UI, you will also be able to select a User in a multi-select box.
我们在针对现有的 User
只能添加如下几种关系:
many-to-one
,比如一个Car
可以有many-to-one
关系到User
。这将会生成一个特殊的查询在新的实体Repository
,从而能够查找该用户的拥有的实体。再生成的前端代码中,Car
将会有一个User
的下拉列表。many-to-many
和one-to-one
关系,其他的实体必须是关系的拥有者,也就是User
不能出现在to
的左边。比如一个Team
可以和User
是many-to-many
的关系,但是只能让Team
添加删除User
,而反过来是不行的。在前端UI
中,将会出现一个用户的多选框。
实际应用
我们在实际项目中,会有很多和 User
相关的实体,但是 JHipster
无法通过 JDL
修改 User 实体,所以需要另辟蹊径:参考该文: https://stackoverflow.com/a/44076306/2000468
I think the best solution is a compromise between the two solutions offered by @Pedro and @alcuvi (who references to the JHipster Documentation):
First, create an “ExtendedUser” entity with the additional fields (don’t forget to use git, you will have to undo this/delete the entity). A one-to-one relationship to “User” is not necessary.
After that, you can copy many parts from “ExtendedUser” to the other parts of the JHipster Application:
Liquibase changelog columns (also add them to users.csv)
ExtendedUser.java → User.java and UserDTO.java
extendedUser-dialog.* → register.html/.controller.js and settings.html/.controller.js
Adapt AccountResource.java and UserService.java (and UnitTests if you use them). This step is mostly done by using getters and setters copied in the step before. JHipster Documentation (https://jhipster.github.io/tips/022_tip_registering_user_with_additional_information.html) might be helpful here.
Delete the “ExtendedUser” Entity (using git, or manually, see also: How to delete an entity after creating it using jhipster?)
The advantages are:
Using JHipster code generation capabilities
No additional entity (which comes with new DB tables and many files)
I hope this information will help other developers in the future!