1. Oracle自增主键的两种策略解析
第一次接触Oracle的generated always as identity和generated by default as identity时,我也被这两个长得像双胞胎的参数搞懵了。这就像你去餐厅点餐,服务员问"要默认套餐还是定制套餐",看起来都是套餐,但实际体验天差地别。
先看个典型建表示例:
sql复制-- 灵活模式
CREATE TABLE order_info (
order_id NUMBER generated by default as identity,
product_name VARCHAR2(100)
);
-- 严格模式
CREATE TABLE log_record (
log_id NUMBER generated always as identity,
content CLOB
)
这两种语法都出现在Oracle 12c之后,用来替代传统的"序列+触发器"方案。但它们的核心区别在于:是否允许人工干预自增值。by default就像个开明的家长,既允许系统自动分配ID,也接受你手动指定;而always则是个固执的班主任,只允许按它的规矩来。
实测发现个有趣现象:当使用always策略时,即使你写的insert语句完全正确,只要包含ID字段就会报错:
sql复制-- 这个能成功
INSERT INTO log_record(content) VALUES('系统启动');
-- 这个必失败
INSERT INTO log_record(log_id, content) VALUES(1001, '用户登录');
错误信息会直接告诉你:"ORA-32795: 无法插入生成的always身份列"。这种设计在需要数据迁移或指定特殊ID的场景下会成为致命伤。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MyBatis集成时的致命陷阱
去年我们团队就踩过这个坑。当时有个电商项目使用always策略生成订单ID,在MyBatis中配置了这样的映射:
xml复制<insert id="createOrder" parameterType="Order">
<selectKey keyProperty="orderId" order="BEFORE" resultType="long">
SELECT order_seq.nextval FROM dual
</selectKey>
INSERT INTO orders(order_id, user_id)
VALUES(#{orderId}, #{userId})
</insert>
结果每次提交订单都报错!原因就在于selectKey获取的ID值会被MyBatis自动填入INSERT语句,而always策略拒绝接受这种"夹带私货"的行为。这就像你拿着VIP邀请函去参加严格验票的音乐会,保安却说:"我们只认现场抽选的座位号"。
经过反复测试,发现只有以下两种方案能解决问题:
- 改用
by default策略 - 完全移除
selectKey,在Java代码中处理业务逻辑
但方案2会导致无法获取插入后的自增ID,对于需要即时显示订单号的场景根本不可行。这就是为什么说Java项目要慎用always策略。
3. 实战中的选型建议
经过多个项目实战,我总结出这张决策对照表:
| 场景特征 | 推荐策略 | 原因说明 |
|---|---|---|
| 需要获取插入后的自增ID | by default | 兼容MyBatis的selectKey机制 |
| 需要人工指定特殊ID值 | by default | 允许override系统生成值 |
| 严格防止ID篡改 | always | 强制使用系统序列 |
| 历史数据迁移场景 | by default | 支持指定原始ID值 |
特别提醒:使用by default时要注意序列缓存设置。有次我们遇到性能问题,最后发现是序列缓存太小导致的:
sql复制-- 优化序列配置
ALTER TABLE order_info
MODIFY (order_id GENERATED BY DEFAULT AS IDENTITY (CACHE 100));
4. 完整避坑代码示范
给出一套经过生产验证的完整方案。首先建表语句:
sql复制CREATE TABLE payment_record (
payment_id NUMBER GENERATED BY DEFAULT AS IDENTITY,
amount NUMBER(10,2),
pay_time TIMESTAMP DEFAULT SYSTIMESTAMP
)
对应的MyBatis映射配置:
xml复制<insert id="insertPayment" parameterType="Payment">
<!-- 使用BEFORE模式避免序列跳号 -->
<selectKey keyProperty="paymentId" order="BEFORE" resultType="long">
SELECT payment_seq.nextval FROM dual
</selectKey>
INSERT INTO payment_record(payment_id, amount)
VALUES(#{paymentId}, #{amount})
</insert>
这里有个精妙的设计:故意在insert语句中显式写出payment_id。如果省略这个字段,Oracle会先执行selectKey使序列+1,执行insert时又会+1,导致内存中的ID与实际存储ID不一致。我们曾经因此出现订单ID错乱,显示给用户的ID和数据库记录对不上。
对于批量插入场景,可以这样优化:
java复制public void batchInsert(List<Payment> payments) {
// 预获取ID范围
Long startId = sqlSession.selectOne("getNextIdRange", payments.size());
for(int i=0; i<payments.size(); i++) {
payments.get(i).setPaymentId(startId + i);
sqlSession.insert("insertPaymentDirect", payments.get(i));
}
}
对应的SQL映射:
xml复制<select id="getNextIdRange" resultType="long">
SELECT payment_seq.nextval + #{size} - 1 FROM dual
</select>
<insert id="insertPaymentDirect">
INSERT INTO payment_record(payment_id, amount)
VALUES(#{paymentId}, #{amount})
</insert>
这种方案比单条插入性能提升5倍以上,同时保证ID连续性。记得在事务中执行,避免其他会话占用ID。
5. 性能优化与监控
使用自增主键后,我们发现个隐藏问题:序列缓存导致的主键不连续。通过这个查询可以监控序列使用情况:
sql复制SELECT sequence_name, cache_size, last_number
FROM user_sequences
WHERE sequence_name LIKE 'ISEQ$$%';
建议设置适当的缓存大小平衡性能与连续性。对于财务系统这类需要严格连续编号的场景,可以设置CACHE=1,但会降低并发性能。
还有个容易忽视的点:自增列和索引的配合。不当的索引设计会导致插入性能下降:
sql复制-- 好的索引设计
CREATE INDEX idx_payment_time ON payment_record(pay_time);
-- 不好的设计(自增列天然有序,不需要单独索引)
CREATE INDEX idx_payment_id ON payment_record(payment_id);
在Oracle 19c之后,还可以使用IDENTITY列的增强特性:
sql复制-- 限制自增范围
CREATE TABLE limited_ids (
id NUMBER GENERATED BY DEFAULT AS IDENTITY
(START WITH 1000 MAXVALUE 9999 CYCLE),
description VARCHAR2(100)
);
6. 特殊场景处理方案
遇到数据迁移时,可以用这个技巧临时关闭自增特性:
sql复制-- 临时允许手动插入
ALTER TABLE payment_record
MODIFY (payment_id GENERATED BY DEFAULT ON NULL AS IDENTITY);
-- 迁移完成后恢复
ALTER TABLE payment_record
MODIFY (payment_id GENERATED BY DEFAULT AS IDENTITY);
对于需要重置自增值的场景(比如测试环境),这个方案比删除表更优雅:
sql复制-- 先移除自增属性
ALTER TABLE test_table MODIFY (test_id DROP IDENTITY);
-- 重置数据
TRUNCATE TABLE test_table;
-- 重新添加自增(从指定值开始)
ALTER TABLE test_table MODIFY (
test_id GENERATED BY DEFAULT AS IDENTITY (START WITH 1)
);
在微服务环境下,如果需要避免不同实例产生重复ID,可以考虑这种分布式ID方案:
java复制// 结合机器标识和自增序列
Long machineId = 1L; // 从配置中心获取
Long localId = sqlSession.selectOne("getNextId");
Long globalId = (machineId << 32) | (localId & 0xFFFFFFFFL);
7. 常见错误排查指南
遇到ORA-32795错误时,首先检查:
- 表定义是否使用
always策略 - MyBatis是否误将ID包含在INSERT语句中
- 是否有触发器在干扰自增逻辑
对于获取到错误ID值的情况,检查:
- selectKey的order属性是否为BEFORE
- insert语句是否显式包含ID列
- 序列缓存是否设置过大
有个特别隐蔽的坑:当使用JPA的@GeneratedValue注解时,默认策略可能不兼容Oracle IDENTITY。推荐显式指定:
java复制@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long paymentId;
最后分享个诊断脚本,可以快速检查表自增策略:
sql复制SELECT table_name, column_name, generation_type, identity_options
FROM user_tab_identity_cols
WHERE table_name = 'PAYMENT_RECORD';
