如何用MyBatis TypeHandler应对表里的奇葩column

在关系型数据库的表设计中, 一般要遵循3个范式.第一范式就是:

单个字段不可分割

但是, 规则往往有人打破, 比如:

  • 在一个字段里存个JSON
  • 在一个字段里存CSV
  • 在一个字段里存多个布尔值

今天, 分享下如何巧妙运用 Mybatis TypeHandler, 优雅应对这个场景.

环境准备

数据表

建立这么一个表t, 字段c 存着一个JSON 字符串.

CREATE TABLE `t` (
  `id` int(11NOT NULL,
  `config` text COLLATE utf8mb4_0900_bin NOT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin;

config 字段里的内容, 序列化自如下的类:

@Data
public class Config {
    Boolean haveCat;
    Boolean haveDog;
    Boolean haveGirlFriend;
}

建立数据表映射类

如果不希望在业务层, 进行手动序列化和反序列化, 这里的类型不要用String, 而是写成强类型的Config.

@Data
public class T {
    Integer id;
    Config config;
    //String config 不使用 String
}

接下来, 就到了TypeHandler出场时刻了.

TypeHandler 简介

众所周知, Java 里的类型是int, long, String,等等等,而数据库里的类型则是JdbcType.NUMERIC, JdbcType.INTEGER等等.

  • 当从数据库里把数据查出来时候, MyBatis需要知道, 每一种数据库的类型应该如何转换为Java类型.
  • 当把Java对象写入数据库时候, MyBatis也需要知道, Java类型,如何变成JdbcType.

这里面的桥梁, 就是TypeHandler.

org.apache.ibatis.type包下面, mybatis 默认提供里几十种 TypeHandler的实现.但如果不够用的话, 我们可以自由的来实现该接口, 添加新实现.

使用mybatis-spring的话, 在配置里指定一下自定义TypeHandler的位置即可:

mybatis.type-handlers-package=sh.now.afk.sql.sqls.typehandler

自定义xml配置话,也有相应的配置区域. 也可以考虑是全局起作用, 还是局部起作用.

JSONTypeHandler

自己实现的 ConfigTypeHandler 如下

public class ConfigTypeHandler extends org.apache.ibatis.type.BaseTypeHandler<Config{
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Config parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public Config getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return JSON.parseObject(rs.getString(columnName), getRawType());
    }
}

这里偷懒用fastjson, 换任意JSON库均可.还有2个方法,已经省略.

如此这般, 在遇到 Config这样不认识的类时候, mybatis就知道如何去处理, 在数据库类型和Java类型之间做映射了.

CSV Type Handler

CSV 也就逗号分割值,俗称 Comma Separate Values.

  • 1,2,3 这样就是一个典型的csv
  • 1|2|3 这样属于自定义分隔符为(|)的广义CSV

我们也可以针对这种类型, 写一个typehandler, 比如这样.

public abstract class CSVTypeHandler<T extends List<E>, Eextends org.apache.ibatis.type.BaseTypeHandler<T{
    abstract protected E of(String s);

    abstract protected T ofList(List<E> s);

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter.stream().map(x -> String.valueOf(x)).collect(Collectors.joining(",")));
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return ofList(Arrays.stream(rs.getString(columnName).split(",")).map(this::of).toList());
    }
}

这次这个是泛型的, 需要用时候, 继承该类, 并实现好两个工厂方法即可.

比如我们甚至可以在值里存成a=1,b=2,c=3这种格式, 然后解析成List<Pair<String,Integer>, 只需要这么写实现类:

public class KVListCSVHandler extends CSVTypeHandler<List<Pair<String,Integer>>, Pair<String,Integer>>{
    @Override
    protected Pair<String, Integer> of(String s) {
        String[] arr = s.split("=");
        return Pair.of(arr[0], Integer.valueOf(arr[1]));
    }

    @Override
    protected List<Pair<String, Integer>> ofList(List<Pair<String, Integer>> s) {
        return s;
    }
}

前面的JSON类型, 也可以抽象出类似的抽象类, 就不再提了. 这个CSV的写法有问题, 不过因为我十分不推荐存CSV, 就不去改动他了, 仅作示意.

