资讯动态

Spring Boot 整合 MyBatis 与 PostgreSQL:从配置到性能优化全解析

发布时间:2026/9/30 3:28:12 来源:尧图企业网站定制
做 Java 后端这几年Spring Boot、MyBatis、PostgreSQL 这三样东西几乎成了我项目里的固定搭配。不管是刚入行的新手还是已经被线上事故磨过几轮的老兵最终都会发现一套用得住、讲得清、改得动的数据访问方案比追着框架版本跑要重要得多。这篇文章就围绕“Spring Boot 整合 MyBatis 与 PostgreSQL”这条主线把我从零搭环境、写 Mapper、调缓存、排故障的全过程拉一遍也顺手回答那些你大概率搜过的问题PostgreSQL 到底下载哪个版本MyBatis 一级二级缓存怎么才有效TypeHandler 工作流程是什么批量插入为什么这么慢。这不是官方文档的搬运全是我实际操作过、踩过坑之后沉淀下来的东西。适合三类人看准备用 Spring Boot 写第一个完整项目的初学者项目里想从 MySQL 迁到 PostgreSQL 或者正在选型的后端开发以及马上要面试、想把 MyBatis 底层和 PostgreSQL 常见坑一次讲清楚的候选人。1. 这套技术栈到底好在哪选型思路与架构拆解1.1 为什么是 Spring Boot MyBatis PostgreSQL我帮人看过不少项目也重构过一些祖传代码最后得出的结论是技术选型没有银弹但 Spring Boot MyBatis PostgreSQL 的组合是绝大多数业务系统里最不容易翻车的一种。Spring Boot 解决的问题很纯粹把 Spring 那一大堆 XML 配置、Bean 装配、依赖管理全部收敛成“约定大于配置”。你新建一个项目加了依赖就能跑内置 Tomcat打包成 jar 直接扔服务器执行。它不负责业务但它把你从环境搭建里解放出来让你专心写代码。MyBatis 的存在则有点反主流。当年 JPA/Hibernate 火的时候很多人觉得“全自动 ORM 才是未来”但真到了复杂报表、多表 join、分库分表、SQL 调优的时候Hibernate 的自动 SQL 反而成了约束。MyBatis 的思路是“半自动”你写 SQL它帮你在 Java 对象和数据库记录之间做映射。SQL 是你的执行计划是数据库优化器说了算映射的细节由 MyBatis 处理。这种“把 SQL 主动权还给开发者”的理念在性能敏感、SQL 复杂的业务里特别舒服。PostgreSQL 更是被低估的选手。过去大家默认用 MySQL但 PostgreSQL 在标准兼容性、JSON 支持、事务隔离、扩展能力上其实更接近 Oracle 这类商业数据库。很多从 Oracle 迁出来的项目第一站就是 PostgreSQL。它的 JSONB 类型可以直接当文档数据库用数组类型也让一些复杂业务字段省掉一张子表再加上出色的 MVCC 并发控制这套组合扛住中小型系统的全部流量毫无压力。说白了这套组合的逻辑是Spring Boot 提供稳定底座MyBatis 给你 SQL 控制力PostgreSQL 提供现代数据库能力。三者各管一段没有哪个环节特别“黑盒”出了问题你能自己定位。1.2 版本选型Spring Boot 2 还是 3PostgreSQL 用哪个版本版本问题是新手最容易纠结的地方也是线上故障的常见源头。直接给结论。Spring Boot 目前主流就是 3.x 系列但它要求 Java 17 以上。如果你还在用 JDK 8那就老老实实用 Spring Boot 2.7.x这是 2.x 最后一个大版本还在维护期内。Spring Boot 3 最大的变化有两个一是 javax 命名空间换成了 jakarta二是很多自动配置类的内部实现被重构。如果你的项目要从 2 升 3需要重新梳理依赖尤其是 Spring Security 的配置方式网上搜“spring boot 3 spring security 配置迁移”能看到大量踩坑记录本质上都是命名空间和配置类位置变了。MyBatis 官方提供了专门的 Starter版本对应关系要记牢Spring Boot 版本JDK 要求mybatis-spring-boot-starter 推荐版本2.5.x - 2.7.x8/112.2.x / 2.3.x3.0.x - 3.2.x173.0.x3.3.x173.0.4千万不要用一个很新的 Spring Boot 3.3 去配 mybatis-spring-boot-starter 2.3那会直接因为版本冲突起不来。Maven 报错一般很明确但能避免的冲突就别等着报错。PostgreSQL 版本我建议分场景看。新项目直接用 17它是当前稳定版性能、并行查询、逻辑复制都有明显改进。老项目如果依赖了某个版本的行为特性没有特殊情况就别乱升PostgreSQL 的大版本升级涉及底层数据文件格式不能原地覆盖升级要用 pg_upgrade 或者 dump/restore 迁移。生产环境里我还见过一套项目跑在 PostgreSQL 10 上都快 EOL 了还在硬撑这种属于技术债建议尽快规划迁移。PostgreSQL JDBC 驱动版本选 42.7.x 这一类新版本就行它向后兼容 server 9.4 以上的绝大多数版本。驱动版本直接放到 pom.xml 里Spring Boot 的 dependency management 会帮你管理驱动版本但你也可以显式指定避免父 POM 升级时驱动被悄悄换掉。2. 环境准备从零把 PostgreSQL 跑起来2.1 安装 PostgreSQLWindows 和 Linux 两种最典型场景PostgreSQL 下载哪个版本这个问题搜索量一直很高其实答案很统一去官网 postgresql.org/download 拿官方安装包别去第三方站点下什么“优化版”“绿色版”。Windows 上直接下载 EDB 提供的 installer它是一个图形化安装向导能装服务端、pgAdmin 图形客户端还能顺手装 Stack Builder。安装过程中会让你设置 postgres 超级用户的密码这个密码千万别忘记后面所有数据库操作都要从它展开。Windows 装完 PostgreSQL服务默认是自动启动的。如果“服务启动失败”最常见的原因是安装时选的端口 5432 已经被占用或者修改了数据目录权限。排查方法很简单cmd 里跑一句netstat -ano | findstr 5432看端口到底是哪个进程占用。如果是之前残留的 PostgreSQL 实例先把旧服务停掉再启动新服务。实在搞不定检查 Windows 服务管理器里 PostgreSQL 对应的服务状态和日志目录日志里会写清楚失败原因。Linux 上我以 CentOS 7.9 为例因为这是很多企业内部服务器的现状。CentOS 7 自带的软件源里 PostgreSQL 版本很老所以要用官方 PGDG 仓库sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm sudo yum install -y postgresql17-server sudo /usr/pgsql-17/bin/postgresql-17-setup initdb sudo systemctl enable --now postgresql-17安装后默认会创建一个 postgres 系统用户以及一个同名的超级数据库用户。切换到 postgres 用户后就可以建库建账号sudo -i -u postgres psql -c CREATE ROLE app_user LOGIN PASSWORD YourPassword; psql -c CREATE DATABASE mydb OWNER app_user;如果是本机开发这样已经能用了。但如果你是想在另一台电脑上用 Navicat、DBeaver 或者 Spring Boot 应用连这个 PostgreSQL有两个配置必须要改监听地址和认证规则。默认 PostgreSQL 只监听本机地址外部连接会直接超时或者被拒。编辑 postgresql.conflisten_addresses *然后编辑 pg_hba.conf在文件末尾加上host all all 0.0.0.0/0 scram-sha-256最后重载配置systemctl reload postgresql-17注意pg_hba.conf 的规则是从上往下匹配的如果你在之前已经有更严格的规则覆盖了 0.0.0.0/0要把新规则放在前面或者直接替换掉之前的下一行规则。生产环境不要用 0.0.0.0/0 这种全开放写法要精确到内网网段。2.2 用 Docker 快速起一个开发用 PostgreSQL日常开发我更喜欢用 Docker 起一个 PostgreSQL 实例。原因很简单干净、可重复、切换版本方便。一个命令就能得到一个全新的 PG 17 环境不想用了删掉容器再起一个数据说丢就丢不污染本机。docker run -d --name postgres17 \ -e POSTGRES_PASSWORDmysecret \ -e POSTGRES_DBmydb \ -v pgdata:/var/lib/postgresql/data \ -p 5432:5432 \ postgres:17这个命令里的三个环境变量要注意POSTGRES_PASSWORD 是超级用户 postgres 的密码POSTGRES_DB 在首次初始化时自动创建一个空数据库POSTGRES_USER 如果不写默认就是 postgres。-v pgdata:/var/lib/postgresql/data 是数据卷持久化把数据库文件放到 Docker 卷里这样容器删了数据还在。如果你本机 5432 端口已经被占冒号左边改一个端口比如 -p 5433:5432连接 URL 里也要相应改成 5433。容器启动后想进去测试一下docker exec -it postgres17 psql -U postgres -d mydb看到 PostgreSQL 的提示符之后执行 SELECT 1;能返回一行结果就说明服务端正常。这一步测试非常关键因为后面 Spring Boot 应用连不上的时候你要先想办法确认“数据库本身是好的”再去排查应用配置。我用 macOS 和 Windows 都跑过这套 Docker 方案只要 Docker Desktop 能正常启动PostgreSQL 容器基本不会出乱子。反倒是本机直接装 PostgreSQL 时不同系统的服务管理方式不同Windows 的服务管理器、Linux 的 systemd、macOS 的 brew services 各有一套出问题时的排查路径完全不同。开发环境用 Docker 能把这些差异全部抹掉强烈建议。3. Spring Boot 项目搭建与 MyBatis 整合配置3.1 快速创建第一个 Spring Boot 程序并连上 PostgreSQL创建项目直接去 start.spring.io这是官方脚手架比在 IDE 里点向导更可控。选择 Maven 项目、Java 版本、Spring Boot 版本依赖部分加上 Spring Web、MyBatis Framework、PostgreSQL Driver、Lombok。如果你是在国内网络环境下访问 start.spring.io 偶尔拉不动也可以直接用 IDE 内置的 Spring Initializr效果一样。生成出来的项目 pom.xml 里关键依赖是这样dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency如果你的 Spring Boot 版本是 3.x那 mybatis starter 必须用 3.x如果你用的是 Spring Boot 2.7那版本就用 2.3.x这点前面已经强调过别让 Maven 帮你猜直接钉死版本。启动类里加一行 MapperScan让 MyBatis 自动扫描 Mapper 接口并生成代理对象SpringBootApplication MapperScan(com.example.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这里 MapperScan 的作用是告诉 MyBatis 去哪个包底下找接口。如果不加每个 Mapper 接口上都要单独标 Mapper 注解比较啰嗦。3.2 数据源与 MyBatis 核心配置详解所有持久层框架的第一步都是把数据源配对。Spring Boot 2.x 开始默认用 HikariCP 作为连接池这个选择很聪明HikariCP 性能极好而且配置极简。在 application.yml 里这样写spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: app_user password: YourPassword driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000连接 JDBC URL 的格式是固定的jdbc:postgresql://主机:端口/数据库名。如果本机用 Docker 映射了 5433这里的 URL 就要写成 jdbc:postgresql://localhost:5433/mydb。然后配置 MyBatis 自身的参数mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl逐项解释一下mapper-locations 指定 XML 映射文件放哪。我习惯在 resources 下建一个 mapper 目录所有 XML 统一放在那里。Spring Boot 默认不会把 src/main/java 底下的 XML 打进 classpath所以千万别把 XML 放在 Mapper 接口同级目录除非你额外加资源构建配置。放在 resources/mapper 下是最省心的。type-aliases-package 会让 MyBatis 把该包下所有类注册为短别名。比如实体类 User在 XML 里可以直接用 resultTypeUser 而不写全限定类名。map-underscore-to-camel-case 开启下划线转驼峰。PostgreSQL 里字段命名规范用 user_nameJava 属性用 userName这个开关一开resultMap 都不用写字段映射了。log-impl 设成 StdOutImpl执行 SQL 时会直接把参数和执行结果打印到控制台开发阶段排查 SQL 简直神器。配好之后写个最简单的查询验证整条链路。新建一个表CREATE TABLE t_user ( id BIGSERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, email VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );然后写一个 Mapper 接口public interface UserMapper { ListUser findAll(); }XML 映射select idfindAll resultTypeUser SELECT id, name, email, created_at FROM t_user /select启动应用看到 SQL 打印出来说明 Spring Boot、MyBatis、PostgreSQL 三者已经通了。这一步是整个脚手架的地基地基稳了后面加 CRUD、加缓存都是锦上添花。3.3 MyBatis 的初始化工作流程一次讲透这个点也是面试高频题。MyBatis 整个初始化工作流程拆开看是四个阶段。第一阶段是构建 SqlSessionFactory。MyBatis 启动时要解析全局配置文件和应用里的 Mapper XML把每一个 SQL 语句、参数映射、结果映射都包装成 MappedStatement 对象放入 Configuration 对象中。Configuration 是 MyBatis 的“心脏”里面存了所有运行时需要的元信息。第二阶段是创建 SqlSessionFactory。通过 SqlSessionFactoryBuilder 读配置生成一个不可变的 SqlSessionFactory 单例。在 Spring Boot 环境里mybatis-spring-boot-starter 替我们自动完成了这一个过程它会在 Spring 容器启动时调用 SqlSessionFactoryBuilder获取数据源和配置构建出工厂。第三阶段是创建 SqlSession。SqlSession 是 MyBatis 执行 SQL 的门面。在 Spring 集成环境下我们不会直接操作原生 SqlSession而是使用 SqlSessionTemplate。SqlSessionTemplate 实现了线程安全每个需要执行 Mapper 方法的时候从工厂拿到一个 SqlSession执行完关闭提交/回滚交给 Spring 的数据库事务管理统一处理。第四阶段是 Mapper 代理。Mapper 接口本身没有实现类MyBatis 在启动时通过 JDK 动态代理为每个接口生成代理对象。你调用 userMapper.findAll()代理对象会找到对应的 MappedStatement交给 SqlSessionTemplate 执行最后把 JDBC ResultSet 通过 ResultMap 映射成 Java 对象返回。这条链路理解了后面很多坑都能对上号。比如为什么 MyBatis 缓存失效、为什么 Mapper 不能被 Spring 的切面代理处理、为什么一个接口可以没有实现类。这些面试题的本质都是这条链路。4. 写一个完整的增删改查动态 SQL、TypeHandler 与分页4.1 实体类、Mapper 接口与 XML 映射文件的正确对齐方式建表、建实体、建接口、建 XML这是 MyBatis 项目的标准四件套。以用户表为例Data public class User { private Long id; private String name; private String email; private LocalDateTime createdAt; private String status; }PostgreSQL 的 BIGSERIAL 自增主键在 MyBatis 插入时需要告诉它主键是数据库生成的以及要把生成的主键回填到对象的 id 属性上insert idinsertUser parameterTypeUser useGeneratedKeystrue keyPropertyid INSERT INTO t_user (name, email, status, created_at) VALUES (#{name}, #{email}, #{status}, #{createdAt}) /insert关于参数传递有个细节值得单独拎出来说Mapper 接口方法有多个参数时一定要用 Param 注解指定参数名。比如ListUser findByStatusAndName(Param(status) String status, Param(name) String name);XML 里就写 #{status} 和 #{name}。如果不加 ParamMyBatis 也可以用参数位置下标比如 #{param1}、#{param2}但这种写法可读性太差代码审查时几乎过不了。还有 #{} 和 ${} 的区别这是 MyBatis 面试必考#{} 是预编译占位符会生成 JDBC 的 PreparedStatement 参数安全${} 是字符串拼接有 SQL 注入风险只用在表名、列名、排序字段这类不能预编译的场景并且必须做好白名单校验。4.2 动态 SQL 实战多条件查询、批量插入、PostgreSQL 的 ON CONFLICT动态 SQL 是 MyBatis 最实用的能力没有之一。一个典型场景用户管理页面要按姓名模糊查、按状态精确查、按创建时间范围查三个条件可组合也可留空。select idfindByCondition resultTypeUser SELECT id, name, email, status, created_at FROM t_user where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null and status ! AND status #{status} /if if teststartTime ! null AND created_at gt; #{startTime} /if if testendTime ! null AND created_at lt; #{endTime} /if /where ORDER BY created_at DESC /select标签有两个隐藏功能自动在前面补 WHERE 关键字自动把第一个多余的 AND 或 OR 去掉。这两个功能在动态条件查询里至关重要少了它你得自己在每个 if 里拼表名和 AND极易出错。批量插入也是日常高频操作。一个常见错误是把一万条数据放到 for 循环里一条条 insert速度慢得让人怀疑人生。正确写法是用 foreachinsert idbatchInsert INSERT INTO t_user (name, email, status, created_at) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.email}, #{item.status}, #{item.createdAt}) /foreach /insert但要记住两个限制PostgreSQL 对单条 INSERT 语句能携带的多值列表有上限一般是 65535 个参数所以一次批量插入的最佳实践是 500 到 1000 条分一批别一个批次塞几万条第二个是批量数据量太大的话SQL 字符串本身可能超出数据库参数上限或者内存限制。PostgreSQL 还有一个 MySQL 没有的杀手级语法ON CONFLICT也就是插入时存在唯一键冲突就执行更新。MyBatis 里可以直接写insert idupsertUser INSERT INTO t_user (id, name, email) VALUES (#{id}, #{name}, #{email}) ON CONFLICT (id) DO UPDATE SET name EXCLUDED.name, email EXCLUDED.email /insert这种写法在同步任务、定时抓取场景里特别实用不用先查再判断插入还是更新一次 SQL 完成。4.3 TypeHandler 工作流程与自定义 JSONB 映射TypeHandler 是 MyBatis 里最容易讲不清楚的概念。它干的事很简单Java 类型和 JDBC 类型之间的双向转换。你在 XML 写的 #{name}执行时 MyBatis 会调用对应的 TypeHandler 的 setParameter 方法把 Java 对象的属性值设置到 PreparedStatement 上查询返回的 ResultSetMyBatis 又会调用 TypeHandler 的 getResult 方法把数据库列的值取出来构造成 Java 对象。所以一个完整的 TypeHandler 类型处理器包含两个方向的转换逻辑。MyBatis 内置了很多常用 TypeHandlerString、Integer、Long、LocalDateTime、Map、List 等。但 PostgreSQL 有一些特有类型比如 jsonb、数组类型内置 TypeHandler 处理不了需要自定义。我在项目里最常用的是把 PostgreSQL 的 jsonb 字段映射成 Jackson 的 JsonNode 对象。做法是继承 BaseTypeHandler 并指定泛型MappedTypes(JsonNode.class) MappedJdbcTypes(JdbcType.OTHER) public class JsonNodeTypeHandler extends BaseTypeHandlerJsonNode { private static final ObjectMapper MAPPER new ObjectMapper(); Override public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.toString()); } Override public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException { return parse(rs.getString(columnName)); } Override public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return parse(rs.getString(columnIndex)); } Override public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return parse(cs.getString(columnIndex)); } private JsonNode parse(String json) { try { return MAPPER.readTree(json); } catch (Exception e) { return null; } } }这里用 setString 的原因很简单PostgreSQL JDBC 驱动对 jsonb 类型的处理比较特殊直接以文本形式写入就能被数据库正确识别。而读取时数据库返回的本身就是 JSON 字符串交给 Jackson 解析即可。在 XML 里使用时要指定 typeHandlerresultMap idUserResultMap typeUser result columnprofile propertyprofile typeHandlercom.example.demo.handler.JsonNodeTypeHandler/ /resultMap如果你只是处理一个字段也可以用注解直接用在大字段上。TypeHandler 在面试里被高频问到“工作流程是什么样的”其实就是 setParameter 和 getResult 这两个方向的转换时机把这个讲清楚就达标了。4.4 分页查询PageHelper 与手写 limit 的正确姿势分页是后端系统躲不开的需求。集成 PageHelper 是最常见的做法Maven 加依赖dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version2.1.0/version /dependency然后应用类上加配置或者直接在使用时调用PageHelper.startPage(pageNum, pageSize); ListUser users userMapper.findByCondition(query); PageInfoUser pageInfo new PageInfo(users);PageHelper 的原理是拦截器。调用 startPage 后会往 ThreadLocal 里存分页参数接下来执行的第一个 Mapper 查询会被 PageInterceptor 拦截先在原 SQL 基础上生成 count 统计 SQL再拼接 limit 实现分页。但我必须提醒一点PageHelper 的 count 查询在复杂 SQL 上经常“自作聪明”。比如你的查询里带了 GROUP BY、带了 UNION自动生成的 count SQL 可能统计错误。所以我现在的项目分页已经不怎么依赖 PageHelper 了更喜欢手写 limitselect idfindByPage resultTypeUser SELECT id, name, email, status, created_at FROM t_user ORDER BY created_at DESC LIMIT #{pageSize} OFFSET #{offset} /selectoffset 的计算由业务代码完成。这种方式可控、性能可预期而且在大数据量下更容易做优化。还有一个深层翻页优化的技巧如果页数很深直接用 OFFSET 100000 这种写法数据库要扫描前面十万行才能返回结果性能会很差。常见做法是改成基于游标的分页方式用 id lastMaxId 或者 created_at lastMaxTime 来切分这就是另一种思路了。5. 缓存、日志与性能优化别等线上才补课5.1 MyBatis 一级缓存与二级缓存的真实情况MyBatis 缓存是面试题常客也是日常开发里最容易产生“我以为它生效了其实没有”的错觉的地方。一级缓存是 SqlSession 级别的。同一个 SqlSession 中执行相同的查询第二次会直接走缓存不查库。但在 Spring Boot 整合之后默认情况下我们每次通过 Mapper 接口执行方法SqlSessionTemplate 都会新建一个 SqlSession执行完就关闭。也就是说不开启事务时一级缓存本质上不生效因为 SqlSession 生命周期太短。唯一能让一级缓存发挥作用的方式是加 Transactional让同一个事务内共享同一个 SqlSession。二级缓存是 Mapper 级别的跨 SqlSession 共享。开启方式很直接在 Mapper XML 里加一行cache/然后实体类要实现 Serializable否则序列化缓存会报错。开启后同一个 namespace 下所有查询结果都进二级缓存其他 SqlSession 也能读到。但是二级缓存在实际生产里问题很多。首先是脏读问题如果两个 Mapper 操作同一张表一个开启了缓存、另一个没开或者 namespace 不同一方更新数据后另一方缓存里的旧数据照样被读出来。其次是缓存命中率问题只要有任何更新操作MyBatis 默认会清空整个 namespace 的缓存在写多读少的业务里缓存基本白开。我的结论是MyBatis 二级缓存适合那种极其稳定、极少更新的配置型数据比如数据字典、行政区划表、系统参数表。业务数据表就用业务层缓存也就是 Spring Cache 或者 Redis可控性高得多。5.2 SQL 日志打印与慢 SQL 定位开发阶段我强烈建议把 MyBatis SQL 日志打出来。两种方式一种是前面配置里写过的 StdOutImpl把 SQL 直接打到 stdout另一种是按 Mapper 包设置 debug 日志级别logging: level: com.example.demo.mapper: debug这种方式更精细生产环境也可以保留配合日志采集系统能记录每个 Mapper 方法的实际 SQL。但要注意日志里会打印完整的 SQL 参数值包括用户姓名、手机号这类敏感信息生产环境一定要做脱敏或者用占位符方式记录。慢 SQL 定位也不能只靠应用日志。PostgreSQL 端有两个插件非常有用auto_explain 和 pg_stat_statements。在 postgresql.conf 里配置shared_preload_libraries auto_explain,pg_stat_statements重启 PostgreSQL 后设置ALTER SYSTEM SET auto_explain.log_min_duration 500ms; ALTER SYSTEM SET pg_stat_statements.track ALL;然后执行 pg_reload_conf()。之后所有执行超过 500ms 的 SQL 都会被记录到 PostgreSQL 日志pg_stat_statements 视图能按总耗时、调用次数、平均耗时排序一眼找到慢查询。这套组合拳比单纯肉眼扫应用日志高效得多。5.3 业务级缓存接入Caffeine 和 Spring Cache业务数据想加缓存我首选 Spring Cache 配合 Caffeine本地缓存方案足够应对大多数单体应用。配置很简单spring: cache: type: caffeine caffeine: spec: maximumSize10000,expireAfterWrite5m然后在 Service 方法上加注解Cacheable(value userList, key #query.toString()) public ListUser listUsers(UserQuery query) { return userMapper.findByCondition(query); }缓存 key 的设计是个技术活。最简单粗暴的是把整个查询对象作为 key但如果对象里有时间戳、页码、随机数这种字段缓存命中率会非常低。更好的方式是把查询条件拆出来拼成规范化字符串或者只缓存热点数据比如按 id 缓存单条记录。Caffeine 的 expireAfterWrite 是写入后过期这个语义很好理解适合读多写少的静态数据。我为什么不用 MyBatis 二级缓存做这件事核心原因是缓存维度的问题。MyBatis 二级缓存锁死在 Mapper namespace 上一旦涉及多表关联查询或者跨 Mapper 的写操作缓存一致性很难保证。而业务缓存可以控制 Service 层粒度更粗也更容易配合失效逻辑。6. 常见问题排查与避坑实录6.1 连接类问题服务起不来、连不上、认证失败这套组合里90% 的“数据库连不上”问题出在 PostgreSQL 的配置层面应用代码反而很少出问题。我按排查路径列一下典型场景。第一个是服务本身没起来。Linux 上执行 systemctl status postgresql-17Windows 上打开服务管理器看 PostgreSQL 服务状态没起来就先看日志。日志里最常见的错误是没有权限访问数据目录。CentOS 上装完 PG 后数据目录默认是 /var/lib/pgsql/17/data属主必须是 postgres 用户如果你手动改过目录权限导致属主不对把属主改回去再重试。第二个是网络层连不通。远程连不上时先确认两件事postgresql.conf 里 listen_addresses 是否包含了服务器 IP 或者 *pg_hba.conf 里是否配置了允许该来源 IP 的认证规则。默认 pg_hba.conf 只允许 local 和 127.0.0.1外部连接会被直接拒绝。改完后要 reload 配置记住是 reload 不是 restartreload 不中断现有连接。第三个是认证失败。错误信息一般是 password authentication failed for user xxx。这时候要检查 pg_hba.conf 里的认证方式常见的 scram-sha-256 是加盐哈希如果之前用明文密码建的用户在 PG 14 之后默认无法用明文认证。还有一种情况是数据库角色本身密码为空重新 ALTER ROLE user WITH PASSWORD 一下就好。第四个是 JDBC 连接参数中的 SSL 报错。PostgreSQL JDBC 驱动默认会尝试 SSL 连接如果服务端不支持或者配置不对会报 sslmode 相关错误。开发环境直接在连接串后面加 ?sslmodedisable 就能消除但生产环境要按安全要求配置 SSL。6.2 映射类问题时间字段、NULL 处理、PostgreSQL 特有类型时间字段映射是 MyBatis 跨数据库时最闹心的问题。在 PostgreSQL 里TIMESTAMP 映射到 Java 的 LocalDateTime 很顺畅。但有几个细节要注意。第一PostgreSQL 的 timestamp with time zone也就是 timestamptz 类型映射到 LocalDateTime 时可能会因为时区产生偏差。解决的思路是连接串里显式指定时区jdbc:postgresql://localhost:5432/mydb?timezoneAsia/Shanghai第二MyBatis 在设置 null 参数时默认会把它当作 JDBC 的 OTHER 类型PostgreSQL 对这种歧义类型很敏感有时会报“无法确定参数的数据类型”。解决办法是在 XML 里写 #{createdAt, jdbcTypeTIMESTAMP}传入 null 时指定明确的 JDBC 类型。更全局的做法是在 MyBatis 配置里加 jdbc-type-for-nullNULL但这个配置在某些场景会引起反向问题我建议按字段单独处理。第三Oracle 用 MyBatis 查询时间映射容易踩的坑是 DATE 类型Oracle 的 DATE 其实是带时分秒的跟 MySQL 和 PostgreSQL 的 DATE 语义不一样映射到 Java 的 java.util.Date 时要小心。如果你维护过 Oracle 迁移到 PostgreSQL 的项目这一步一定要整理一份字段类型映射对照表逐个验证。还有一个热词问题MyBatis 能支持 Gauss 这类兼容 PostgreSQL 协议国产数据库吗。答案是可以尝试但必须测试。这类数据库大多兼容 PostgreSQL 的驱动协议应用层可以换驱动但具体的 JSON 函数、主键自增方式、特殊类型支持各有差异MyBatis 的方言和 TypeHandler 可能都需要微调。我的建议是选型阶段拿真实 schema 和 SQL 跑一遍兼容性测试别只看宣传。6.3 性能问题批量插入慢与分页查询慢批量插入是日常高频坑。同样的代码插 1000 条数据MySQL 下还好PostgreSQL 下单条 insert 循环能给你干到几秒起步。除了用 foreach 多值插入之外还有两个能明显提性能的点。第一个是 JDBC URL 加参数jdbc:postgresql://localhost:5432/mydb?reWriteBatchedInsertstrue这个参数对使用 PreparedStatement 的 executeBatch 批量插入有效。PostgreSQL 默认批量插入时驱动还是会逐条发送给服务器开了这个参数后驱动会把多条 INSERT 合并成一条多值 INSERT 发送性能提升非常明显。第二个是 MyBatis 的 ExecutorType。如果用的是 SqlSessionTemplate 且调用了 insert 太多次可以考虑把 defaultExecutorType 设成 BATCH。但要注意Batch 模式下MyBatis 的更新操作不会立即执行要等批量刷新才会发送到数据库所以查询同一个事务里刚插入的数据可能查不到这种模式适合纯插入场景不适合混用读写。分页查询慢的问题我在前面提过两种方案一是把 OFFSET 深翻页改成游标分页二是给排序字段建合适的索引。还有一个细节LIMIT 和 OFFSET 的值在 MyBatis 里如果用 #{}, 参数类型一定要对PostgreSQL 的 LIMIT 是 bigint 类型别传成字符串否则数据库可能隐式转换导致索引失效。6.4 其他高频搜索问题速查结合各类高频搜索热词我整理了一张常见问题速查表方便直接对照。问题可能原因解决思路PostgreSQL 服务启动失败端口占用 / 数据目录权限不对netstat 查端口检查目录属主Spring Boot 启动报 Failed to determine a suitable driver classJDBC URL 配置错误检查 spring.datasource.url 前缀连接报 no pg_hba.conf entry客户端 IP 没被允许修改 pg_hba.conf 并 reload执行 SQL 报 column X does not exist大小写问题PostgreSQL 对双引号敏感SQL 里字段名全小写插入时主键冲突自增序列不同步检查序列 nextval 值必要时 setvalMyBatis 查询不到数据但不报错表名或字段大小写不匹配确认实际表结构的字段名查询字段类型为 jsonb 报错缺少 TypeHandler自定义 JsonNodeTypeHandler批量插入很慢JDBC URL 缺参数 / 单条插入开 reWriteBatchedInserts用 foreach时间字段相差 8 小时时区不一致连接串加 timezone确认数据库时区内存缓存不生效一级缓存被每次 SqlSession 重置开启事务或使用业务级缓存7. 从真实项目场景看这套组合的价值7.1 典型业务系统里的数据访问模式搜热词时会看到很多真实项目标题比如“基于 Spring Boot 的企业办公用品管理系统的设计与实现”“Spring Boot 设计题目商城”“Spring Boot 餐饮 SaaS AI 集成”。这些项目形态各异但数据访问模式高度相似。管理系统核心是 CRUD 加审批流。办公用品管理就是用品分类、库存管理、领用申请、审批记录、统计报表这一套。这类系统的复杂 SQL 集中在报表查询和汇总统计上MyBatis 手写 SQL 的优势在这里非常突出你可以直接写 PostgreSQL 的窗口函数做分组排名也可以写物化视图刷报表数据不用被 ORM 的 API 束缚。商城类系统的关键在事务。下单时要扣库存、生成订单、扣减账户余额多个写操作必须在同一个事务里任何一个失败都要整体回滚。Spring Boot 的 Transactional 加上 MyBatis 的 SqlSessionTemplate 能保证同一个事务内复用同一个 SqlSession这正好也是一级缓存能起作用的时候。同时PostgreSQL 的行级锁、唯一约束在并发下单场景里是库存不超卖的重要保障。餐饮 SaaS 加 AI 的场景更加现代。菜单、订单、门店信息往往结构多变PostgreSQL 的 JSONB 字段可以灵活存储AI 生成菜品推荐或者智能库存预测最终也要落到结构化查询上。这样的项目特别适合 Spring Boot 做 API 层MyBatis 做数据层PostgreSQL 兼顾结构化数据和非结构化数据。这些项目的共同特点是把数据库当作核心资产。选 MyBatis 不选 JPA不是因为 JPA 不行而是因为当你对 SQL 有强控制需求时手写 SQL 比生成 SQL 更贴近业务。7.2 可扩展的方向代码生成器、多租户与数据同步这套组合项目走向生产之后有几个高频扩展方向。代码生成器是提升团队效率的利器。MyBatis Generator 或者新版 MyBatis Code Generator 可以根据数据库表结构自动生成实体类、Mapper 接口、XML 文件然后把生成的代码作为起点手工调整复杂 SQL。配合 MyBatis-Plus 也能自动生成一套基础的 CRUD 方法如果你不想重复写简单的单表操作用 MyBatis-Plus 还能进一步简化。但要注意MyBatis-Plus 的 LambdaQueryWrapper 虽然省代码但在复杂查询上我还是建议走 XML可读性和可调优性都好很多。多租户是 SaaS 项目绕不开的话题。实现方案分两种PostgreSQL 的 schema 隔离和行级 Tenant 字段隔离。schema 隔离每个租户一张独立表结构安全隔离性好但数据迁移和连接管理复杂行级隔离实现简单所有租户数据在同一张表里靠 tenant_id 区分开发成本低但需要在所有查询上加过滤条件防止串租户数据。用 MyBatis 做多租户的话可以使用拦截器统一添加租户条件避免业务代码里到处写 tenant_id。数据同步则是数据库运维层的问题。PostgreSQL 原生的逻辑复制可以把数据实时同步到备库用于读写分离和容灾。如果需要把数据同步到其他数据库或者消息队列可以用 Debezium 这类 CDC 工具它的原理是读取 PostgreSQL 的逻辑复制流。反过来如果你要做增量同步软件选型核心要点是是否支持 PostgreSQL 逻辑复制、是否能处理 DDL 变更、延迟是多少、是否支持数据回放。没有一层不变的最佳方案只有按业务取舍。7.3 一些个人实践心得写到最后分享几个我自己长期使用这套组合沉淀下来的习惯。项目初始化时我永远先把版本组合钉死。Spring Boot 版本、mybatis starter 版本、JDBC 驱动版本、PostgreSQL 大版本写进 README 的“技术栈清单”里。这个动作看起来简单但能帮团队省掉大把的依赖冲突排查时间。数据库 schema 一定要纳入版本管理。就算项目只有一个人开发也建议用 Flyway 管理 SQL 变更脚本。V1__init.sql、V2__add_user_role.sql 这种文件命名按顺序执行每次部署环境一变数据库结构永远和代码同步。我见过太多项目数据库结构和代码对不上最后只能靠人肉比对这是最让人崩溃的维护方式。还有一个小技巧开发环境连接串里显式把 log 打开第一次跑通时把日志里打印的 SQL 复制到 pgAdmin 或者 DBeaver 里执行确认 SQL 本身没问题。这能快速定位是 SQL 写错还是 MyBatis 配置错误。DBeaver 和 pgAdmin 都是开源免费工具完全够用不需要去找什么商业客户端的破解版一旦用了没保障的东西后果可能比付费更严重。这套 Spring Boot MyBatis PostgreSQL 的组合我已经在多个项目里验证过它的稳定性和可维护性。你把它学会之后不管是你自己的毕设、公司内部管理系统还是长期迭代的商用产品都能稳稳拿住。希望这篇文章里的配置、代码和排障经验能让你少走我当年走过的那些弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