ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot整合MyBatis与PostgreSQL实战:避坑指南与核心配置

Spring Boot整合MyBatis与PostgreSQL实战:避坑指南与核心配置 最近重构一个内部资产管理系统时我把技术栈从 MySQL JPA 换成了 PostgreSQL MyBatis。这套组合看起来不算新但真正落地的时候坑比想象中多驱动版本、JSONB 映射、动态 SQL、批量插入、参数大小写、时区问题每一个都能卡你半天。这篇博文不是官方文档复读而是把我这一路踩过的坑、权衡过的设计、最终能跑通的代码都整理出来完全围绕 Spring Boot 整合 MyBatis 与 PostgreSQL 这条主线展开从装库到复杂查询逐层过一遍。这篇文章适合下面几类人看正在把老项目从 MySQL 迁到 PostgreSQL 的开发者刚接手 Spring Boot 3 MyBatis 组合、想避开常见配置坑的新手以及准备 MyBatis 面试想顺带把缓存、TypeHandler 工作流程搞明白的人。内容里所有 SQL 都基于 PostgreSQL 16 验证过Spring Boot 版本以 3.2.x 为主但文末问题排查部分也会提到 Boot 2.x 的差异。1. 为什么是 Spring Boot MyBatis PostgreSQL先说结论这套组合最适合“SQL 需要精细控制、数据表结构复杂、业务查询里既有 CRUD 又有一堆统计报表”的项目。如果你只是做一个简单的 CRUD Demo用 JPA 更省事但如果数据模型有一百多张表、查询条件经常变、DBA 还要求 SQL 能手工调优MyBatis 的 XML 映射会给你最大的掌控力。1.1 三个组件各自解决什么问题Spring Boot 在这套组合里负责“组装”。自动配置把数据源、事务、Mapper 扫描、日志这些基础设施全部接好你不需要写 ApplicationContext 的手动装配代码。尤其到了 Spring Boot 3注解驱动和自动配置的边界比 Boot 2 更清晰一个 starter 就能把 MyBatis 接进来。MyBatis 负责“SQL 与对象的转换”。它不像 JPA 那样帮你生成查询而是把你自己写的 SQL 原样发给数据库再把结果集映射成对象。这带来两个直接好处第一SQL 不会被框架重新改写线上慢查询可以直接拿到 explain 分析第二PostgreSQL 特有的语法比如 JSONB 操作符、窗口函数、ON CONFLICTMyBatis 都能原样执行不会被 ORM 拦截。PostgreSQL 负责“数据能力”。我这次迁移感受最深的是它的 JSONB、数组、数值精度和时区处理。业务里有个字段是扩展属性内容不固定MySQL 里我可能得建一张 EAV 表或者用 TEXT 硬存 JSON 再在代码里解析PostgreSQL 直接用 JSONB 列配合查询条件甚至能在数据库层过滤省掉不少 Java 代码。1.2 相比 JPA 和 MyBatis-Plus我为什么仍选原生 MyBatis这个选择当时也被同事质疑过MyBatis-Plus 不是更好用吗确实如果项目全是单表 CRUD、分页、逻辑删除MyBatis-Plus 的 BaseMapper 能省一半工作量。但我们这套系统有大量多表关联和报表统计MyBatis-Plus 的 LambdaQueryWrapper 写复杂查询反而比 XML 更绕。原生 MyBatis 的 XML 虽然啰嗦但胜在“查询怎么写、结果怎么映射”完全透明团队里任何人打开一个 mapper XML 就能看懂。另外PostgreSQL 的很多高级类型MyBatis-Plus 的通用 TypeHandler 覆盖得并不全。原生 MyBatis 允许你针对单列指定 typeHandlerJSONB、数组这种特殊类型自己写处理器就行不依赖框架内置支持。这也是我坚持用原生 MyBatis 的原因。2. 环境准备PostgreSQL 安装、服务启动与连接检查很多人第一步就卡在 PostgreSQL 装不上、服务起不来。这部分我分别说 Windows、Linux以及装完之后必须做的连接验证。2.1 Windows 下安装与常见启动问题Windows 上最省事的方式是下载 EDB 官方安装包。安装过程中有三个地方容易被忽略一是安装目录不要带中文和空格二是端口一般保持默认 5432如果本机已装过旧版 PostgreSQL端口冲突会直接导致服务启动失败三是设置 postgres 超级用户密码时别用太简单的纯数字后面客户端连接、主从配置都会用到。安装完成后很多人打开“服务”面板发现 PostgreSQL 服务没启动。先不要直接点启动去安装目录看日志通常在C:\Program Files\PostgreSQL\16\data\pg_log下。我遇到过最典型的两个原因端口被占用netstat -ano | findstr 5432看一下监听进程如果是其它程序占用改 PostgreSQL 的postgresql.conf里port 5433再启动。数据目录权限不对Windows 用户对data目录没有完全控制权右键目录属性给当前用户授予完全控制权限。启动后用命令行验证一下psql -U postgres -h localhost -p 5432 -d postgres能进入 psql 提示符说明服务正常。如果提示connection refused优先检查服务是否在跑如果提示password authentication failed检查密码有没有记错或者pg_hba.conf里 local 连接的认证方式是不是scram-sha-256。2.2 Linux 下安装与 systemd 管理Linux 分两类发行版。Debian/Ubuntu 直接sudo apt update sudo apt install postgresql postgresql-contrib装完系统会自动创建postgres用户和一个名为postgres的默认数据库。切到该用户再进 psqlsudo -u postgres psqlCentOS/RHEL 系列sudo yum install postgresql-server postgresql-contrib sudo postgresql-setup initdb sudo systemctl enable --now postgresqlCentOS 上特别容易漏掉postgresql-setup initdb这一步不做初始化的话数据目录是空的服务起了也会退出。初始化完成后再设置密码ALTER USER postgres WITH PASSWORD your_password;2.3 连接前必做的数据库和用户准备实际项目不要直接用 postgres 超级用户连业务库这是我在生产环境吃过亏的教训。建议为项目单独建用户和库CREATE USER app_user WITH PASSWORD app_pass_2024; CREATE DATABASE asset_db OWNER app_user; GRANT ALL PRIVILEGES ON DATABASE asset_db TO app_user;把database的连接权限、schema的建表权限都收拢到 app_user 上。这样即使连接串泄露也只是业务库权限不会波及整个实例。3. Spring Boot 工程初始化与核心配置工程层面最核心的三件事依赖选对版本、数据源配置写对、MyBatis 的全局配置和日志输出调通。很多项目起不来看似是代码问题其实都在配置阶段。3.1 依赖版本怎么选Spring Boot 3 与 Boot 2 的区别如果你用 Spring Boot 3.xJava 版本至少是 17对应的 MyBatis 集成 starter 应该是mybatis-spring-boot-starter的 3.x 系列如果项目还在 Boot 2.x则用 2.x 系列。这个版本对应关系是 MyBatis 官方在维护的混用会报各种奇怪的 NoClassDefFoundError。以 Spring Boot 3.2.5 为例pom 里关键依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId version42.7.3/version scoperuntime/scope /dependencyPostgreSQL JDBC 驱动版本建议比数据库小版本新一点。比如数据库是 16.x驱动用 42.7.x 完全没问题。驱动升级到 42.7 之后sslmode、connectTimeout等参数的行为有变化细节我放到排查部分讲。3.2 application.yml 完整配置与逐行解释这是我实际使用的配置模板spring: datasource: url: jdbc:postgresql://localhost:5432/asset_db username: app_user password: app_pass_2024 driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 sql: init: mode: never mybatis: mapper-locations: classpath:mapper/**/*.xml type-aliases-package: com.example.asset.entity configuration: map-underscore-to-camel-case: true jdbc-type-for-null: null log-impl: org.apache.ibatis.logging.stdout.StdOutImpl几个容易被忽略的点driver-class-name在 Spring Boot 3 里其实可以不写数据库驱动会通过url自动推断。但建议显式写避免多个数据库驱动同时在 classpath 时选错。hikari.maximum-pool-size不要拍脑袋设成 50。PostgreSQL 每个连接都是一个后端进程连接数越大越糟。一般业务系统 10~20 足够。jdbc-type-for-null: null是 MyBatis 的经典设置。某些 PostgreSQL 的 SQL 语句里如果 Java 参数为 nullMyBatis 默认会传JdbcType.OTHERPostgreSQL 驱动可能报无法推断类型设置成NULL可避免这类问题。log-impl改为 StdOutImpl 是为了开发时看 SQL生产环境建议删掉这行改用 Slf4j 配合日志框架避免 SQL 打满控制台。3.3 启动类与 Mapper 扫描启动类上必须加MapperScan或者给每个 Mapper 接口加Mapper。我推荐前者统一扫描包路径新增 Mapper 接口时不用重复加注解SpringBootApplication MapperScan(com.example.asset.mapper) public class AssetApplication { public static void main(String[] args) { SpringApplication.run(AssetApplication.class, args); } }这一步如果漏掉最常见的报错是Field assetMapper in ... required a bean of type ... that could not be found。不是 MyBatis 没生效而是 Mapper 接口没有被扫描注册成 Bean。4. 表结构设计与实体层映射PostgreSQL 建表和 MySQL 建表有很不一样的习惯。建表时如果只想着“能用”后面写 XML 映射会非常别扭。我建议从一开始就把“实体字段”和“表字段”的对应关系理顺。4.1 PostgreSQL 建表时的几个关键习惯以资产表为例CREATE TABLE assets ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, asset_code VARCHAR(32) NOT NULL, asset_name VARCHAR(128) NOT NULL, category VARCHAR(64), purchase_price NUMERIC(12,2), extra_info JSONB NOT NULL DEFAULT {}::jsonb, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_assets_status ON assets(status); CREATE INDEX idx_assets_category ON assets(category);两个点我着重说明一下。第一主键不要用BIGSERIAL用GENERATED ALWAYS AS IDENTITY是更现代的标准写法。BIGSERIAL本质是序列加默认值它允许显式插入 id容易在数据迁移时产生序列不同步问题identity 列更规范但在 MyBatis 中要配合useGeneratedKeys处理返回主键。第二时间字段统一用TIMESTAMPTZ不要用TIMESTAMP WITHOUT TIME ZONE。业务系统迟早会遇到跨时区协作TIMESTAMPTZ存储的是带时区的时间点JDBC 驱动读出来后能正确对应LocalDateTime不会出现“入库早一小时、显示晚一小时”的灵异事件。4.2 实体类设计LocalDateTime 与 JSONB 的处理实体类我用的是基础 POJO字段与表字段用驼峰命名对应public class Asset { private Long id; private String assetCode; private String assetName; private String category; private BigDecimal purchasePrice; private MapString, Object extraInfo; private Integer status; private LocalDateTime createdAt; private LocalDateTime updatedAt; }extraInfo对应 JSONB 列Java 类型直接用MapString, Object。这里必须配合自定义 TypeHandler否则 MyBatis 会拿到PGobject类型转换直接报错。TypeHandler 的工作流程其实很简单写库时 Java 对象 - JSON 字符串 -PGobject- PostgreSQL JSONB 列读库时反过来JSONB 列 - 字符串 - Java 对象。我自己实现的JsonbTypeHandler在后面映射章节给出完整代码。4.3 Mapper 接口与 XML 文件组织方式Mapper 接口只写方法签名和参数注解public interface AssetMapper { Asset findById(Param(id) Long id); ListAsset search(Param(category) String category, Param(status) Integer status, Param(keyword) String keyword); int insert(Asset asset); int batchInsert(Param(list) ListAsset assets); int update(Asset asset); int deleteById(Param(id) Long id); }XML 文件放在src/main/resources/mapper目录下文件名与 Mapper 接口名保持一致例如AssetMapper.xml。这样做的好处是 IDE 插件比如 MyBatisX可以自动跳转团队协作时找文件也快。5. MyBatis XML 映射与动态 SQL 实战这一章是整篇的干货核心。我把日常写 SQL 时最高频的几个场景拆开讲resultMap、动态查询、批量插入、分页。每个场景不仅给出可运行的代码还会说明为什么这么写。5.1 resultMap 与自定义 TypeHandler 的完整用法XML 里先定义一个 resultMap把数据库列名和 Java 属性映射起来重点看extraInfo这一列resultMap idassetMap typecom.example.asset.entity.Asset id propertyid columnid/ result propertyassetCode columnasset_code/ result propertyassetName columnasset_name/ result propertycategory columncategory/ result propertypurchasePrice columnpurchase_price/ result propertyextraInfo columnextra_info typeHandlercom.example.asset.handler.JsonbTypeHandler/ result propertystatus columnstatus/ result propertycreatedAt columncreated_at/ result propertyupdatedAt columnupdated_at/ /resultMapJsonbTypeHandler完整代码如下package com.example.asset.handler; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.postgresql.util.PGobject; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; public class JsonbTypeHandler extends BaseTypeHandlerMapString, Object { private static final ObjectMapper MAPPER new ObjectMapper(); Override public void setNonNullParameter(PreparedStatement ps, int i, MapString, Object parameter, JdbcType jdbcType) throws SQLException { try { PGobject jsonObject new PGobject(); jsonObject.setType(jsonb); jsonObject.setValue(MAPPER.writeValueAsString(parameter)); ps.setObject(i, jsonObject); } catch (Exception e) { throw new SQLException(Cannot convert Map to JSONB, e); } } Override public MapString, Object getNullableResult(ResultSet rs, String columnName) throws SQLException { return readValue(rs.getString(columnName)); } Override public MapString, Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return readValue(rs.getString(columnIndex)); } Override public MapString, Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return readValue(cs.getString(columnIndex)); } private MapString, Object readValue(String json) { if (json null) { return null; } try { return MAPPER.readValue(json, Map.class); } catch (Exception e) { throw new RuntimeException(Cannot parse JSONB to Map, e); } } }这个处理器的关键点有两个写库时一定要setType(jsonb)否则数据库可能把它当成json类型部分操作会有差异读库时直接从 ResultSet 取字符串再解析不要尝试让 MyBatis 自动转换PGobject。5.2 动态 SQLwhere、set、foreach 的实战写法PostgreSQL 的查询条件经常是“可选 组合”用where标签最合适。where会自动去掉子句开头的AND或OR比在每个条件里手写WHERE 11干净得多。select idsearch resultMapassetMap SELECT * FROM assets where if testcategory ! null and category ! AND category #{category} /if if teststatus ! null AND status #{status} /if if testkeyword ! null and keyword ! AND (asset_code ILIKE CONCAT(%, #{keyword}, %) OR asset_name ILIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY created_at DESC /select这里我用了ILIKE而不是LIKE是 PostgreSQL 特有的不区分大小写模糊匹配。如果项目需要走索引这种写法在数据量大的时候可能要改造成pg_trgm的 GIN 索引但小规模业务这样写已经够了。更新语句用set标签同样能自动处理尾逗号update idupdate UPDATE assets set if testassetName ! nullasset_name #{assetName},/if if testcategory ! nullcategory #{category},/if if teststatus ! nullstatus #{status},/if updated_at NOW() /set WHERE id #{id} /update5.3 批量插入PostgreSQL 的参数上限与分批策略批量插入我建议直接拼单条 INSERT 的多值语句而不是用 JDBC 的executeBatch()循环。PostgreSQL 对多值 INSERT 的解析效率很高而且能减少客户端与数据库的往返次数。insert idbatchInsert INSERT INTO assets (asset_code, asset_name, category, purchase_price, extra_info, status) VALUES foreach collectionlist itemitem separator, (#{item.assetCode}, #{item.assetName}, #{item.category}, #{item.purchasePrice}, #{item.extraInfo, typeHandlercom.example.asset.handler.JsonbTypeHandler}, #{item.status}) /foreach /insert注意 PostgreSQL 对单条语句的占位符数量有上限虽然数值很大但实际单批次行数不要盲目拉高。我通常控制在 500 到 1000 行一批。比如每行 6 个字段1000 行就是 6000 个参数很安全如果一次塞 5000 行参数数量逼近上限可能报PreparedStatement参数过多或数据库内存飙升。写入后再用一个for循环分页调batchInsert性能实测比 MyBatis 默认的ExecutorType.BATCH更稳定。5.4 分页查询用分页插件还是手工 limit/offset如果你的业务分页条件不复杂我建议先用 PostgreSQL 原生的LIMIT/OFFSET写一个公共的分页查询不要一上来就引 PageHelper。理由很简单PageHelper 的物理分页逻辑会改写你的 SQL遇到WITH子句、窗口函数这种复杂查询时偶尔会生成错误的 count 语句。简单分页长这样select idsearchPage resultMapassetMap SELECT * FROM assets where if testasset.status ! null AND status #{asset.status} /if /where ORDER BY created_at DESC LIMIT #{pageSize} OFFSET #{offset} /select配套的 count 查询单独写一个select返回long。这样可以保证 count 和 page 的查询条件完全一致也方便在 count 里删除不必要的ORDER BY。6. PostgreSQL 专属特性在 MyBatis 中的落地这一章是很多从 MySQL 转过来的团队最想看的。PostgreSQL 的高级类型和 SQL 特性用好了确实能简化应用层代码。但如果 MyBatis 映射没配对反而会比 MySQL 更痛苦。6.1 JSONB 的读写条件过滤与常见坑除了用 TypeHandler 完成映射有时还需要在 SQL 层过滤 JSONB 字段。比如按扩展属性里的brand过滤select idsearchByBrand resultMapassetMap SELECT * FROM assets WHERE extra_info - brand #{brand} /select-返回的是文本-返回的是 JSON 值。PostgreSQL 的 JSONB 查询很灵活但有一个坑是MyBatis 的#{}参数在 SQL 里是?占位符而 JSONB 的空值判断如果写成extra_info - brand ?参数类型很难推断。最好用-取出文本再和字符串比较或者显式加::text转换。对 JSONB 字段建索引时也别用普通 B-tree应该用 GIN 索引比如CREATE INDEX idx_assets_extra ON assets USING GIN (extra_info);这一个索引能加速很多 JSONB 包含查询。6.2 返回主键与 UPSERTON CONFLICT 的实现新增记录时如果不想先查一遍再决定 insert 还是 updatePostgreSQL 的ON CONFLICT是最好用的。配合 MyBatis 的useGeneratedKeys可以拿到新生成的 idinsert idinsertWithUpsert parameterTypecom.example.asset.entity.Asset useGeneratedKeystrue keyPropertyid INSERT INTO assets (asset_code, asset_name, category, purchase_price, extra_info, status) VALUES (#{assetCode}, #{assetName}, #{category}, #{purchasePrice}, #{extraInfo, typeHandlercom.example.asset.handler.JsonbTypeHandler}, #{status}) ON CONFLICT (asset_code) DO UPDATE SET asset_name EXCLUDED.asset_name, category EXCLUDED.category, purchase_price EXCLUDED.purchase_price, extra_info EXCLUDED.extra_info, status EXCLUDED.status, updated_at NOW() /insert这段 SQL 里有个小细节asset_code是唯一键冲突时用EXCLUDED引用插入但被拒绝的新值。这种方式天然适合“相同业务编码只保留一条最新记录”的场景比先 select 再 insert 的竞态控制可靠得多。6.3 窗口函数与分组取最新一条报表系统里经常要按分类分组取每条分组最新的一条记录。在 MySQL 8 之前你得写子查询加 joinPostgreSQL 的窗口函数就很直接select idfindLatestByCategory resultMapassetMap SELECT id, asset_code, asset_name, category, purchase_price, extra_info, status, created_at, updated_at FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY created_at DESC) AS rn FROM assets ) t WHERE rn 1 /select注意这种 SQL 里extra_info仍要用resultMap映射否则 MyBatis 拿到的是PGobject。窗口函数本身不改变列名和类型所以唯一要多做的就是保持 resultMap 正确。记住一个原则PostgreSQL 擅长的事情尽量在 SQL 里做不要全部拉回 Java 再 stream 处理。因为数据库端基于索引的排序和聚合通常比 JVM 内存里的全量计算高效得多。7. 事务、缓存与连接管理防止线上问题代码能跑只是第一步真正上线后出问题的大多是事务边界、缓存一致性和连接池管理。这几个点也是 MyBatis 面试题里的高频区。7.1 Transactional 的正确使用范围Spring 声明式事务最常见的问题是“事务被一个自己调自己的方法绕过”。比如Service public class AssetService { public void process(Asset asset) { updateAsset(asset); } Transactional(rollbackFor Exception.class) public void updateAsset(Asset asset) { assetMapper.update(asset); } }process方法通过this.updateAsset调用事务注解根本不会生效因为 Spring 事务是通过代理对象实现的类内部调用走的是原始对象。解决办法是把事务方法放到另一个 Service或者注入自身代理Transactional(rollbackFor Exception.class) public void process(Asset asset) { assetMapper.update(asset); doOtherThing(); // 如果此处抛异常上面 update 会回滚 }另外rollbackFor Exception.class在 Spring Boot 中默认已经对所有 RuntimeException 生效但受检异常默认不回滚。如果你有自定义的受检业务异常必须显式加rollbackFor否则数据就会半提交。7.2 MyBatis 一级缓存与二级缓存面试考点与工程取舍MyBatis 一级缓存是 SqlSession 级别的。在 Spring Boot 集成下SqlSession 的生命周期通常绑定到一次数据库操作甚至一个事务方法所以一级缓存能缓存同一个 SqlSession 内的相同查询。实际作用没有想象中大因为大多数业务每个 Service 方法都有自己的 SqlSession。二级缓存是 Mapper namespace 级别的跨 SqlSession 共享。开启很简单在 XML 里加一行cache/但实话实说我在业务系统里基本不开启二级缓存。原因有三个第一多表更新时非常容易命中过期缓存第二分布式环境下默认缓存只在单机内有效一致性很难保证第三PostgreSQL 本身的查询性能已经不差很多查询完全可以直接走数据库缓存。如果真有高频只读数据我宁可引入独立的缓存组件而不是把 MyBatis 二级缓存作为一个长期方案。7.3 HikariCP 连接池参数调整与连接泄漏Spring Boot 默认连接池是 HikariCP它对单条 SQL 的执行效率很好但连接数配置不合理会有两种典型现象连接池被打满报Connection is not available, request timed out。数据库端出现大量idle in transaction连接物理连接数暴增。前者通常是因为业务并发高而maximum-pool-size太小后者更常见是代码里的事务没有正确提交或回滚连接一直被事务占着。排查方法很简单在 PostgreSQL 里执行SELECT pid, state, now() - xact_start AS duration, query FROM pg_stat_activity WHERE state idle in transaction;看到长时间idle in transaction的连接就去对应代码里查Transactional方法是不是抛了异常没回滚或者有没有在事务里做了耗时很长的外部调用。高并发场景下Hikari 的max-lifetime建议小于数据库端的连接超时时间。比如 PostgreSQL 默认没有强制 kill 空闲连接但中间代理层可能限制连接最大存活时间所以要把max-lifetime调成比代理超时短一点避免“连接被服务端关闭后客户端还在复用”的报错。8. 高频问题排查速查表这部分是我压箱底的排查记录。很多问题看起来五花八门归根结底就那么几类。我整理成表格方便你遇到问题时直接对照。症状常见原因解决办法No suitable driver found for jdbc:postgresql://...缺少 PostgreSQL JDBC 依赖确认 pom 里引入org.postgresql:postgresql且 scope 不是providedrelation assets does not exist表名大小写问题或 schema 不对检查表实际名称加 schema 前缀如public.assetscolumn assetcode does not exist下划线转驼峰没生效确认map-underscore-to-camel-case: true或显式在 resultMap 配 columnCannot infer type for parameterJava 参数为 null 时 JdbcType 推断失败在 MyBatis 全局配置设置jdbc-type-for-null: nullconnection refusedPostgreSQL 服务没启动、端口不对、防火墙拦截服务先启动测试psql -h localhost -p 5432 -U postgresPSQLException: FATAL: password authentication failed密码错误或 pg_hba.conf 认证策略不对检查连接用户名密码确认pg_hba.conf中 host 认证方式class java.lang.String cannot be cast to ... PGobjectJSONB 列没有使用自定义 TypeHandlerresultMap 和 insert 参数里都加JsonbTypeHandlerarguments cannot be null批量插入集合为空或元素属性为 nullforeach 前先判断集合非空必填字段加校验Spring Boot 启动报 Invalid value type for property sslMode驱动版本与连接串参数不匹配检查连接串中的sslmode参数确认驱动是 42.x 新版本8.1 时区问题为什么查出来老是差 8 小时PG 的TIMESTAMPTZ存储的是 UTC 时间JDBC 读出来转成LocalDateTime时是否带时区转换取决于驱动参数。最简单的规避方式是连接串里明确指定时区url: jdbc:postgresql://localhost:5432/asset_db?serverTimezoneAsia/Shanghai准确说 PostgreSQL 的 JDBC 参数名不是serverTimezone那套参数是 MySQL 的习惯。PostgreSQL 中更常见的是用TimeZone参数但 JDBC 读取行为主要取决于 JVM 默认时区和 PG 服务端设置。我的做法是数据库统一用timestamptzJava 实体全部用LocalDateTimeJVM 启动参数加-Duser.timezoneAsia/Shanghai这样基本不会出现偏差。8.2 MyBatis 打印 SQL 与日志脱敏开发时想看到真实 SQL 和参数把log-impl设为 StdOutImpl 即可。但这样会把完整的参数都打出来生产环境不适合。如果只想看 mapper 里某个慢查询可以把日志级别调整到只对某个 mapper 包生效logging: level: com.example.asset.mapper: debug同时注意日志里如果打印了 SQL 参数要确保purchase_price、extra_info这类字段不是敏感数据。PostgreSQL 的 SQL 日志本身也有log_statement参数默认none线上别随便开成ddl或mod否则大查询日志量会爆炸。8.3 Spring Boot 3 与旧版 MyBatis starter 的兼容性问题如果你从 Boot 2 升级 Boot 3一定要同步升级mybatis-spring-boot-starter到 3.x。很多升级编译不过的报错都出在org.mybatis.spring.SqlSessionTemplate或者org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration的变更上。Boot 3 里ConfigurationProperties、DataSource 自动配置的类名和位置都变了靠百度旧代码硬调越调越乱。升级后的配置项大多是兼容的mybatis.mapper-locations、mybatis.configuration.map-underscore-to-camel-case这些还是同样写法。真正要检查的是有没有在代码里直接new SqlSessionFactoryBean手动配置数据源如果有那就要适配 Boot 3 的DataSourceBuilder。8.4 批量操作遇到PreparedStatement相关报错Batch update returned unexpected row count是我见过的经典报错。这个报错在 PostgreSQL 上往往不是 SQL 错而是 MyBatis 的ExecutorType.BATCH和表里某些触发器、外键约束的预期行数不一致导致 JDBC 驱动认为执行失败。遇到这种别死磕 XML换个思路回到多值 INSERT 的写法问题通常会消失。这也是我在批量插入一节坚持用foreach拼多值语句的原因之一。写在最后的一点经验这套 Spring Boot MyBatis PostgreSQL 的组合真正跑顺之后给我最大的感受是“稳定且透明”。MyBatis 让我对每一条 SQL 都有掌控PostgreSQL 又把这些 SQL 的潜力发挥到最大Spring Boot 则负责把这些粘合起来。如果只给你一个建议我会说从项目第一天就按 PostgreSQL 的习惯来设计表结构和 SQL不要带着 MySQL 的旧习惯硬套。见过太多团队迁库之后SQL 还是老写法JSONB 不用、数组不用、窗口函数也不用结果迁完性能反而更差。先花半天把 PostgreSQL 的常用运算符和类型体系过一遍后面能省你数倍的时间。最后再分享一个小技巧开发环境连接 PostgreSQL 时尽量用 DBeaver 或 Navicat 这类图形工具看执行计划但线上问题排查一定学会用pg_stat_statements和EXPLAIN ANALYZE。MyBatis 打出来的 SQL 复制进 psql 里执行再EXPLAIN ANALYZE一遍你会发现大多数慢查询问题根本不在框架层而在 SQL 本身。把排查精力放到 SQL 上比折腾框架配置有意义得多。
返回列表