基于 Spring Data JPA 实现简单的分表功能

他们说,一个表的数据不要太多,太多就要分表,再多就要分库。

那就听他们的。然后为了分库分表,引入了一堆中间件,重不重?烦不烦?

为此,本文尝试提供一个小而美的分表实现,其基于 Spring Data JPA 模块,同时为了方便,还用到了 MapStructQueryDsl,后两项非必需。


说它小而美,是因为它只解决了一个最简单的场景:

我们卖了很多 IoT 设备,这些设备会不定时通过 MQTT 向服务端传送信息,服务端先是保存这些信息,再用定时任务每天分析这些数据。

一个简化后的信息,有一个自增的 id 字段、一个代表该设备用户的 userNo 字段和一个代表消息内容的 message 字段。

业务上对信息的使用很简单:

  1. 将信息存入表。
  2. 将信息表中的数据,按单个用户查出并分析,将分析结果存入其它的表。

所以,在没分表之前,这个信息表基本只会用到如下语句:

insert into message values (?);
select * from message where user_no = ?;

但随着用户的变多,每天要接收处理的数据要迅速增加,分表早晚要进行。

分表之后,用到的语句基本不变,但怎么将要执行的语句定位到给定的子表中,需要细细考虑与设计。


因为我们分析的主体是单个用户的信息,所以,我们根据 userNo 字段来决定数据落在哪张表中。

为了演示方便,假设我们只分2张表:message_01message_02。这两张表的结构完全一样。

在插入数据和查询数据时,我们对 userNo 的 hashcode 对 2 取余:

userNo.hashCode() % 2;
  • 当余数为 0 时,数据落在 message_01 表中;
  • 反之,余数为 1 时,数据落在 message_02 表中;

如果你要分更多的表,在上面基础上调整即可。

以上是理论阶段,接下来看看怎么用代码实现


因为用的是 Spring Data JPA 模块来做的,所以,我们需要两个实体类 —— Message01.javaMessage02.java

@Data
@Entity
@Table(name = "message_01")
public class Message01 extends Message {
}

@Data
@Entity
@Table(name = "message_02")
public class Message02 extends Message {
}

这两个实体类都是空类,主要是为了符合 Spring 对实体的要求,一张表要对应一个实体。实体真正的信息都在父类 Message.java 中:

@Data
@MappedSuperclass
public class Message {
    @Id
    @Column(columnDefinition = "bigint(20) COMMENT 'ID,自增'")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "user_no", columnDefinition = "varchar(32) COMMENT '用户编号'", nullable = false)
    String userNo;
    @Column(columnDefinition = "varchar(100) COMMENT '消息'", nullable = false)
    String message;
}

注意这里 @MappedSuperclass 注解的使用,表示子类将继承父类对表的字段的定义。

按 Spring 的规范,有了实体类,接下来是 Repository 接口类,以方便进行 CRUD 操作:

public interface Message01Repository extends JpaRepository<Message01Long{
}

public interface Message02Repository extends JpaRepository<Message02Long{
}

这两个类没什么可说的。。

下面是重要的实现阶段了。


对于一个分表的实现,最好设计成对使用者来说是透明的,即,使用者在使用时,只用操作 Message 这张”抽象的表”,而不用知道有 Message01Message02 的存在!

先看看针对新增和查询的消息服务 MessageService.java

public interface MessageService {
    /**
     * 新增一条消息
     *
     * @return 自增的id
     */

    Long addMessage(Message msg);

    /**
     * 查询出给定用户的所有消息
     */

    List<Message> findAll(String userNo);
}

我们再考虑怎么实现上述接口…

  1. 无论是新增还是查询,我们都需要用到具体的 Repository 接口,所以 MessageService 的实现类中要有注入 Message01RepositoryMessage02Repository 这两个 bean;
  2. 在新增时,我们要把 Message 入参转换为具体的子类(Message01Message02),再使用相应的 Repository 进行 save() 操作;
  3. 无论是新增还是查询,都要根据入参中的 userNo 信息定位到具体的 RepositoryMessage 子类。

有了以上分析,我们的 MessageService 的实现类大致如下:

@Service
public class MessageServiceImpl implements MessageService {

    @Autowired
    Message01Repository message01Repository;
    @Autowired
    Message02Repository message02Repository;

    /**
     * 用来将 {@link Message} 类转换为具体的子类
     */

