前言

JPAJava Persistence API的简称,中文名为Java持久层API,是JDK5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
Sun引入新的JPA ORM规范出于两个原因: 其一, 简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下 归一。
JPA包括以下3方面的内容:

  1. 一套API标准。在javax.persistence的包下面,用来操作 实体对象,执行CRUD操作,框架在后台替代我们完成所有的事情,开 发者从烦琐的JDBC和SQL代码中解脱出来。
  2. 面向对象的查询语言:Java Persistence Query Language(JPQL)。这是持久化操作中很重要的一个方面,通过面向 对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密 耦合。
  3. ORM(object/relational metadata)元数据的映射。JPA 支持XML和JDK5.0注解两种元数据的形式,元数据描述对象和表之间 的映射关系,框架据此将实体对象持久化到数据库表中。

JPA的宗旨是为POJO提供持久化标准规范,由此可见,经过这几 年的实践探索,能够脱离容器独立运行,方便开发和测试的理念已经 深入人心了。Hibernate 3.2+、TopLink 10.1.3以及OpenJPA都提供 了JPA的实现,以及最后的Spring的整合Spring Data JPA。目前互联 网公司和传统公司大量使用了JPA的开发标准规范,如下图所示。
Thingsboard源码分析-Spring Data JPA - 图1

对象实体映射

JPA(Java持久性API)是存储业务实体关联的实体来源。它显示了如何定义一个面向普通Java对象(POJO)作为一个实体,以及如何与管理关系实体提供一套标准。因此,下面的有些注解还是必须要去了解的,以便于更好的提高工作效率。
Thingsboard相关对象实体在org.thingsboard.server.dao.model.sql目录下:
Thingsboard源码分析-Spring Data JPA - 图2
我们先看下Device(设备)相关对象实体存储的定义和实现。
Thingsboard源码分析-Spring Data JPA - 图3
DeviceEntity实体类,代码如下:

  1. @Data
  2. @EqualsAndHashCode(callSuper = true)
  3. @Entity
  4. @TypeDef(name = "json", typeClass = JsonStringType.class)
  5. @Table(name = ModelConstants.DEVICE_COLUMN_FAMILY_NAME)
  6. public final class DeviceEntity extends AbstractDeviceEntity<Device> {
  7. public DeviceEntity() {
  8. super();
  9. }
  10. public DeviceEntity(Device device) {
  11. super(device);
  12. }
  13. @Override
  14. public Device toData() {
  15. return super.toDevice();
  16. }
  17. }Copy to clipboardErrorCopied
  1. @Entity定义对象将会成为被JPA管理的实体,将映射到指定的数据库表。
  2. @Table指定数据库的表名。name: 表的名字,可选。如果不填写,系统认为实体的名字为表名。
  3. @TypeDef 允许您将Java对象或Jackson JsonNode为JPA实体属性。

AbstractDeviceEntity实体类,代码如下:

  1. @Data
  2. @EqualsAndHashCode(callSuper = true)
  3. @TypeDefs({
  4. @TypeDef(name = "json", typeClass = JsonStringType.class),
  5. @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
  6. })
  7. @MappedSuperclass
  8. public abstract class AbstractDeviceEntity<T extends Device> extends BaseSqlEntity<T> implements SearchTextEntity<T> {
  9. @Column(name = ModelConstants.DEVICE_TENANT_ID_PROPERTY, columnDefinition = "uuid")
  10. private UUID tenantId;
  11. @Column(name = ModelConstants.DEVICE_CUSTOMER_ID_PROPERTY, columnDefinition = "uuid")
  12. private UUID customerId;
  13. @Column(name = ModelConstants.DEVICE_TYPE_PROPERTY)
  14. private String type;
  15. @Column(name = ModelConstants.DEVICE_NAME_PROPERTY)
  16. private String name;
  17. @Column(name = ModelConstants.DEVICE_LABEL_PROPERTY)
  18. private String label;
  19. @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
  20. private String searchText;
  21. @Type(type = "json")
  22. @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
  23. private JsonNode additionalInfo;
  24. @Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_ID_PROPERTY, columnDefinition = "uuid")
  25. private UUID deviceProfileId;
  26. @Type(type = "jsonb")
  27. @Column(name = ModelConstants.DEVICE_DEVICE_DATA_PROPERTY, columnDefinition = "jsonb")
  28. private JsonNode deviceData;
  29. ......
  30. }Copy to clipboardErrorCopied
  1. @MappedSuperclass基于代码复用和模型分离的思想,在项目开发中使用JPA的@MappedSuperclass注解将实体类的多个属性分别封装到不同的非实体类中。例如,数据库表中都需要id来表示编号,id是这些映射实体类的通用的属性,交给jpa统一生成主键id编号,那么使用一个父类来封装这些通用属性,并用@MappedSuperclas标识。注意:
    • 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
    • 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。
  2. @Column 定义该属性对应数据库中的列名,
    • name: 数据库中表的列名。可选,如果不填写认为字段名和实体属性名一样。
    • unique : 是否唯一,默认false,可选
    • nullable: 数据字段是否允许空。可选,默认true
    • insertable: 执行insert操作的时候是否包含此字段,默认true,可选
    • updateabe:执行update的时候是否包含此字段,默认true,可选

BaseSqlEntity类,代码如下:

  1. @Data
  2. @MappedSuperclass
  3. public abstract class BaseSqlEntity<D> implements BaseEntity<D> {
  4. @Id
  5. @Column(name = ModelConstants.ID_PROPERTY, columnDefinition = "uuid")
  6. protected UUID id;
  7. @Column(name = ModelConstants.CREATED_TIME_PROPERTY)
  8. protected long createdTime;
  9. @Override
  10. public UUID getUuid() {
  11. return id;
  12. }
  13. @Override
  14. public void setUuid(UUID id) {
  15. this.id = id;
  16. }
  17. @Override
  18. public long getCreatedTime() {
  19. return createdTime;
  20. }
  21. public void setCreatedTime(long createdTime) {
  22. if (createdTime > 0) {
  23. this.createdTime = createdTime;
  24. }
  25. }
  26. }Copy to clipboardErrorCopied

