内容纲要

背景

在上一篇关于多态反序列化的文章中,我们讨论了如何在复杂业务场景下使用 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结构的原因如下:

  1. 灵活性:JSON 结构可以用一个字段包含多种业务信息。
  2. 性能影响很小:虽然 JSON 列不支持直接索引,但在大部分业务场景中,并没有基于 JSON 字段进行查询的需求,因此对查询性能的影响较小。
  3. 查询支持: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 查看完整实现。

最后修改日期: 2025年2月16日

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。