背景
在上一篇关于多态反序列化的文章中,我们讨论了如何在复杂业务场景下使用 Jackson 处理前端请求体。然而,复杂业务场景不仅影响前端请求体的设计,数据库表的设计同样面临挑战。例如,在一个内容管理系统中,文章的阅读权限可能涉及多种条件,如用户等级、付费状态、日期区间等。这些条件可能对应不同的字段和逻辑,如果为每种条件设计单独的表,会导致表结构冗余且难以维护。
当然,也可以使用一个宽表来记录所有数据,但随着业务场景的不断扩展,该表可能会变得异常复杂,且每次业务扩展都需要新增字段,增加了维护成本。
为了解决上述问题,我们可以采用 单表 + JSON 字段 的设计方案。通过将不同业务场景的字段存储在 JSON 格式的列中,既避免了表结构的频繁变更,又保持了数据的灵活性。
技术实现
简介
JPA(Java Persistence API)提供了 单表继承策略,通过 @DiscriminatorColumn
和 @DiscriminatorValue
注解,可以在同一张表中存储多种类型的实体,并通过一个区分字段(如 type
)来标识具体的实体类型。
这种设计不仅减少了表的数量,还提高了查询效率,同时保持了代码的灵活性和可维护性。
数据表设计
根据上述的表设计我们完善创建表的sql:
create table read_permission
(
id bigint auto_increment
primary key,
article_id bigint not null,
type varchar(32) not null,
permission json not null,
created_time datetime default CURRENT_TIMESTAMP not null
);
在这张表中,permission
字段是 JSON 格式,用于存储不同业务场景下的阅读权限详情。我们可以插入几条测试数据,如下:
使用json结构的原因如下:
- 灵活性:JSON 结构可以用一个字段包含多种业务信息。
- 性能影响很小:虽然 JSON 列不支持直接索引,但在大部分业务场景中,并没有基于 JSON 字段进行查询的需求,因此对查询性能的影响较小。
- 查询支持:JSON 也支持根据其中某些字段进行查询。
具体实现
确定表继承策略
JPA 支持三种继承策略:
- 单表继承(SINGLE_TABLE):所有子类共用一张表,通过区分字段标识具体类型。
- Joined 表继承(JOINED:每个子类对应一张表,通过外键关联父类表。
- 表 per 类继承(TABLE_PER_CLASS):每个子类对应一张独立的表。
根据上述的表设计,我们使用 单表继承策略,它最适合处理字段差异较小的多态场景。
父类定义
@Entity
@Table(name = "read_permission")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
public abstract class ReadPermissionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Integer articleId;
@Enumerated(value = EnumType.STRING)
@Column(name = "type", insertable = false, updatable = false)
private ConditionType type;
private LocalDateTime createdTime;
}
子类定义
@Entity
@DiscriminatorValue("MEMBER_LEVEL")
@TypeDef(name = "json", typeClass = JsonType.class)
public class MemberLevelPermissionEntity extends ReadPermissionEntity {
@Type(type = "json")
@Column(name = "permission", columnDefinition = "json")
private MemberLevel permission;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public static class MemberLevel {
private Integer minLevel;
private Integer maxLevel;
}
}
@Entity
@DiscriminatorValue("PAID_MEMBER")
@TypeDef(name = "json", typeClass = JsonType.class)
public class PaidMemberPermissionEntity extends ReadPermissionEntity {
@Type(type = "json")
@Column(name = "permission", columnDefinition = "json")
private PaidMember permission;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public static class PaidMember {
private Boolean isPaidMember;
}
}
@Entity
@DiscriminatorValue("DATE_RANGE")
@TypeDef(name = "json", typeClass = JsonType.class)
public class DateRangePermissionEntity extends ReadPermissionEntity {
@Type(type = "json")
@Column(name = "permission", columnDefinition = "json")
private DateRange permission;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public static class DateRange {
@DateTimeFormat(pattern = "YYYY-MM-dd")
private LocalDate startDate;
@DateTimeFormat(pattern = "YYYY-MM-dd")
private LocalDate endDate;
}
}
@Entity
@DiscriminatorValue("POINTS")
@TypeDef(name = "json", typeClass = JsonType.class)
public class PointsPermissionEntity extends ReadPermissionEntity {
@Type(type = "json")
@Column(name = "permission", columnDefinition = "json")
private Points permission;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public static class Points {
private int points;
}
}
由于数据表中的 permission
字段是 JSON 格式的,我们使用了 Hibernate 的 @Type
注解和 hibernate-types-52
库来处理。@Type
和 @TypeDef
注解所需的依赖如下:
implementation 'org.hibernate:hibernate-core'
implementation 'com.vladmihalcea:hibernate-types-52:2.14.0'
使用jpa查询数据
@Repository
public interface ReadPermissionJpaRepository extends JpaRepository<ReadPermissionEntity, Integer> {
}
测试
@SpringBootTest
class ReadPermissionMapperTest {
@Autowired
private ReadPermissionJpaRepository repository;
@Test
void should_query_all_type_permission_from_database() {
List<ReadPermissionEntity> entities = repository.findAll();
MemberLevelPermissionEntity memberLevelPermissionEntity = (MemberLevelPermissionEntity) entities.get(0);
PaidMemberPermissionEntity paidMemberPermissionEntity = (PaidMemberPermissionEntity) entities.get(1);
PointsPermissionEntity pointsPermissionEntity = (PointsPermissionEntity) entities.get(2);
DateRangePermissionEntity dateRangePermissionEntity = (DateRangePermissionEntity) entities.get(3);
assertEquals(3, memberLevelPermissionEntity.getPermission().getMinLevel());
assertEquals(10, memberLevelPermissionEntity.getPermission().getMaxLevel());
assertTrue(paidMemberPermissionEntity.getPermission().getIsPaidMember());
assertNotNull(dateRangePermissionEntity.getPermission().getStartDate());
assertNull(dateRangePermissionEntity.getPermission().getEndDate());
assertEquals(10, pointsPermissionEntity.getPermission().getPoints());
}
}
当使用上述测试数据后,该测试可以成功证明表中的数据成功实例化为子类,并且 JSON 类型的字段信息也成功映射。
总结
通过 JPA 的 @DiscriminatorColumn
和 @DiscriminatorValue
注解,我们可以优雅地处理数据库表的多态映射问题。单表继承策略不仅减少了表的数量,还提高了查询效率,同时保持了代码的灵活性和可维护性。
针对这篇文章和上一篇关于 JSON 多态反序列化的代码,可以访问我的 GitHub 查看完整实现。
留言