@Id: 定义属性为数据库的主键,一个实体里面必须有一个。
在这里,我在这里再提供几个Thingsboard中使用到的注解。
@Enumerated:这个很好用,直接映射enum枚举类型的字段。
@Transient表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性。
其他的表结构和对象实体映射都是差不多的,大家自行阅读就可以了啊!

基础查询方法

Thingsboard源码分析-Spring Data JPA - 图4
Repository的源码如下:
Thingsboard源码分析-Spring Data JPA - 图5
上面的类定义了所有Repository操作的实体和ID两个泛型参数。我们不需要继承任何接口,只要继承这个接口,就可以使用Spring Data JPA里面提供的很多约定的方法查询和注解查询。
Thingsboard数据库查询方法在org.thingsboard.server.dao.sql目录中,如下图:
Thingsboard源码分析-Spring Data JPA - 图6
DeviceCredentials(设备认证)数据库查询方法,如下:

  1. public interface DeviceCredentialsRepository extends CrudRepository<DeviceCredentialsEntity, UUID> {
  2. DeviceCredentialsEntity findByDeviceId(UUID deviceId);
  3. DeviceCredentialsEntity findByCredentialsId(String credentialsId);
  4. }Copy to clipboardErrorCopied

CrudRepository提供了公共的通用的CRUD方法。
Thingsboard源码分析-Spring Data JPA - 图7
Spring JPA Repository的实现原理是采用动态代理的机制,所以我们介绍两种定义查询方法:

  1. 从方法名称中可以指定特定用于存储的查询和更新;
  2. 通过使用@Query手动定义的查询,这个取决于实际存储操作。

内部基础架构中有个根据方法名的查询生成器机制,对于在存储 库的实体上构建约束查询很有用。该机制方法的前缀有find…By、 read…By、query…By、count…By和get…By,从这些方法可以分析 它的其余部分(实体里面的字段)。
引入子句可以包含其他表达式, 例如在Distinct要创建的查询上设置不同的标志。然而,第一个By作 为分隔符来指示实际标准的开始。在一个非常基本的水平上,你可以定义实体性条件,并与它们串联(And和Or)。
用一句话概括,待查询功能的方法名由查询策略(关键字)、查 询字段和一些限制性条件组成。
方法的查询策略的属性表达式
属性表达式(Property Expressions)只能引用托管(泛化)实 体的直接属性,在上面那个例子中:
DeviceCredentialsEntity findByDeviceId(UUID deviceId);
将使用DeviceId作为查询参数,并执行相应的SQL语句。
Device(设备)数据库查询方法,如下:

  1. public interface DeviceRepository extends PagingAndSortingRepository<DeviceEntity, UUID> {
  2. @Query("SELECT new org.thingsboard.server.dao.model.sql.DeviceInfoEntity(d, c.title, c.additionalInfo) " +
  3. "FROM DeviceEntity d " +
  4. "LEFT JOIN CustomerEntity c on c.id = d.customerId " +
  5. "WHERE d.id = :deviceId")
  6. DeviceInfoEntity findDeviceInfoById(@Param("deviceId") UUID deviceId);
  7. ......
  8. @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId")
  9. Page<DeviceEntity> findByTenantId(@Param("tenantId") UUID tenantId,
  10. Pageable pageable);
  11. @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
  12. "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
  13. Page<DeviceEntity> findByTenantId(@Param("tenantId") UUID tenantId,
  14. @Param("textSearch") String textSearch,
  15. Pageable pageable);
  16. ......
  17. @Query("SELECT DISTINCT d.type FROM DeviceEntity d WHERE d.tenantId = :tenantId")
  18. List<String> findTenantDeviceTypes(@Param("tenantId") UUID tenantId);
  19. DeviceEntity findByTenantIdAndName(UUID tenantId, String name);
  20. List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List<UUID> deviceIds);
  21. List<DeviceEntity> findDevicesByTenantIdAndIdIn(UUID tenantId, List<UUID> deviceIds);
  22. DeviceEntity findByTenantIdAndId(UUID tenantId, UUID id);
  23. }Copy to clipboardErrorCopied

PagingAndSortingRepository继承CrudRepository所有的基本方法,它增加了分页和排序等对查询结果进行限制的基本的、常用的、通用的一些分页方法。
我们在方法中将org.springframework.data.domain.Pageable实例传递给查询方法,以便动态地将分页添加到静态定义的查询中。 Page知道可用的元素和页面的总数。
注解式查询方法
使用命名查询为实体声明查询是一种有效的方法,对于少量查询 很有效。一般只需要关心@Query里面的value和nativeQuery的值。使 用声明式JPQL查询有一个好处,就是启动的时候就知道语法正确与 否。
@Query分页

  1. @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId")
  2. Page<DeviceEntity> findByTenantId(@Param("tenantId") UUID tenantId,
  3. Pageable pageable);Copy to clipboardErrorCopied

直接用Page对象接收接口,参数直接用Pageable的 实现类即可。

整体持久化逻辑业务类流向

Thingsboard源码分析-Spring Data JPA - 图8
以上就是关于Thingsboard的数据持久化业务逻辑流向,我在上面的文档中,用的是设备表,其他Thingsboard的表结构和逻辑操作都是类似的。请大家仔细阅读和理解。