BIT TypeHandler

某些奇怪场景下, 会有往一个 COLUMN 里存入尽量多信息的要求. 比如一个int的32个bit, 要存入32个布尔值.

  • 第0bit表示有没有男朋友
  • 第1bit表示有没有女朋友
  • 第2bit表示有没有猫
  • 第3bit表示有没有狗
  • ….

这种情况下, 也可以建立一个Config类,然后使用 TypeHandler. 不过相比之下, 最重要的部分,是如何记录和组织某一个bit究竟是什么意思, 已经避免重复使用同一个位置bit表示不同意思.

我也没想到什么好方法, 只能暂时用反射,考虑组织成一个java+1个enum, 比如这样:

@Data
public class Config {
    Boolean haveCat;
    Boolean haveDog;
    Boolean haveGirlFriend;
    Boolean live;
    Boolean haveBoyFriend;
}

public enum ConfigEnum {
    man(0, getField("haveBoyFriend")),
    live(1, getField("live")),
    haveGirlFriend(2, getField("haveGirlFriend")),
    haveDog(3, getField("haveDog")),
    haveCat(4, getField("haveCat")),

    ;

    private final int bitPosition;
    private final Field field;

    ConfigEnum(int i, Field field) {
        this.bitPosition = i;
        this.field = field;
    }

    public static int agg(Config config) {
        int value = 0L;

        for (ConfigEnum configEnum : values()) {
            Object fieldValue = ReflectionUtils.getField(configEnum.field, config);
            if (fieldValue == nullcontinue;
            int flag = (Boolean) fieldValue ? 1 : 0;
            value |= flag << configEnum.bitPosition;
        }
        return value;
    }

    public static void write(Config target, int n) {
        for (ConfigEnum configEnum : values()) {
            int mask = 1 << configEnum.bitPosition;
            int l = (n & mask) >>> configEnum.bitPosition;
            ReflectionUtils.setField(configEnum.field, target, l == 1);
        }
    }

    private static Field getField(String name) {
        Field field = ReflectionUtils.findField(Config.classname);
        ReflectionUtils.makeAccessible(field);
        return field;
    }
}

这样在编写TypeHandler时候就比较简单了, 用Enum小工具即可.

public class IntegerToBitConfigTypeHander extends BaseTypeHandler<Config{
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Config parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, ConfigEnum.agg(parameter));
    }

    @Override
    public Config getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int n = rs.getInt(columnName);
        Config target = new Config();
        ConfigEnum.write(target,n);
        return target;
    }
}

当然, 也可以用更多其他的类型或者Map之类东西来操作, 依赖反射是件不好的事.

后话

遵守数据库设计的范式是好习惯.

今天的例子全是坏例子, 是在表已经被设计烂了的情况下, 如何去补救, 如何把不应该暴露给业务层的JSON,CSV,BIT解析给隐藏起来. 这些都是存储方式的具体细节, 不应该侵入业务. 当然, 写到数据库框架里到底对不对, 也是很有待商榷的事.

额外补充

前面JSON什么的, 其实还能忍.后面的, 就非常没必要了. 因为MySQL提供了一个原生的数据类型SET, 他天生就是CSV+BIT化的, 最多占用8个字节,存放64个元素是否存在的信息.

比如按照下面语句建表

CREATE TABLE `t` (
  `id` int(11NOT NULL,
  `config` set('haveGirlFriend','haveBoyFriend','haveDog','haveCat'CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_bin;

那这里的存储,每个串是否存在,就只占用了一个bit, 但是读写起来却更易懂.每个bit代表哪个值, 也是维护在表定义中, 而非暴露在Java里.自己搞CSV或者BIT,属实属于MySQL不熟,造方轮子了.

id config
1 haveDog,haveCat
insert into t(`id``config`VALUES(1, ('haveDog,haveCat'));
select * from t;


如何用MyBatis TypeHandler应对表里的奇葩column


原文始发于微信公众号(K字的研究):如何用MyBatis TypeHandler应对表里的奇葩column

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

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

(0)
小半的头像小半

相关推荐

发表回复

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