从MySQL迁移到PostgreSQL经验总结

背景

最近一两周在做从MySQL迁移到PostgreSQL的任务(新项目,历史包袱较小,所以迁移比较顺利), 感觉还是有一些知识,可以拿出来分享,希望对大家有所帮助。

为什么要转到PostgreSQL

因架构团队安全组安全需求,需要将Mysql迁移到PostgreSQL。实际迁移下来,发现PostgreSQL挺优秀的,比MySQL严谨很多,很不错。

迁移经验

引入PostgreSQL驱动,调整链接字符串
pagehelper方言调整
涉及order, group,name, status, type 等关键字,要用引号括起来
JSON字段及JsonTypeHandler

项目中用到了比较多的JSON字段。在mysql中,也有JSON字段类型,但是有时候我们用了varchar或text,在mybatis typehandler中是当成字符来处理的。但是在postgresql中,相对严谨,如果字段类型是json,那么在java中会被封装为PGObject,所以我们原来的JsonTypeHandler就要被改造。

typescript 复制代码
/**
 * JSON类型处理器
 *
 * @author james.h.fu
 * @create 2024/10/9 20:45
 */
@Slf4j
public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
    private static final ObjectMapper mapper = new ObjectMapper();

    static {
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, Boolean.FALSE);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    private final Class<T> clazz;
    private TypeReference<? extends T> typeReference;

    public JsonTypeHandler(Class<T> clazz) {
        if (clazz == null) throw new IllegalArgumentException("Type argument cannot be null");
        this.clazz = clazz;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        setObject(ps, i, parameter);
    }

    @Override
    public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return toObject(rs, columnName);
    }

    @Override
    public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return toObject(rs, columnIndex);
    }

    @Override
    public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return toObject(cs, columnIndex);
    }

    protected TypeReference<? extends T> getTypeReference() {
        return new TypeReference<T>() {};
    }

    private String toJson(T object) {
        try {
            return mapper.writeValueAsString(object);
        } catch (Exception ex) {
            log.error("JsonTypeHandler error on toJson content:{}", JsonUtil.toJson(object), ex);
            throw new RuntimeException("JsonTypeHandler error on toJson", ex);
        }
    }

    private T toObject(String content) {
        if (!StringUtils.hasText(content)) {
            return null;
        }
        try {
            if (clazz.getName().equals("java.util.List")) {
                if (Objects.isNull(typeReference)) {
                    typeReference = getTypeReference();
                }
                return (T) mapper.readValue(content, typeReference);
            }
            return mapper.readValue(content, clazz);
        } catch (Exception ex) {
            log.error("JsonTypeHandler error on toObject content:{},class:{}", content, clazz.getName(), ex);
            throw new RuntimeException("JsonTypeHandler error on toObject", ex);
        }
    }

//    protected boolean isPostgre() {
//        SqlSessionFactory sqlSessionFactory = SpringUtil.getBean(SqlSessionFactory.class);
//        Configuration conf = sqlSessionFactory.getConfiguration();
//        DataSource dataSource = conf.getEnvironment().getDataSource();
//        try (Connection connection = dataSource.getConnection()) {
//            String url = connection.getMetaData().getURL();
//            return url.contains("postgresql");
//        } catch (SQLException e) {
//            throw new RuntimeException("Failed to determine database type", e);
//        }
//    }

    @SneakyThrows
    private void setObject(PreparedStatement ps, int i, T parameter) {
        PGobject jsonObject = new PGobject();
        jsonObject.setType("json");
        jsonObject.setValue(JsonUtil.toJson(parameter));
        ps.setObject(i, jsonObject);
    }

    @SneakyThrows
    private T toObject(ResultSet rs, String columnName) {
        Object object = rs.getObject(columnName);
        return toObject(object);
    }

    @SneakyThrows
    private T toObject(ResultSet rs, int columnIndex) {
        Object object = rs.getObject(columnIndex);
        return toObject(object);
    }

    @SneakyThrows
    private T toObject(CallableStatement rs, int columnIndex) {
        Object object = rs.getObject(columnIndex);
        return toObject(object);
    }

    public T toObject(Object object) {
        if (object instanceof String json) {
            return this.toObject(json);
        }
        if (object instanceof PGobject pgObject) {
            String json = pgObject.getValue();
            return this.toObject(json);
        }

        return null;
    }
}

<resultMap>
    <result column="router_info" jdbcType="OTHER" property="routerInfo" typeHandler="***.cms.cmslib.mybatis.JsonTypeHandler"/>
</resultMap>

<set>
<if test="routerInfo != null">
	router_info = #{routerInfo,typeHandler=***.cms.cmslib.mybatis.JsonTypeHandler}
  </if>
</set>
where id = #{id}

