ORM使用扩展
[一]JPA
本系统使用的核心 ORM 组件是 JPA,即 Spring Data
套件中的 Spring Data JPA
。
为什么选用 JPA?
JPA 确实有很多可取之处。JPA 是一种规范,Hibernate 也是遵从他的规范的。可以简单理解为 Hibernate 是 JPA 的一个实现。
JPA 规范和 Spring Data
的实现,设计理念绝对是超前的。软件开发复杂性的一个解决手段是遵循 DDD(DDD 只是一种手段,但不是唯一手段)。这里着重介绍几点来聊聊 JPA 的设计中是如何体现领域驱动设计思想的,抛砖引玉。
[1]聚合根和值对象
领域驱动设计中有两个广为大家熟知的概念,Entity
(实体)和 value object
(值对象)。
Entity
的特点是具有生命周期的,有标识的,而值对象是起到一个修饰的作用,其具有不可变性,无标识。在 JPA 中 ,需要为数据库的实体类添加 @Entity
注解,相信这并不是巧合。
@Entity
@Table(name = "t_order")
public class Order {
@Id
private String oid;
@Embedded
private CustomerVo customer;
@OneToMany(cascade = {CascadeType.ALL}, orphanRemoval = true, fetch = F etchType.LAZY, mappedBy = "order")
private List<OrderItem> orderItems;
}
如上述的代码,Order
便是 DDD 中的实体,而 CustomerVo
,OrderItem
则是值对象。
程序设计者无需关心数据库如何映射这些字段,因为在 DDD 中,需要做的工作是领域建模,而不是数据建模。实体和值对象的意义不在此展开讨论,但通过此可以初见端倪,JPA 的内涵绝不仅仅是一个 ORM 框架。
[2]仓储
Repository
模式是领域驱动设计中另一个经典的模式。
在早期,我们常常将数据访问层命名为:DAO,而在 Spring Data JPA
。 中,其称之 Repository
(仓储),这也不是巧合,而是设计者有意为之。
[3]复杂的多表查询
JPA 对复杂 SQL 的支持不好,没有实体关联的两个表要做 join
,这也是它最大的诟病,但其设计理念上本身如此:
现代微服务的架构,各个服务之间的数据库是隔离的,跨越很多张表的 join
操作本就不应该交给单一的业务数据库去完成。
解决方案是:使用 ElasticSearch
做视图查询或者 Mongodb
一类的 NoSQL 去完成。问题本不是问题。
[4]总结
真正走进 JPA,真正走进 Spring Data
会发现,并不是在解决一个数据库查询问题,并不是在使用一个 ORM 框架,而是真正地在实践领域驱动设计。
重要
再次补充:DDD 只是一种手段,但不是唯一手段
[二]JPA特性
[1]生命周期回调
JPA 提供很多实体声明周期的回调注解。
@PrePersist
:在实体被持久化前调用的方法。@PostPersist
:在实体被持久化后调用的方法。@PreUpdate
:在实体被更新前调用的方法。@PostUpdate
:在实体被更新后调用的方法。@PreRemove
:在实体被删除前调用的方法。@PostRemove
: 在实体被删除后调用的方法。
利用这些注解,可以辅助我们开发一些特殊的功能支持。
例如:在 Dante Cloud 就利用这个机制,通过简单的代码就实现了权限元数据变更的监听,从而实现权限数据的及时同步。代码如下:
public class SysAttributeEntityListener extends AbstractApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(SysAttributeEntityListener.class);
@PostUpdate
protected void postUpdate(SysAttribute entity) {
log.debug("[Herodotus] |- [1] SysAttribute entity @PostUpdate activated, value is : [{}]. Trigger SysAttribute change event.", entity.toString());
publishEvent(new SysAttributeChangeEvent(entity));
}
}
[2]JPA审计
使用 JPA 提供的便捷机制,就可以很容易实现数据库审计。
这是最简单的审计形式。在这里,不跟踪所做的改变,而只是跟踪谁创建或修改了一个业务实体,以及何时完成的。
具体来说,将跟踪每个业务对象的以下额外字段:
- 创建时间
- 创建者
- 修改者
- 修改时间
@Column(name = "created_by")
@CreatedBy
private String createdBy;
@Column(name = "updated_by")
@LastModifiedBy
private String updatedBy;
@Column(name = "created_on")
@CreatedDate
private Date createdOn;
@Column(name = "updated_on")
@LastModifiedDate
private Date updatedOn;
Dante Cloud 实际的应用代码示例如下:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseJpaEntity extends BaseEntity {
@Schema(name = "创建人")
@Column(name = "create_by")
@CreatedBy
private String createBy;
@Schema(name = "最后修改")
@Column(name = "update_by")
@LastModifiedBy
private String updateBy;
@Schema(name = "排序值")
@Column(name = "ranking")
private Integer ranking = 0;
public Integer getRanking() {
return ranking;
}
public void setRanking(Integer ranking) {
this.ranking = ranking;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public String getUpdateBy() {
return updateBy;
}
public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("createBy", createBy)
.add("updateBy", updateBy)
.add("ranking", ranking)
.toString();
}
}
[3]实体审计
尽管JPA审计为基本的审计提供了简单的配置,但它只提供以下信息
- 实体是什么时候创建的,谁创建了它。
- 实体最后一次修改是什么时候,谁修改了它。
它并没有为你提供一个实体的所有修改/更新的细节,例如一个客户实体可能被修改了5次。通过JPA审计,我没有办法找出在这5次更新中,实体的哪些地方被修改了,以及谁做了这些修改。
因此,Hibernate Envers
,它为一个实体提供完整的 审计历史为一个实体提供完整的信息
Dante Cloud 已经添加了实体审计支持。使用时,仅需要在需要开启审计的实体类中,加入@Audited
注解即可。例如:
@Audited
public class Customer {
....
}
Envers 审计会额外的增加审计数据存储表,如果一些配置信息不符合你实际的使用需求,那么可以通过以下配置进行修改:
spring:
jpa:
properties:
org:
hibernate:
envers:
audit_table_suffix: _AUDIT
revision_field_name: REVISION_ID
revision_type_field_name: REVISION_TYPE
[4]软删除(逻辑删除)
在 6.4 版本中,Hibernate 团队为 Hibernate ORM 引入了官方的软删除功能。现在只需要 @SoftDelete
注解即可激活实体类的软删除。然后,Hibernate 会生成软删除记录所需的 SQL UPDATE 语句,并调整所有查询语句以排除软删除的记录。
示例代码:
@Entity
@Table(name = "tag")
@SoftDelete
public class Tag {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
}
[5]注意事项
以上所述的 JPA 所包含的特性,除了 JPA 审计之外,其它功能均没有进行统一设置,一方面没有必要因为并不是所有业务都需要,另一方面增加过多的处理势必会增加处理损耗影响性能。所以需要您根据自己的实际需求进行个性化设置。
[三]Mybatis Plus 使用
因为一部分用户只习惯使用、甚至只会使用 Mybatis Plus,所以本系统也集成了 Mybatis Plus ORM 组件。
Dante Cloud MyBatis Plus 可以与 JPA 同时使用。
注意
这里所说的同时使用是指:新增的代码即可以选择使用 JPA 也可以选择使用 MyBatis Plus,两者不冲突。
但是要注意的是同时使用不是指,用 JPA 编写的代码可以自动切换为 MyBatis Plus 的代码。这个是无法做到的,两者的机制完全不同。
所以系统现有使用 JPA 编写代码,如果你不习惯或者不喜欢,想要切换为 MyBatis Plus,唯一的途径就是将所有的 JPA 代码自己用 MyBatis Plus 全部重新写一遍。
[四]QueryDSL
JPA 确实对复杂 SQL 支持不是那么理想,如果您有个查询使用 JPA 使用觉得不方便,那么可以尝试使用 QueryDSL 对查询进行改进。
QueryDSL 是一个框架,它可以通过它提供的的API帮助我们构建静态类型的SQL-like查询,也就是在上面我们提到的组织查询方式。可以通过诸如Querydsl之类的流畅API构造查询。
QueryDSL 是出于以类型安全的方式维护HQL查询的需要而诞生的。 HQL查询的增量构造需要String连接,这导致难以阅读的代码。通过纯字符串对域类型和属性的不安全引用是基于字符串的HQL构造的另一个问题。
随着域模型的不断变化,类型安全性在软件开发中带来了巨大的好处。域更改直接反映在查询中,而查询构造中的自动完成功能使查询构造更快,更安全。
用于Hibernate的 HQL是 QueryDSL 的第一个目标语言,如今 QueryDSL 支持JPA,JDO,JDBC,Lucene,Hibernate Search,MongoDB,Collections和 RDFBean 作为它的后端。
Dante Cloud 中已经集成了 QueryDSL,在使用时仅需要在代码中,注入 BlazeJPAQueryFactory
即可以编写 QueryDSL 代码。
QueryDSL 在 Dante Cloud 中实际应用的示例如下:
@Service
public class SysSocialBindingService extends BaseJpaService<SysSocialBinding, String> {
private final SysSocialBindingRepository sysSocialBindingRepository;
private final BlazeJPAQueryFactory blazeJPAQueryFactory;
public SysSocialBindingService(SysSocialBindingRepository sysSocialBindingRepository, BlazeJPAQueryFactory blazeJPAQueryFactory) {
this.sysSocialBindingRepository = sysSocialBindingRepository;
this.blazeJPAQueryFactory = blazeJPAQueryFactory;
}
@Override
public BaseJpaRepository<SysSocialBinding, String> getRepository() {
return this.sysSocialBindingRepository;
}
@Transactional
public List<AccessSource> findAllByUserId(String userId) {
QSysUser sysUser = QSysUser.sysUser;
QSysSocialUser sysSocialUser = QSysSocialUser.sysSocialUser;
QSysSocialBinding socialBinding = QSysSocialBinding.sysSocialBinding;
// 通过UserId查询到当前用户已经 Binding 的社交信息 子查询
SubQueryExpression<SysSocialUser> socialUserSubQuery = JPAExpressions.select(sysSocialUser)
.from(sysSocialUser)
.join(sysUser)
.on(sysSocialUser.users.any().userId.eq(sysUser.userId))
.where(sysUser.userId.eq(userId));
return blazeJPAQueryFactory
.select(
Projections.bean(AccessSource.class,
socialBinding.bindingId.as("id"),
socialBinding.bindingCode.as("source"),
socialBinding.bindingName.as("name"),
sysSocialUser.socialId,
sysSocialUser.updateTime.as("bindingTime"),
sysSocialUser.nickname.as("detail"),
sysSocialUser.avatar
)
)
.from(socialBinding)
.leftJoin(socialUserSubQuery, sysSocialUser)
.on(sysSocialUser.source.eq(socialBinding.bindingCode))
.fetch();
}
}