跟我学JHipster-JHipster管理对象之间的关系

摘要: 本文参考官方文档 Managing relationships,详细介绍如何通过 JHipster 来创建 Entity 之间的关系。通过本文的学习,能够熟练使用 JHipster Domain Language 创建对象间的关系。

概述

首先我们需要知道,JHipster 维护的这种关系只适用于关系型数据库,也就是使用 JPA。其他的 NoSQL 不适用该文档。

该工具的强大之处在于,一次定义,生成附加的所有内容,包括如下:

  1. 带有详细 JPA 注解的 Java POJO
  2. 生成 Spring Data 对应的 Repository
  3. 生成正确的 Liquibase 变化文档,用于帮助建立数据库
  4. 生成测试
  5. 生成前端的对象 (AngularReact

工具

有若干种生成方式:

  1. 使用 JHipster UML,首先定义使用工具定义 UML,然后使用 JHipster UML 生成
  2. 使用 JDL,首先定义 JDL,然后在使用 import-jdl 生成,JDL 可以使用 JDL Studio,或者其他插件编辑
  3. 使用 JHipster 交互式命令行。

本文主要介绍第二种方式生成。

对象间关系及生成方式

目前有如下几种关系

  1. A bidirectional one-to-many relationship
  2. A unidirectional many-to-one relationship
  3. A unidirectional one-to-many relationship
  4. Two one-to-many relationships on the same two entities
  5. A many-to-many relationship
  6. A one-to-one relationship
  7. 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
2
3
4
5
6
entity Owner
entity Car

relationship OneToMany {
Owner{car} to Car{owner}
}

单向 many-to-one 关系

这个关系是仅仅关注第一个双向的 one-to-many 反过来的关系。也就是说我们只关注 Car 的拥有者是谁,而不能搜索 Owner 有多少 Car

数据库层面,数据表 Car 会有一个 Foreign Key 指向一个 Owner

单向关系的设计目的有两点:

  1. 从业务逻辑出发,关系确实如此。
  2. 轻微性能的提升,因为 Owner 不需要维护其 Car 的列表。
1
Owner (1) <----- (*) Car

JDL 表达,这里上面有三点不同:一是关系的名字变成了 ManyToOne;二是 Car 放在了前面,表示 Many;三是 Owner 后面没有了关系名称的大括号。

1
2
3
4
5
6
entity Owner
entity Car

relationship ManyToOne {
Car{owner} to Owner
}

单向 one-to-many 关系

单向(unidirectional)意味着,只能从前者向后者搜索,反过来是无法进行搜索的。也就是说,只能通过 Owner 找到其所拥有的 Car,但是反过来,不能从 Car 查找其拥有者。

数据库层面,数据表 Car 会有一个 Foreign Key 指向一个 Owner

1
Owner (1) -----> (*) Car

这种关系不被支持,可能是因为使用场景很少,参考该 issuer #1569

推荐做法就是使用前面的双向关系。

1
2
3
4
5
6
entity Owner
entity Car

relationship OneToMany {
Owner{car} to Car{owner}
}

或者修改生成的代码:(不推荐)

  1. 移除 @OneToMany 注解中的 mappedBy 属相
  2. 同时需要处理 mvn liquibase:diff

两个 one-to-many关系应用于相同的两个实体

一个人可以拥有多辆车,同时一个人可以驾驶多辆车:

1
2
Person (1) <---owns-----> (*) Car
Person (1) <---drives---> (*) Car

对应的 JDL 表达方法如下:

1
2
3
4
5
6
7
8
9
10
entity Person
entity Car

relationship OneToMany {
Person{ownedCar} to Car{owner}
}

relationship OneToMany {
Person{drivedCar} to Car{driver}
}

我们可以发现两个关系的实体是一样的,但是其关系名称不同,通过这样的表达,生成的实体会有两个字段,虽然类型一样,但是名称和含义不同。在数据库表中的定义也不同。

many-to-many 关系

一个驾驶者可以驾驶多辆车,一辆车可以被多个驾驶者驾驶。

数据库层面,两个表会有一个 Join Table

1
Driver (*) <-----> (*) Car

对应的 JDL 表达方法如下:

1
2
3
4
5
6
entity Driver
entity Car

relationship ManyToMany {
Car{driver} to Driver{car}
}

one-to-one 关系

两个关系是唯一的,其关系如下

1
Driver (1) <-----> (1) Car

JDL 如下

1
2
3
4
5
6
entity Driver
entity Car

relationship OneToOne {
Car{driver} to Driver{car}
}

单向 one-to-one 关系

两个关系是唯一的,但是只能从左侧查找右侧,其关系如下

1
Citizen (1) -----> (1) Passport

JDL 如下

1
2
3
4
5
6
entity Citizen
entity Passport

relationship OneToOne {
Citizen{passport} to Passport
}

命令行工具生成对象

我们通过一个例子来说明命令行工具生成实体的问题的含义,以及对应的 JDL

比如我们要生成一个双向的 one-to-many 关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jhipster entity Owner
...
Generating relationships with other entities
? Do you want to add a relationship to another entity? Yes
? What is the name of the other entity? Car
? What is the name of the relationship? car
? What is the type of the relationship? one-to-many
? What is the name of this relationship in the other entity? owner

jhipster entity Car
...
Generating relationships with other entities
? Do you want to add a relationship to another entity? Yes
? What is the name of the other entity? Owner
? What is the name of the relationship? owner
? What is the type of the relationship? many-to-one
? When you display this relationship with Angular, which field from 'Owner' do you want to use? id
  1. 首先询问是否添加关系,当前的实体就是在左侧,是关系的拥有者
  2. 关系的另一方实体名称
  3. 为该关系命名,决定了该关系在该实体的对象字段名称
  4. 指定关系名称
  5. 前端显示关系实体的字段

同时为了生成一个双向的关系,需要生成两个关系。如果是单向则无需这样,比如生成单向的 many-to-one 关系

1
2
3
4
5
6
7
8
9
10
11
12
13
jhipster entity Owner
...
Generating relationships with other entities
? Do you want to add a relationship to another entity? No

jhipster entity Car
...
Generating relationships with other entities
? Do you want to add a relationship to another entity? Yes
? What is the name of the other entity? Owner
? What is the name of the relationship? owner
? What is the type of the relationship? many-to-one
? When you display this relationship with Angular, which field from 'Owner' do you want to use? id

关于 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 只能添加如下几种关系:

  1. many-to-one,比如一个 Car 可以有 many-to-one 关系到 User。这将会生成一个特殊的查询在新的实体 Repository,从而能够查找该用户的拥有的实体。再生成的前端代码中,Car将会有一个User的下拉列表。
  2. many-to-manyone-to-one关系,其他的实体必须是关系的拥有者,也就是 User 不能出现在 to 的左边。比如一个 Team 可以和 Usermany-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!