    @Autowired
    MessageMapper messageMapper;

    /**
     * 子表到对应实体的映射
     */

    Map<Integer, Supplier<? extends Message>> entityMap;
    /**
     * 子表到对应 Repository 的映射
     */

    Map<Integer, JpaRepository<? extends Message, Long>> repositoryMap;

    @PostConstruct
    void init() {

        repositoryMap = new HashMap<>();
        repositoryMap.put(0, message01Repository);
        repositoryMap.put(1, message02Repository);

        entityMap = new HashMap<>();
        entityMap.put(0, Message01::new);
        entityMap.put(1, Message02::new);
    }

    /**
     * 定位用户的数据在哪一张表
     */

    int getTablePartitionFor(String userNo) {
        return userNo.hashCode() % 2;
    }


    /**
     * 定位用户所在具体实体类
     */

    Supplier<? extends Message> getEntityFor(String userNo) {
        return entityMap.get(getTablePartitionFor(userNo));
    }

    /**
     * 找出操作用户数据表的 Repository
     */

    JpaRepository<? extends Message, Long> getRepositoryFor(String userNo) {
        return repositoryMap.get(getTablePartitionFor(userNo));
    }


    @Override
    public Long addMessage(Message msg) {
        // 找到操作用户数据的 Repository
        JpaRepository repository = getRepositoryFor(msg.getUserNo());

        // 找到用户数据对应的实体
        Message entity = getEntityFor(msg.getUserNo()).get();

        // 将入参数据设置到刚才的实体中
        messageMapper.mapToTarget(msg, entity);

        // 保存实体
        repository.save(entity);

        // 返回自增id
        return entity.getId();
    }


    @Override
    public List<Message> findAll(String userNo) {
        // 找到操作用户数据的 Repository
        JpaRepository repository = getRepositoryFor(userNo);

        // 找到用户数据对应的实体
        Message entity = getEntityFor(userNo).get();
        entity.setUserNo(userNo);

        // 按 userNo 查询
        Example<Message> exam = Example.of(entity);

        // 返回查询结果
        return repository.findAll(exam);
    }

}

为了方便理解,上述代码加入了详细的解释。

另外,MessageMapper 作为把 Message 转换到具体子类的转换类,其代码如下:

@Mapper
public interface MessageMapper {

    Message mapToTarget(Message base, @MappingTarget Message target);
}

其具体的实现,由 MapStruct 自动生成。


Spring Data JPA 提供的查询不够强大,我一般会将其结合 QueryDsl 来使用,结合后,上面的 findAll() 方法还能这样实现:

@Service
public class MessageServiceImpl2 implements MessageService {

    @PersistenceContext
    private EntityManager em;

    /**
     * 子表到相应 EntityPath 的映射
     */

    Map<Integer, EntityPath<?>> entityPathMap;

    @PostConstruct
    void init() {

        entityPathMap = new HashMap<>();
        entityPathMap.put(0, QMessage01.message01);
        entityPathMap.put(1, QMessage02.message02);
    }

    int getTablePartitionFor(String userNo) {
        return userNo.hashCode() % 2;
    }

    EntityPath<?> getEntityPathFor(String userNo) {
        return entityPathMap.get(getTablePartitionFor(userNo));
    }


    public Long addMessage(Message msg) {
        // 忽略
        return null;
    }

    public List<Message> findAll(String userNo) {
        EntityPath<?> path = getEntityPathFor(userNo);
        JPAQuery<Message> query = new JPAQuery<Message>(em).from(path);
        QMessageBase q = new QMessageBase(path.getRoot().toString());

        // 添加更灵活的条件
        query.where(q.userNo.eq(userNo));

        return query.fetch();
    }
}

之所以提及这个,因为当查询条件除了 userNo 外还有其它字段时,用 QueryDsl 处理会更加得心应手。


哦了,你还有别的疑惑吗?

对了,下面是使用效果截图:

基于 Spring Data JPA 实现简单的分表功能
查询用户 B,在 message_01 表中
基于 Spring Data JPA 实现简单的分表功能
查询用户 A,在 message_02 表中
基于 Spring Data JPA 实现简单的分表功能
插入后的数据确实也分表存储了

– END –


原文始发于微信公众号(背井):基于 Spring Data JPA 实现简单的分表功能

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246716.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!