如果JSON中存储是的List<T>, Map<String, Object>,Set<T>等类型时, 会存在泛型类型中类型擦除的问题。因此,如果存在这种情况,我们需要扩展子类,在子类中提供详细的类型信息TypeReference

scala 复制代码
/**
 * @author james.h.fu
 * @create 2024/12/9 20:45
 */
public class ComponentUpdateListJsonTypeHandler extends JsonTypeHandler<List<ComponentUpdate>> {
    public ComponentUpdateListJsonTypeHandler(Class<List<ComponentUpdate>> clazz) {
        super(clazz);
    }

    @Override
    protected TypeReference getTypeReference() {
        return new TypeReference<List<ComponentUpdate>>() {
        };
    }
}
  1. pgsql不支持mysql insert ignore语法, pgsql提供了类似的语法:
vbnet 复制代码
INSERT INTO orders (product_id, user_id)

VALUES (101, 202)

ON CONFLICT (product_id, user_id) DO NOTHING;

但是与mysql insert ignore并不完全等价, 关于这个点如何改造, 需要结合场景或者业务逻辑来斟酌定夺.

  1. pgsql也不支持INSERT ... ON DUPLICATE KEY UPDATE, 如果代码中有使用这个语法, pgsql提供了类似的语法:
sql 复制代码
INSERT INTO users (email, name, age)
VALUES ('[email protected]', 'John', 30)
ON CONFLICT (email)
DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age;

EXCLUDED 是一个特殊的表别名,用于引用因冲突而被排除(Excluded)的、尝试插入的那条数据.
CONFLICT也可以直接面向唯一性约束. 假如users有一个唯一性约束: unique_email_constraint, 上述SQL可以改成:

sql 复制代码
INSERT INTO users (email, name, age)
VALUES ('[email protected]', 'John', 30)
ON CONFLICT ON CONSTRAINT unique_email_constraint
DO UPDATE SET
name = EXCLUDED.name,
age = EXCLUDED.age;
  1. 分页:mysql的分页使用的是: limit B(offset),A(count), 但是pgsql不支持这种语法, pgsql支持的是如下两种:

(1)、limit A offset B; (2)、OFFSET B ROWS FETCH NEXT A ROWS ONLY;

  1. pgsql查询区分大小写, 而mysql是不区分的
  2. 其它情况 (1)、代码中存在取1个数据的场景,原来mysql写法是limit 0,1, 要调整为limit 1; (2)、在mysql中BIT(1)或tinyint(值0,1)可以转换为Boolean。但是在pgsql中不支持。需要明确使用boolean类型或INT类型, 或者使用typerhandler处理。
sql 复制代码
ALTER TABLE layout 
ALTER COLUMN init_instance TYPE INT2
USING CASE 
    WHEN init_instance = B'1' THEN 1
    WHEN init_instance = B'0' THEN 0
    ELSE NULL
END;

update component c
set init_instance = cp.init_instance 
from  component_publish cp 
where c.init_instance is null and c.id = cp.component_id ;

(3)、迁移数据后,统一将自增列修改

vbnet 复制代码
DO $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN 
        SELECT 
            tc.sequencename
        FROM pg_sequences tc
    LOOP
        EXECUTE format('ALTER SEQUENCE %I RESTART WITH 100000', rec.sequencename);
        RAISE NOTICE 'Reset sequence % to 100000', rec.sequencename;
    END LOOP;
END $$;

总结

在日常开发中,我们一定要再严谨一些,规范编码。这样能让写我的代码质量更好,可移植性更高。

附录

在PostgreSQL 中,有哪些数据类型?

PostgreSQL 支持多种数据类型,下面列出一些常用的数据类型:

  • 数值类型

    • smallint:2字节整数
    • integer:4字节整数
    • bigint:8字节整数
    • decimalnumeric:任意精度的数值
    • real:4字节浮点数
    • double precision:8字节浮点数
    • smallserial:2字节序列整数
    • serial:4字节序列整数
    • bigserial:8字节序列整数
  • 字符与字符串类型

    • character varying(n)varchar(n):变长字符串,最大长度为n
    • character(n)char(n):定长字符串,长度为n
    • text:变长字符串,没有长度限制
  • 日期/时间类型

    • date:存储日期(年月日)
    • time [ (p) ] [ without time zone ]:存储时间(时分秒),可指定精度p,默认不带时区
    • time [ (p) ] with time zone:存储时间(时分秒),可指定精度p,带时区
    • timestamp [ (p) ] [ without time zone ]:存储日期和时间,默认不带时区
    • timestamp [ (p) ] with time zone:存储日期和时间,带时区
    • interval:存储时间间隔
  • 布尔类型

    • boolean:存储真或假值
  • 二进制数据类型

    • bytea:存储二进制字符串
  • 几何类型

    • point:二维坐标点
    • line:无限长直线
    • lseg:线段
    • box:矩形框
    • path:闭合路径或多边形
    • polygon:多边形
    • circle:圆
  • 网络地址类型

    • cidr:存储IPv4或IPv6网络地址
    • inet:存储IPv4或IPv6主机地址和可选的CIDR掩码
    • macaddr:存储MAC地址
  • 枚举类型

    • enum:用户定义的一组排序标签
  • 位串类型

    • bit( [n] ):固定长度位串
    • bit varying( [n] ):变长位串
  • JSON类型

    • json:存储JSON数据
    • jsonb:存储JSON数据,以二进制形式存储,并支持查询操作
  • UUID类型

    • uuid:存储通用唯一标识符
  • XML类型

    • xml:存储XML数据

