JPA映射含空值的复合键:遗留数据库无主键列的技术咨询
解决JPA映射含空值的复合键问题
我太懂你对接遗留数据库的糟心了——没有主键只能用复合嵌入ID,结果复合键里一出现空值,JPA就开始闹脾气。毕竟JPA规范里默认要求主键字段不能为null,咱们得找些变通的法子来适配这个遗留场景,下面几个方案你可以根据实际情况选:
方案1:给数据库加虚拟主键(优先推荐,如果能操作数据库的话)
这是最稳妥的办法,既符合JPA的规范,又能避开复合键空值的问题:
- 给你的
person表新增一个自增的主键列(比如id),不用改原有数据; - 把原来的
first_Name和dobr字段设为唯一约束,保证业务层面的唯一性; - 修改JPA实体类,用新增的主键作为
@Id,原来的复合字段作为普通字段:
@Entity @Table(name = "person", uniqueConstraints = { @UniqueConstraint(columnNames = {"first_Name", "dobr"}) }) class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 适配你的数据库,比如Oracle用SEQUENCE private Long id; @Column(name = "first_Name", precision = 4, scale = 0) private String firstName; @Column(name = "dobr", precision = 4, scale = 0) private String dob; // getter & setter 方法 }
这个方案对遗留系统的影响极小,只是加了个主键列,原有业务逻辑几乎不用改。
方案2:用Hibernate自定义类型处理空值(不能改数据库时用)
如果数据库完全动不了,而你用的是Hibernate作为JPA实现,可以自定义一个UserType来把空值转换成数据库能存储的特殊标记,读取时再转回来:
第一步:实现自定义UserType
public class NullSafeStringType implements UserType { // 定义数据库对应的SQL类型 @Override public int[] sqlTypes() { return new int[]{Types.VARCHAR}; } @Override public Class<?> returnedClass() { return String.class; } // 重写空值处理逻辑 @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { String value = rs.getString(names[0]); // 把数据库里的标记值转回null return "NULL_PLACEHOLDER".equals(value) ? null : value; } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { // 把null转换成数据库能存储的标记值 if (value == null) { st.setString(index, "NULL_PLACEHOLDER"); } else { st.setString(index, (String) value); } } // 以下是必须实现的其他方法 @Override public boolean equals(Object x, Object y) throws HibernateException { if (x == y) return true; if (x == null || y == null) return false; return x.equals(y); } @Override public int hashCode(Object x) throws HibernateException { return x == null ? 0 : x.hashCode(); } @Override public Object deepCopy(Object value) throws HibernateException { return value == null ? null : new String((String) value); } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) deepCopy(value); } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return deepCopy(cached); } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return deepCopy(original); } }
第二步:在复合ID类里使用这个类型
class PersonId { @Type(type = "com.yourpackage.NullSafeStringType") // 替换成你的包路径 String firstName; @Type(type = "com.yourpackage.NullSafeStringType") String dob; // getter & setter 方法 }
这样JPA就会把空值转换成NULL_PLACEHOLDER存储,读取时再转成null,绕开主键非空的限制。
方案3:用Hibernate的@NaturalId替代复合主键
如果不想用复合主键,还可以把原来的复合字段标记为@NaturalId(Hibernate特有),这样既可以通过这些字段做唯一查询,又允许字段为空:
@Entity class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NaturalId(mutable = true) // mutable=true允许后续修改 @Column(name = "first_Name", precision = 4, scale = 0) private String firstName; @NaturalId(mutable = true) @Column(name = "dobr", precision = 4, scale = 0) private String dob; // getter & setter 方法 }
查询时可以用Hibernate的API通过自然ID查询:
Person person = session.byNaturalId(Person.class) .using("firstName", "John") .using("dob", "19900101") .load();
总结一下
- 能改数据库就选方案1,最规范也最省心;
- 不能改数据库但用Hibernate的话,方案2或3都可以,方案2更贴近原来的复合键结构,方案3更简洁。
内容的提问来源于stack exchange,提问作者Sanjay
相关产品推荐
相关产品推荐

