背景
最近一两周在做从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>>() {
};
}
}
- 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并不完全等价, 关于这个点如何改造, 需要结合场景或者业务逻辑来斟酌定夺.
- 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;
- 分页:mysql的分页使用的是: limit B(offset),A(count), 但是pgsql不支持这种语法, pgsql支持的是如下两种:
(1)、limit A offset B; (2)、OFFSET B ROWS FETCH NEXT A ROWS ONLY;
- pgsql查询区分大小写, 而mysql是不区分的
- 其它情况 (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字节整数decimal
或numeric
:任意精度的数值real
:4字节浮点数double precision
:8字节浮点数smallserial
:2字节序列整数serial
:4字节序列整数bigserial
:8字节序列整数
-
字符与字符串类型
character varying(n)
或varchar(n)
:变长字符串,最大长度为ncharacter(n)
或char(n)
:定长字符串,长度为ntext
:变长字符串,没有长度限制
-
日期/时间类型
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 和数据库之间进行转换。