这些数据类型可以满足大多数应用的需求。在创建表时,根据实际需要选择合适的数据类型是非常重要的。

在MyBatis中,jdbcType有哪些?

jdbcType 是 MyBatis 和其他 JDBC 相关框架中用于指定 Java 类型和 SQL 类型之间映射的属性。以下是常见的 jdbcType 值及其对应的 SQL 数据类型:

  • NULL:表示 SQL NULL 类型
  • VARCHAR:表示 SQL VARCHAR 或 VARCHAR2 类型
  • CHAR:表示 SQL CHAR 类型
  • NUMERIC:表示 SQL NUMERIC 类型
  • DECIMAL:表示 SQL DECIMAL 类型
  • BIT:表示 SQL BIT 类型
  • TINYINT:表示 SQL TINYINT 类型
  • SMALLINT:表示 SQL SMALLINT 类型
  • INTEGER:表示 SQL INTEGER 类型
  • BIGINT:表示 SQL BIGINT 类型
  • REAL:表示 SQL REAL 类型
  • FLOAT:表示 SQL FLOAT 类型
  • DOUBLE:表示 SQL DOUBLE 类型
  • DATE:表示 SQL DATE 类型(只包含日期部分)
  • TIME:表示 SQL TIME 类型(只包含时间部分)
  • TIMESTAMP:表示 SQL TIMESTAMP 类型(包含日期和时间部分)
  • BLOB:表示 SQL BLOB 类型(二进制大对象)
  • CLOB:表示 SQL CLOB 类型(字符大对象)
  • ARRAY:表示 SQL ARRAY 类型
  • DISTINCT:表示 SQL DISTINCT 类型
  • STRUCT:表示 SQL STRUCT 类型
  • REF:表示 SQL REF 类型
  • DATALINK:表示 SQL DATALINK 类型
  • BOOLEAN:表示 SQL BOOLEAN 类型
  • ROWID:表示 SQL ROWID 类型
  • LONGNVARCHAR:表示 SQL LONGNVARCHAR 类型
  • NVARCHAR:表示 SQL NVARCHAR 类型
  • NCHAR:表示 SQL NCHAR 类型
  • NCLOB:表示 SQL NCLOB 类型
  • SQLXML:表示 SQL XML 类型
  • JAVA_OBJECT:表示 SQL JAVA_OBJECT 类型
  • OTHER:表示 SQL OTHER 类型
  • LONGVARBINARY:表示 SQL LONGVARBINARY 类型
  • VARBINARY:表示 SQL VARBINARY 类型
  • LONGVARCHAR:表示 SQL LONGVARCHAR 类型

在使用 MyBatis 或其他 JDBC 框架时,选择合适的 jdbcType 可以确保数据正确地在 Java 和数据库之间进行转换。

相关推荐
Moonbit几秒前
MoonBit 成都之行圆满收官,见证基础软件新生力量
后端
CodeFox5 分钟前
线上 nacos 挂了 !cp 模式下,naming server down 掉问题深度解析!
java·后端·架构
brzhang6 分钟前
流量大了就加机器?太 Low 了!负载均衡的这些高级玩法,让你部署、测试、安全一步到位!
前端·后端·架构
我是哪吒7 分钟前
分布式微服务系统架构第122集:NestJS是一个用于构建高效、可扩展的服务器端应用程序的开发框架
前端·后端·面试
VvUppppp8 分钟前
动态代理与反射
java·后端
JustLorain12 分钟前
如何实现事务的可串行化快照隔离
数据库·后端·架构
一个热爱生活的普通人13 分钟前
如何用go语言实现类似AOP的功能
后端·面试·go
lqstyle13 分钟前
Redis地理相亲事务所:Geo/Bitmap/HLL底层脱单算法全解
后端·面试
GetcharZp14 分钟前
用 Go 语言也能优雅输出表格!一招学会 tablewriter
后端·go
执念36514 分钟前
Java多线程(每天两道面试题)
后端·面试