从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 ('test@example.com', '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 ('test@example.com', '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 和数据库之间进行转换。

相关推荐
神探阿航19 分钟前
基于 Spring Boot 和 Vue.js 的全栈购物平台开发实践
java·vue.js·spring boot·后端
ByteBlossom6661 小时前
Ruby语言的数据库交互
开发语言·后端·golang
web网站装修工2 小时前
你会选择java还是node做后台管理
java·前端·javascript·vue.js·后端·前端框架·node.js
begei2 小时前
SpringBoot教程(三十二) SpringBoot集成Skywalking链路跟踪
spring boot·后端·skywalking
Json____2 小时前
5. 使用springboot做一个音乐播放器软件项目【业务逻辑开发】
java·spring boot·后端·java项目·springboot项目·音乐播放器项目·万物oop
Cikiss2 小时前
图解Git——服务器上的Git《Pro Git》
服务器·git·后端·源代码管理
m0_548514773 小时前
Spring Boot 集成MyBatis-Plus
spring boot·后端·mybatis
计算机学姐3 小时前
基于微信小程序的健身房预约管理系统
java·vue.js·spring boot·后端·mysql·微信小程序·小程序
Quantum&Coder3 小时前
Objective-C语言的数据类型
开发语言·后端·golang
疯狂小小小码农4 小时前
C语言的数据库交互
开发语言·后端·golang