做业务系统最烦的一种情况,就是建表的人和后端写代码的人不是同一个。表早就建好了,某个字段是 bit 类型,等你写 Java 实体的时候,根本不知道拿什么去对应。去问 DBA,他说“就是 0 和 1 嘛”;你高高兴兴用 Boolean 一跑,插入直接报错;换成 Integer 也报错;最后靠字符串 "1" 稳住了。这种场景我在项目里遇到不止一次,尤其在瀚高数据库(HighGo)这类兼容 PostgreSQL 生态的数据库上,bit 类型和 Java 类型的对应,看起来简单,实际暗坑不少。
这篇文章专门讲清楚瀚高数据库中 bit 类型和 Java 代码类型怎么映射。我会先给一套可以直接套用的结论,再解释数据库里的 bit 到底是什么,接着把 Boolean、String、byte[]、BitSet 四种 Java 侧映射方式逐个写清楚,最后补充纯 JDBC、MyBatis、JPA 三种框架里的落地写法,以及我踩过的四个高频坑。你按自己场景找到对应章节,抄完代码基本就能用。
1. 先给结论:一套能直接照用的 bit 映射方案
1.1 三种场景三种写法,别再让 Boolean 背锅
先说结论。瀚高数据库的 bit 类型,在 Java 侧并没有一个“唯一正确答案”,映射方式完全取决于字段的业务含义和 bit 的长度。我一般按这三条规则定:
- 表结构是
bit(1),业务上表示开关、是否、激活状态这类二值语义,Java 侧用Boolean,但读写时统一通过"1"/"0"字符串过渡,不要直接依赖驱动的setBoolean自动转换。 - 表结构是
bit(n),n 大于 1,业务上通常是位掩码、权限位、特征标记,Java 侧用String最省心,值就是"00101100"这样的 01 串。如果要做位运算,再加一个BitSet。 - 表结构是
bit varying(n),变长位串,Java 侧依然推荐String或BitSet,不推荐Boolean,因为长度不是 1,布尔类型根本装不下。
用表格看更直观:
| 数据库类型 | 常见业务含义 | 推荐 Java 类型 | 不推荐 |
|---|---|---|---|
| bit(1) | 开关、是否、有效状态 | Boolean | int、char |
| bit(n)(n>1) | 权限位、掩码、特征位 | String | Boolean |
| bit varying(n) | 变长位串 | String 或 BitSet | Boolean、Integer |
| bytea | 二进制大对象 | byte[] | 和 bit 不是一回事 |
1.2 为什么“用字符串过渡”是瀚高上的保险做法
很多刚接触瀚高的人会困惑:Java 里最像 0/1 的不是 boolean 和 int 吗,为什么我推荐用 String?
原因有两个。第一,瀚高数据库从内核上继承了 PostgreSQL 的类型体系,bit 不是布尔类型,也不是整数类型,它就是一个位串类型。位串的输入格式是B'1010'、'1010'这样的 01 字符串,底层存储是按位打包的字节数组。JDBC 的文本协议把参数发给数据库时,如果传的是字符串"1"或"0",数据库能直接按 bit 输入格式解析;如果传的是 Javaboolean的true/false,很多驱动版本会生成文本"true"或"false",而 bit 类型不认识这两个词,直接报invalid input syntax for type bit。
第二,驱动版本之间存在行为差异。不同版本的 JDBC 驱动对 bit 的getObject返回类型并不完全一致,有的返回Boolean,有的返回String。与其跟驱动版本做斗争,不如统一约定:写入用setString传 01 串,读取用getString拿 01 串,其他类型都在这层字符串基础上转换。这套约定不依赖驱动内部实现,迁移数据库、升级驱动都不会炸。
2. bit 类型到底是个什么类型
2.1 定长 bit(n) 与变长 bit varying 的区别
瀚高的 bit 类型分为定长bit(n)和变长bit varying(n)两种。bit(8)表示固定 8 位,bit varying(8)表示最多 8 位、实际可以更短。它们在物理存储上都是按位紧凑存储的,bit(8)占 1 个字节,bit(16)占 2 个字节,依此类推。变长类型还需要额外的长度信息,但这点不需要我们关心。
看一个建表例子:
create table user_flag ( user_id bigint primary key, is_active bit(1) default B'0', authority bit(8), metadata bit varying(32) ); insert into user_flag (user_id, is_active, authority, metadata) values (1001, B'1', B'00101100', B'101');这里有几个细节值得注意:
B'1'是标准的位串字面量写法,等价于'1',但语义更明确。authority是bit(8),插入B'00101100'就是 8 位,长度正好。metadata是bit varying(32),插入B'101'后实际就是 3 位,不会自动补零,也不会报错。
定长 bit 有一个容易忽略的补零规则:把一个长度不足的 01 串写入定长bit(n)时,数据库会在右侧补零。比如'1100'写入bit(8),实际存储的是11000000,不是00001100。这一点后面讲坑位的时候会专门展开。
2.2 与 boolean、smallint 的关键区别
这三个类型经常被人搞混:boolean、smallint、bit(1)都能表示两个值,但在瀚高的类型体系里完全不同。
boolean只有true和false,外加一个NULL。它没有长度的概念,JDBC 里对应的 Java 类型就是Boolean。smallint是数值类型,取值范围是 -32768 到 32767,可以参与算术运算,能比较大小。bit(1)是长度为 1 的位串,输入只能是B'0'或B'1',不能接true/false,也不能接1/2,更不能做加减乘除。
如果业务字段只是“是/否”,用bit(1)没问题,但要注意它和boolean在 SQL 写法上的差异。比如判断一个boolean字段:
select * from user_flag where is_active = true;而判断bit(1)字段,写成:
select * from user_flag where is_active = B'1';如果你用where is_active = true,瀚高会报类型不匹配的错误。很多从 MySQL 迁过来的同学在这里第一反应就是= 1或者= true,都是坑。最稳的写法是= B'1'或= '1'。
3. Java 侧四种映射方式逐个说透
3.1 Boolean:只推荐用在 bit(1)
bit(1)配Boolean是最直觉的组合,但落地时读写方式要注意。
插入时不要依赖setBoolean。我见过太多人这么写:
PreparedStatement ps = conn.prepareStatement( "insert into user_flag (user_id, is_active) values (?, ?)"); ps.setLong(1, userId); ps.setBoolean(2, active); // 错误示范,bit 列不接受 true/false 文本 ps.executeUpdate();实际跑起来大概率报错,错误信息类似invalid input syntax for type bit: "true"。正确做法是用setString传"1"或"0":
PreparedStatement ps = conn.prepareStatement( "insert into user_flag (user_id, is_active) values (?, ?)"); ps.setLong(1, userId); ps.setString(2, Boolean.TRUE.equals(active) ? "1" : "0"); ps.executeUpdate();读取时也不要直接rs.getBoolean。虽然某些驱动版本能从 bit 列解析出 Boolean,但解析规则在NULL时会变成false,会把空值和假值混在一起。更稳的读法是:
String s = rs.getString("is_active"); Boolean active = s == null ? null : "1".equals(s);这样能精确区分NULL、"0"、"1"三种状态。
3.2 String:通用性最强的选择
不管bit(n)还是bit varying(n),String都是最通用的 Java 映射类型。理由很简单:数据库内部就是按 01 串输入和输出的,字符串和位串之间是最直接的对应,不经过任何类型转换。
读取:
String authority = rs.getString("authority"); // 结果形如 "00101100"插入:
PreparedStatement ps = conn.prepareStatement( "insert into user_flag (user_id, authority) values (?, ?)"); ps.setString(2, "00101100"); ps.executeUpdate();如果字段本身允许NULL,用setString插入空值也很自然:
if (authority == null) { ps.setNull(2, java.sql.Types.VARCHAR); } else { ps.setString(2, authority); }唯一要注意的是写入前自己校验格式:必须是 0/1 字符组成的串,长度要符合字段定义。这个校验放在 Java 侧更省事,省得让数据库报错再排查。
3.3 byte[]:性能场景的选择
如果你的 bit 字段长度很大,比如几十上百位,用String会有内存开销,可以考虑byte[]。但我给个忠告:除非你有明确的性能诉求,否则不要一上来就用byte[],因为位序问题很容易把人绕晕。
举个例子,数据库里的bit(8)值是11000000,如果你希望它对应 Java 里字节的最高位为 1,那就是十六进制0xC0。把字符串转字节时,每 8 个字符一组,第一个字符对应最高位:
public static byte[] bitStringToBytes(String bitStr) { int len = bitStr.length(); byte[] data = new byte[(len + 7) / 8]; for (int i = 0; i < len; i++) { if (bitStr.charAt(i) == '1') { data[i / 8] |= (byte) (0x80 >> (i % 8)); } } return data; }读取时恰恰相反,我强烈不建议直接用rs.getBytes()去取 bit 列,不同驱动版本的返回格式并不统一,有的甚至会返回底层存储的原始字节,和字符串转出来的字节顺序不一致。保险做法是先getString拿到 01 串,再转成自己需要的byte[]:
String s = rs.getString("permission"); byte[] bytes = bitStringToBytes(s);3.4 BitSet:位运算业务的最爱
如果你要做的不是存取,而是权限判断、特征比对这类位运算,BitSet是 Java 侧最顺手的类型。但 BitSet 的索引方向和数据库位串的书写方向正好相反,这是最容易出错的地方。
数据库里bit(8)的"00101100",最左边是最高位。而java.util.BitSet的get(0)表示的是最右边一位,也就是最低位。所以从字符串解析到 BitSet 时,要倒着填:
public static BitSet bitStringToBitSet(String bitStr) { int n = bitStr.length(); BitSet bits = new BitSet(n); for (int i = 0; i < n; i++) { if (bitStr.charAt(n - 1 - i) == '1') { bits.set(i); } } return bits; }反向输出时也要注意,从高位往低位拼:
public static String bitSetToString(BitSet bits, int size) { StringBuilder sb = new StringBuilder(size); for (int i = size - 1; i >= 0; i--) { sb.append(bits.get(i) ? '1' : '0'); } return sb.toString(); }如果业务上约定get(0)表示第一个权限位,那你也可以定义自己的顺序,但必须在团队里形成统一约定,并且把注释写清楚。我在项目里见过两次因位序反了导致权限校验全部失灵的故障,都是因为一个从左往右填、一个从右往左填。
3.5 四选一的决策流程
把四种方式凑到一起,选型的思路其实很简单:
- 字段长度是 1,业务是二值语义,选
Boolean。 - 字段是多位,以存取为主,选
String。 - 字段是多位,需要频繁位运算,选
BitSet,数据库往返仍用字符串。 - 字段超长且对内存敏感,选
byte[],数据库往返仍建议用字符串兜底。
没有特殊理由,不要选byte[]作为默认方案,字符串的可读性和排错便利性是字节数组比不了的。
4. 实战落地:纯 JDBC、MyBatis、JPA 三个场景
4.1 纯 JDBC 的读取兼容写法
如果你的项目还在用原生 JDBC,建议读取时先getObject,再做一次类型兼容判断,避免驱动版本差异带来的问题:
Object obj = rs.getObject("is_active"); Boolean active; if (obj instanceof Boolean) { active = (Boolean) obj; } else if (obj != null) { active = "1".equals(String.valueOf(obj)); } else { active = null; }这样无论驱动返回的是Boolean还是String,都能正确处理。插入统一用setString,天然兼容。
4.2 MyBatis 自定义 TypeHandler
MyBatis 里处理 bit 最优雅的方式是写一个 TypeHandler。这里以bit(1)映射Boolean为例:
import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @MappedTypes(Boolean.class) @MappedJdbcTypes(JdbcType.BIT) public class BooleanBitTypeHandler extends BaseTypeHandler<Boolean> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter ? "1" : "0"); } @Override public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException { String s = rs.getString(columnName); return s == null ? null : "1".equals(s); } @Override public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String s = rs.getString(columnIndex); return s == null ? null : "1".equals(s); } @Override public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String s = cs.getString(columnIndex); return s == null ? null : "1".equals(s); } }然后在 Mapper XML 里声明:
<resultMap id="userFlagMap" type="com.example.UserFlag"> <id column="user_id" property="userId"/> <result column="is_active" property="active" typeHandler="com.example.BooleanBitTypeHandler"/> </resultMap> <insert id="insertUserFlag"> insert into user_flag (user_id, is_active) values (#{userId}, #{active, typeHandler=com.example.BooleanBitTypeHandler}) </insert>如果你嫌 XML 里写类名太长,还可以在 MyBatis 配置里全局注册:
<typeHandlers> <typeHandler handler="com.example.BooleanBitTypeHandler"/> </typeHandlers>注册后,所有Boolean属性映射到BIT列都会自动走这个处理器。
4.3 JPA / Hibernate 用 AttributeConverter
JPA 里处理 bit 推荐用AttributeConverter,把实体里的Boolean转成字符串"1"/"0":
import javax.persistence.AttributeConverter; import javax.persistence.Converter; @Converter public class BooleanToStringConverter implements AttributeConverter<Boolean, String> { @Override public String convertToDatabaseColumn(Boolean attribute) { return attribute == null ? null : (attribute ? "1" : "0"); } @Override public Boolean convertToEntityAttribute(String dbData) { return dbData == null ? null : "1".equals(dbData); } }实体字段上标注:
@Entity public class UserFlag { @Id private Long userId; @Column(name = "is_active", columnDefinition = "bit(1)") @Convert(converter = BooleanToStringConverter.class) private Boolean active; // 其他字段与方法省略 }这里有一个很重要的提醒:@Convert会把 Java 侧类型从Boolean变成String,如果你的实体没有加columnDefinition = "bit(1)",Hibernate 生成 DDL 时很可能会建出一个varchar列。所以只要表结构是手工维护的,就必须把columnDefinition写清楚;如果依赖 Hibernate 自动建表,那就更要注意了。
5. 高频报错与排查经验
5.1 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
插入时invalid input syntax for type bit: "true" | 用了setBoolean,文本协议发送 true/false | 改为setString传 "1"/"0" |
getObject返回类型和预期不一致 | 驱动版本对 bit 映射不同 | 用getString或instanceof兼容处理 |
| bit(8) 写入 "1100" 后读出来是 "11000000" | 定长 bit 右侧自动补零 | 写入前自己补满长度,或按定长语义处理 |
getBoolean读 NULL 变成 false | JDBCgetBoolean对 NULL 返回 false | 先getString判断是否为 NULL |
bit 列查询用= true或= 1报错 | bit 是位串类型,不是布尔或整数 | 使用= B'1'或= '1' |
| BitSet 权限校验结果全反 | Java 索引方向和数据库位序不一致 | 按高位到低位规则转换,统一约定 |
5.2 坑位一:setBoolean 到 bit 列报 invalid input syntax
最典型的问题是插入报错。前面已经说过根因,这里补充一个排查技巧:看到invalid input syntax for type bit时,一定要看具体的错误值。如果是"true"或"t",基本就是布尔直接写到了 bit 列;如果是"2",那就是有人用 int 传了 0 之外的数字;如果是乱码,考虑是不是驱动二进制协议和位串格式不一致。
遇到这类问题,最快定位方法就是打开驱动日志,看一下实际发送到数据库的 SQL 参数值。如果参数是true,把它改成"1"问题就解决了一半。
5.3 坑位二:定长 bit(n) 的右补零和截断
定长 bit 的补零规则是右补零。把"1100"写入bit(8),实际变成"11000000"。这一点在数据迁移、Excel 导入、接口对接时特别容易出问题。
比如外部系统传一个权限串"1100",你直接插入bit(8)字段,再读出来就变成了"11000000"。如果对方语义里"1100"是前四位权限,那结果是对的;如果对方语义是后四位权限,那这个数据已经完全变样了。
建议:在写入bit(n)之前,自己在 Java 侧把字符串补齐到 n 位。补零方向要和业务约定一致,一般按位串左高右低的习惯,右侧补零:
public static String padRight(String bitStr, int n) { if (bitStr.length() >= n) { return bitStr.substring(0, n); } StringBuilder sb = new StringBuilder(bitStr); while (sb.length() < n) { sb.append('0'); } return sb.toString(); }超过 n 位的字符串写入定长 bit 还可能报 bit string too long,这是数据库在保护你的数据,不要用 substring 硬截断,先确认业务上是不是该用bit varying。
5.4 坑位三:NULL 和 B'0' 不能混为一谈
很多表设计喜欢给bit(1)加默认值:
is_active bit(1) default B'0'这种情况下,插入时不传入is_active,数据库会存B'0'。但如果你显式传一个 Javanull,存进去就是NULL,不是B'0'。
查询时这俩差别很大:
select * from user_flag where is_active is null; select * from user_flag where is_active = B'0';前一条会查出显式传 NULL 的行,后一条查出默认值 0 的行。业务上如果“未设置”和“关闭”语义不同,那 NULL 和 B'0' 可以并存;如果语义相同,建议在写入层统一处理,不要产生 NULL。
读取时,用getString就能区分:
String s = rs.getString("is_active"); if (s == null) { // NULL } else if ("1".equals(s)) { // 启用 } else { // 关闭 }5.5 坑位三:位序是怎么把人绕晕的
这个坑值得单独再说一次。数据库位串"00101100",最左边是第 7 位(最高位),最右边是第 0 位。很多 Java 开发者习惯从左往右编号,于是把字符串第 0 个字符对应到 BitSet 的 index 0,结果权限校验全部错位。
我建议在代码里固定一个位序常量注释,比如:
// 数据库位串左高右低,BitSet index 0 对应最右边一位然后在转换函数里写清楚方向。实测中出现位序问题的项目,最后都不是靠临时改索引修好的,而是把转换函数统一收敛到一个工具类里,全项目只走这一个入口。
6. 我对这套映射的长期建议
这套映射方案我在实际项目里用了很久,最后的体会是:与其纠结“bit 对应 Java 什么类型”,不如在建表阶段就把类型选对。如果你业务只是开关状态,直接建boolean或者bit(1)都行,但要固定一个标准,不要今天用 bit,明天用 boolean,后天又冒出一个小整数表示状态,同一个含义在几十张表里类型不统一,后面接手的同事会骂娘。
如果你确实要存多位的状态位、权限位,我建议 Java 侧统一按String作为传输格式,内部用BitSet做运算,这个组合的可维护性远远好于直接操作byte[]。位串长度不大的时候,字符串的开销几乎可以忽略,换来的是日志可读、接口可调试、排查问题不用猜位序。
最后分享一个小技巧:凡是设计位掩码字段,一定把每一位的语义写成注释或者枚举。比如:
// authority bit(8) // bit 7: 是否允许读取 // bit 6: 是否允许写入 // bit 5: 是否允许删除 // bit 0-4: 保留或者建一张枚举表把位索引固化下来。没有这层注释,三个月后你自己回来都未必记得第 5 位表示什么。位操作类型的东西,约定和注释比代码本身更重要。