简介:面向Java开发者的Spring Boot集成GBase 8s入门示例,结合MyBatis框架演示国产分布式数据库在微服务项目中的实际集成步骤,适合需要快速掌握GBase 8s连接配置、数据源管理与持久化操作的初中级开发者参考。压缩包共33个文件,涵盖Java源码、MyBatis映射XML、配置文件、编译生成的class及JDBC驱动jar等,整体大小约24.37MB,目录结构保留标准Maven工程布局,便于直接导入分析。已有1485人学习下载,具备不错的参考价值。项目代码完整展示了从添加依赖、配置数据源到编写Mapper接口与SQL映射的全过程,包含MyBatis逆向工程自动生成的实体类、Mapper接口及XML配置文件;通过运行调试可直观理解GBase 8s的SQL语法差异、特定异常处理方式,以及Spring Boot与MyBatis协同工作的关键细节,为国产数据库集成实践提供了清晰范本。
1. Spring Boot 集成 GBase 8s:这个 demo 帮你把驱动、方言、主键回填一次跑通
把一个跑了很久的 Spring Boot 项目从 MySQL 迁到 GBase 8s,第一关就死在驱动上:jar 装不进 Maven 仓库、URL 写了连不上、自增主键怎么都回填不了。这份 springboot 集成 gbase8s 的 demo,就是一个把连接、CRUD、分页、主键回填全部跑通的最小可运行工程,用的是 Spring Boot + MyBatis 这套最常见的组合。它解决的问题很具体:GBase 8s 的 JDBC 驱动怎么进本地仓库、连接串怎么配、SQL 方言和 MySQL 差在哪、自增列为什么拿不到返回值。适合正在做信创改造、国产数据库替代,或者接到一个“把项目从 MySQL 迁到 GBase 8s”任务的 Java 后端。跟着下面的步骤,从空目录到一个能 curl 通的接口,大概半小时。
2. 连接层适配:GBase 8s 的驱动、URL 参数与连接池配置
2.1 驱动包先装进本地仓库:mvn install:install-file 是第一步
GBase 8s 的 JDBC 驱动不在这两个中央仓库里,第一次拉依赖必失败。驱动文件一般叫 ifxjdbc.jar 或者 gbasedbt-jdbc.jar,随数据库安装包一起分发,或者找 DBA 要。拿到 jar 之后不能直接丢进项目 lib 目录,正规做法是先装进本地 Maven 仓库,再在 pom.xml 里按坐标引用。
mvn install:install-file -Dfile=ifxjdbc.jar -DgroupId=com.gbasedbt -DartifactId=gbasedbt-jdbc -Dversion=8.8.1 -Dpackaging=jar装完之后 pom.xml 里写:
<dependency> <groupId>com.gbasedbt</groupId> <artifactId>gbasedbt-jdbc</artifactId> <version>8.8.1</version> </dependency>装之前先用jar tf ifxjdbc.jar | grep IfxDriver看一眼驱动类全路径,不同版本差异很大:老版本是com.informix.jdbc.IfxDriver,新版本改成了com.gbasedbt.jdbc.IfxDriver。这个类名后面配置 driver-class-name 要用,写错直接 ClassNotFoundException。另外确认一下 jar 是 JDBC 4 以上版本,否则需要手动 Class.forName 注册驱动,Spring Boot 自动装配不会理你。
2.2 URL 写法与关键参数:GBASEDBTSERVER 不能漏
GBase 8s 的连接串和 MySQL 完全不是一个套路,格式是这样的:
jdbc:gbasedbt-sqli://192.168.10.20:9088/testdb:GBASEDBTSERVER=gbase_server;DB_LOCALE=zh_CN.utf8;CLIENT_LOCALE=zh_CN.utf8MySQL 用?后跟useSSL=false&serverTimezone=Asia/Shanghai那种键值对,GBase 8s 用冒号后直接跟参数、分号分隔多个参数。默认端口是 9088(不是 3306 也不是 5432)。中间那段testdb是库名,库名后面那个GBASEDBTSERVER是数据库实例的服务名,配置文件里DBSERVERNAME配的那个值,漏掉它有的版本能连、有的版本报Database server name missing,这种玄学问题最耗时间。
参数主要关注这几个:
| 参数 | 作用 | 典型值 |
|---|---|---|
| GBASEDBTSERVER | 数据库实例服务名 | 安装时配置的 DBSERVERNAME |
| DB_LOCALE | 数据库字符集 | zh_CN.utf8 / en_US.utf8 |
| CLIENT_LOCALE | 客户端字符集 | zh_CN.utf8,和上面保持一致 |
| IFX_USE_STENCACHE | 游标缓存 | 1 开启,分页查询性能会好一些 |
把上面的 URL 拆开看:gbasedbt-sqli是协议名,GBase 8s 有两套协议,sqli 是标准 SQL 接口;9088是默认服务端口,如果 DBA 给的是别的端口,以实际为准;GBASEDBTSERVER后面的值大小写都能传,但必须和数据库端 sqlhosts 里的配置一致。
2.3 application.yml 里数据源和 MyBatis 怎么配
驱动和 URL 都确认了,spring.datasource 这块照抄下面的结构:
spring: datasource: url: jdbc:gbasedbt-sqli://192.168.10.20:9088/testdb:GBASEDBTSERVER=gbase_server;DB_LOCALE=zh_CN.utf8;CLIENT_LOCALE=zh_CN.utf8 driver-class-name: com.gbasedbt.jdbc.IfxDriver username: gbasedbt password: gbasedbt hikari: connection-test-query: SELECT 1 FROM systables minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: trueHikari 的connection-test-query兼容性极佳,优先级高于 JDBC4 的 isValid(),建议加上。SELECT 1 FROM systables是 GBase 8s 里最轻量的探活语句,systables 是系统表,每一条 insert/update 用它做连接测试都能通过。map-underscore-to-camel-case必须开,否则 create_time 映射不到 createTime,你会在查询结果里看到 createTime 为 null。端口、服务名、账号权限这三项只要一项不对,启动就会卡在数据源初始化,日志里如果出现Connection is not available, request timed out,优先怀疑网络和 GBASEDBTSERVER。
3. MyBatis 接入:建表 SQL、Mapper 接口与 XML 映射
3.1 建表 SQL:SERIAL 自增和 DATETIME 是 GBase 8s 的默认姿势
Demo 里要跑 CRUD,先建一张用户表。GBase 8s 和 MySQL 建表语法有差异,自增列不叫 AUTO_INCREMENT,用 SERIAL 或 BIGSERIAL 替代,时间类型用 DATETIME YEAR TO SECOND 而不是 DATETIME(6)。
CREATE TABLE user_info ( id SERIAL, username VARCHAR(50) NOT NULL, email VARCHAR(100), create_time DATETIME YEAR TO SECOND DEFAULT CURRENT YEAR TO SECOND, PRIMARY KEY (id) );SERIAL 是 32 位自增,数据量预期大就把 id 换成 BIGSERIAL,Java 侧对应 Long;CURRENT YEAR TO SECOND等同于 MySQL 的 now(),但只在 INSERT 时求值,UPDATE 不会自动刷新,这点比 MySQL 的 ON UPDATE CURRENT_TIMESTAMP 弱。demo 里建表就这一张,足够验证接下来的所有问题。如果是从 MySQL 迁过来的老表,要把ENGINE=InnoDB AUTO_INCREMENT=... DEFAULT CHARSET=utf8mb4这类尾巴全部去掉,这些语法 GBase 8s 不认,建表时会直接在ENGINE附近报语法错误。
3.2 实体类与 Mapper 接口:字段对齐比想象中更重要
实体类用最常见的写法,createTime 用 LocalDateTime,GBase 8s 的 DATETIME YEAR TO SECOND 在 JDBC 层对应 java.sql.Timestamp,MyBatis 能自动转 LocalDateTime。
public class UserInfo { private Long id; private String username; private String email; private LocalDateTime createTime; // getters and setters }Mapper 接口不需要写 SQL,方法名和 XML 里的 id 对应就行:
public interface UserInfoMapper { UserInfo findById(Long id); List<UserInfo> findByPage(@Param("offset") int offset, @Param("limit") int limit); int insert(UserInfo userInfo); int update(UserInfo userInfo); int delete(Long id); }注意@Param("offset")和@Param("limit")必须显式写上,GBase 8s 的方言后面分页要用这两个参数,如果漏了 @Param,MyBatis 会报Parameter 'offset' not found。接口写到这里先停,XML 才是这段的主角。
3.3 XML 映射:主键回填必须用 selectKey
在 mapper/UserInfoMapper.xml 里写 CRUD,重点是 insert 里的主键回填。GBase 8s 的 JDBC 驱动对 getGeneratedKeys() 支持不完整,useGeneratedKeys="true" keyProperty="id"在 MySQL 上行得通,在 GBase 8s 上大概率返回 null 或 0,所以干脆不用,改用 selectKey 走 DBINFO 系统函数,这是 Informix 系数据库取自增值的标准做法。
<insert id="insert" parameterType="com.demo.entity.UserInfo"> INSERT INTO user_info (username, email, create_time) VALUES (#{username}, #{email}, CURRENT YEAR TO SECOND) <selectKey keyProperty="id" resultType="long" order="AFTER"> SELECT DBINFO('sqlca.sqlerrd1') FROM systables WHERE tabid = 1 </selectKey> </insert> <select id="findById" resultType="com.demo.entity.UserInfo"> SELECT id, username, email, create_time FROM user_info WHERE id = #{id} </select> <update id="update" parameterType="com.demo.entity.UserInfo"> UPDATE user_info SET username = #{username}, email = #{email} WHERE id = #{id} </update> <delete id="delete"> DELETE FROM user_info WHERE id = #{id} </delete>SELECT DBINFO('sqlca.sqlerrd1') FROM systables WHERE tabid = 1这一句通用性最强,INSERT 执行完之后再执行它,能拿到这次会话最新插入的 SERIAL 值。tabid = 1是系统表自身的行,固定值,不用改。order 必须是 AFTER,因为 GBase 8s 的自增值是在 INSERT 完成后才生成的。另外注意 VALUES 里 create_time 直接用数据库函数CURRENT YEAR TO SECOND填充,这样实体类里不传 createTime 也能写入。update 语句只更新 username 和 email,create_time 不动——DATETIME 字段在更新语句里如果重复赋 CURRENT YEAR TO SECOND,会把老数据的创建时间也刷成当前时间,这是个隐蔽的坑。
4. 分页与类型映射:FIRST/SKIP 方言、时间与布尔转换
4.1 分页没有 LIMIT:用 FIRST n SKIP m,顺序不能反
MyBatis 的 XML 里如果写LIMIT #{offset}, #{limit},在 GBase 8s 上直接语法错误。GBase 8s 的分页写法是SELECT FIRST n SKIP m,FIRST 在前、SKIP 在后,而且关键字位置固定,从 SELECT 后面开始。整句顺序是:SELECT → FIRST 行数 → SKIP 偏移量 → 列 → FROM,写反一个位置就报Syntax error near 'SKIP'。
<select id="findByPage" resultType="com.demo.entity.UserInfo"> SELECT FIRST #{limit} SKIP #{offset} id, username, email, create_time FROM user_info ORDER BY id </select>调用时参数顺序是 SKIP 对应 offset(跳过的行数),FIRST 对应 limit(取多少行)。比如查第三页,每页 10 条,offset=20,limit=10,实际执行的是SELECT FIRST 10 SKIP 20,语义是“跳过 20 行,取接下来 10 行”。这里有个惯性坑:MySQL 写LIMIT 20, 10,第一次写 GBase 8s 时容易把 FIRST 和 SKIP 的值写反,结果就是数据对不上。另外注意分页查询必须 ORDER BY,GBase 8s 不保证无排序时的返回顺序,没有 ORDER BY 的分页结果会时多时少,翻页重复或丢失,这不是数据库 Bug,而是没有排序键时的正常表现。
4.2 时间类型映射:DATETIME YEAR TO SECOND 与 LocalDateTime
查询出来的 create_time 是 DATETIME YEAR TO SECOND 类型,JDBC 返回 java.sql.Timestamp,MyBatis 默认能映射到 LocalDateTime,但前提是列的 jdbcType 要写对。上面分页查询的 XML 省略了 resultMap,直接用 resultType,当 GBase 8s 返回的列名和实体属性完全一致时没问题,一旦列名带下划线、属性名驼峰,就会映射失败。稳妥做法是显式写 resultMap:
<resultMap id="UserInfoResultMap" type="com.demo.entity.UserInfo"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="email" column="email"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> </resultMap>jdbcType="TIMESTAMP"是 MyBatis 给 JDBC 层的类型提示,GBase 8s 的 DATETIME 类型必须靠它才能走对转换分支。插入时如果实体类里 createTime 有值,占位符也要写 jdbcType:
INSERT INTO user_info (username, email, create_time) VALUES (#{username}, #{email}, #{createTime, jdbcType=TIMESTAMP})不写 jdbcType 的情况下,MyBatis 走默认推断,碰到 DATETIME YEAR TO SECOND 偶尔会报Invalid column type,写法上多带一个属性,回报是少排查半小时。
4.3 布尔类型:GBase 8s 没有真正的 BOOLEAN
MySQL 的 TINYINT(1) 或 BOOLEAN 在 GBase 8s 里要特别处理。GBase 8s 支持 BOOLEAN 关键字,但存储值是 't' / 'f' 两个字符,不是 0 / 1,MyBatis 的默认 BooleanTypeHandler 处理不了。两种解法:一是建表时用 SMALLINT 代替,Java 侧用 Integer 接收,0 和 1 判断交给代码;二是写自定义 TypeHandler,把 Java Boolean 转成 't' / 'f'。
@MappedTypes(Boolean.class) public class GbasedbtBooleanTypeHandler extends BaseTypeHandler<Boolean> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, Boolean.TRUE.equals(parameter) ? "t" : "f"); } @Override public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException { return "t".equals(rs.getString(columnName)); } @Override public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return "t".equals(rs.getString(columnIndex)); } @Override public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return "t".equals(cs.getString(columnIndex)); } }XML 里在布尔字段上声明 typeHandler 引用:
<result column="is_active" property="active" typeHandler="com.demo.handler.GbasedbtBooleanTypeHandler"/>如果表里还没有布尔字段,建议直接选 SMALLINT 方案,省掉 TypeHandler 的维护成本——GBase 8s 资料少,自定义类型处理器出了问题,排查时你会极度怀念 MySQL 的 TINYINT。
5. 避坑实录:GBase 8s 集成中我踩过的五个坑
5.1 驱动类找不到:ClassNotFoundException 的常见原因
现象:应用启动时抛java.lang.ClassNotFoundException: com.gbasedbt.jdbc.IfxDriver。
原因:一是驱动 jar 根本没进本地 Maven 仓库,pom 里坐标是假的;二是驱动类名写错,老版本 jar 里是com.informix.jdbc.IfxDriver,新版本才是com.gbasedbt.jdbc.IfxDriver,同名 jar 混用时最容易翻车;三是 jar 依赖 scope 写成了 provided 或 test,运行时不在 classpath。
解决:先用jar tf ifxjdbc.jar | grep Driver.class确认真实类路径,再核对 pom 依赖的 scope。替换驱动 jar 版本后,执行mvn clean package -DskipTests重新打一遍,确保没有旧 jar 残留在 target 目录。
5.2 LIMIT 语法报错:不是 MyBatis 的问题,是数据库方言问题
现象:分页 SQL 执行时报SQLException: Syntax error near "LIMIT"。
原因:代码从 MySQL 迁移过来,mapper XML 里还留着LIMIT #{offset}, #{limit},GBase 8s 语法解析器不识别 LIMIT 关键字。
解决:把整段分页 SQL 改成SELECT FIRST #{limit} SKIP #{offset},注意 FIRST 和 SKIP 的位置不能互换。如果项目里分页 SQL 特别多,最省事的方案是引入 MyBatis 分页插件 PageHelper,把 dialect 配成gbasedbt,插件会帮你把 Page 对象翻译成对应的 FIRST/SKIP 语句,但不推荐在 demo 阶段就上插件——先手工写一遍,你才知道底层 SQL 长什么样,后面接插件出了问题才排得动。
5.3 自增主键不回填:useGeneratedKeys 在 GBase 8s 上不靠谱
现象:insert 之后调用实体类的 getId() 返回 0,或者返回 null,但数据库里数据确实插进去了。
原因:GBase 8s 的 JDBC 驱动对getGeneratedKeys()的支持不完整,useGeneratedKeys="true" keyProperty="id"拿不到自动生成的自增值。
解决:放弃 useGeneratedKeys,改用<selectKey keyProperty="id" resultType="long" order="AFTER">,内部执行SELECT DBINFO('sqlca.sqlerrd1') FROM systables WHERE tabid = 1。我已经把这个写法固定成模板,每次写 GBase 8s 的 insert 都先声明 selectKey,不再依赖驱动层面的回填能力。
5.4 中文乱码:DB_LOCALE 与 CLIENT_LOCALE 不一致
现象:插入中文后查出来是??或者乱码,英文正常。
原因:数据库服务端的 DB_LOCALE 是 UTF-8,但 JDBC URL 里没指定 CLIENT_LOCALE,驱动走了默认字符集,两端编码不一致时,中文字符直接变成问号。
解决:在 JDBC URL 末尾追加;DB_LOCALE=zh_CN.utf8;CLIENT_LOCALE=zh_CN.utf8,两者必须同时设置且保持一致。改完之后如果是乱码数据已经入库,用 UPDATE 把脏数据重写一遍,单纯改连接参数不会修复历史数据。
5.5 连接池探活失败:HikariCP 的 isValid() 兼容性问题
现象:应用启动时偶尔报Connection is not available, request timed out after 30000ms,但数据库本身没挂,navicat 能连上。
原因:HikariCP 默认用 JDBC4 的Connection.isValid()做连接探活,部分 GBase 8s 的驱动版本对这个方法实现有缺陷,导致连接池认为连接全部失效,拒绝出借连接。
解决:在 Hikari 配置里显式指定connection-test-query: SELECT 1 FROM systables,让连接池用 query 方式探活,绕开 isValid()。附带把connection-timeout从默认 30 秒下调到 10 秒,失败时能更快暴露问题,缩短排查链路。
6. 验证与进阶:从 demo 跑到 PageHelper 方言适配
demo 跑起来之后,先做一次端到端验证,确认整条链路是通的。写一个最简单的 Controller:
@RestController @RequestMapping("/user") public class UserController { @Resource private UserInfoMapper userInfoMapper; @GetMapping("/{id}") public UserInfo get(@PathVariable Long id) { return userInfoMapper.findById(id); } @PostMapping public Long insert(@RequestBody UserInfo userInfo) { userInfoMapper.insert(userInfo); return userInfo.getId(); } }启动应用后,用 curl 验证插入和回填:
curl -X POST http://localhost:8080/user -H "Content-Type: application/json" -d '{"username":"test01","email":"test01@example.com"}'返回结果不为 0,说明 selectKey 生效。再查回来验证数据落库:
curl http://localhost:8080/user/1如果返回 JSON 里 createTime 有值、中文不乱码,整个集成链路就算跑通了。
接着把分页接到 PageHelper 上,解决项目里大批量分页 SQL 的手改成本。demo 阶段验证过 FIRST/SKIP 写法后,就可以放心引入插件,GBase 8s 有内置方言支持。
@Configuration public class MyBatisConfig { @Bean public PageInterceptor pageInterceptor() { PageInterceptor interceptor = new PageInterceptor(); Properties properties = new Properties(); properties.setProperty("dialect", "gbasedbt"); properties.setProperty("reasonable", "false"); interceptor.setProperties(properties); return interceptor; } }dialect 配成gbasedbt,插件会在执行前把 Page 对象翻译成FIRST n SKIP m,Service 层可以继续用 PageHelper.startPage,业务代码不用感知底层方言。从那以后,我每次对接国产数据库,都先花半天时间把最小 demo 跑通——驱动、URL、方言、主键回填这几个变量先锁死,再谈业务迁移。这招帮我挡掉了后面好多坑,希望帮到你。
本文还有配套的精品资源,点击获取