@JoinColumn关联非ID属性时,如何持久化含字典编码的实体?
首先得明确问题根源:你的DictionaryEntity本质是复合主键(group+code),但之前的映射没有明确这一点,而且@Where注解仅在查询阶段生效,持久化时Hibernate无法自动推断group参数,导致save后关联的字典实体为null。下面给出两种通用解决方案,避免逐个字段手动查询字典。
第一步:正确定义字典实体的复合主键
先为DictionaryEntity创建复合主键类,这是Hibernate正确处理关联的基础:
import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import java.io.Serializable; @Getter @Setter @EqualsAndHashCode public class DictionaryId implements Serializable { private String group; private Integer code; // 必须提供无参构造器供Hibernate使用 public DictionaryId() {} public DictionaryId(String group, Integer code) { this.group = group; this.code = code; } }
然后修改DictionaryEntity,用@IdClass绑定复合主键:
import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.IdClass; import lombok.Getter; import lombok.Setter; @Entity @IdClass(DictionaryId.class) @Getter @Setter public class DictionaryEntity { @Id private String group; @Id private Integer code; private String name; }
方案一:利用Hibernate代理+通用字典服务(推荐,低侵入)
这个方案通过Spring服务获取字典实体的代理对象(无需实际查询数据库),在DTO转实体时自动关联,既高效又通用。
1. 实现通用字典服务
import org.springframework.stereotype.Service; import jakarta.persistence.EntityManager; @Service public class DictionaryService { private final EntityManager entityManager; public DictionaryService(EntityManager entityManager) { this.entityManager = entityManager; } // 获取字典代理对象,不会触发数据库查询 public DictionaryEntity getDictReference(String group, Integer code) { if (code == null) return null; return entityManager.getReference(DictionaryEntity.class, new DictionaryId(group, code)); } }
2. 结合DTO转换自动关联字典
以MapStruct为例(也可以用Spring Converter),定义通用转换逻辑,避免手动处理每个字典字段:
import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Named; import org.springframework.beans.factory.annotation.Autowired; @Mapper(componentModel = "spring") public abstract class ConfigurationMapper { @Autowired protected DictionaryService dictionaryService; // 将DTO的dayOfTheWeek(code值)转换为字典代理对象 @Mapping(target = "dayOfTheWeekDictionary", source = "dayOfTheWeek", qualifiedByName = "toDayOfTheWeekDict") public abstract ConfigurationEntity toEntity(ConfigurationDTO dto); @Named("toDayOfTheWeekDict") protected DictionaryEntity toDayOfTheWeekDict(Integer code) { return dictionaryService.getDictReference("dayOfTheWeek", code); } // 通用字典转换方法,适用于所有字典字段 @Named("toDict") protected DictionaryEntity toDict(Integer code, @Context String group) { return dictionaryService.getDictReference(group, code); } // 示例:其他字典字段的转换 @Mapping(target = "statusDict", source = "statusCode", qualifiedByName = "toDict", context = "orderStatus") public abstract ConfigurationEntity toEntityWithOtherDict(ConfigurationDTO dto); }
这样,客户端PATCH请求传入的dayOfTheWeek=2会自动转换为group=dayOfTheWeek, code=2的字典代理对象,Hibernate save后返回的实体中dayOfTheWeekDictionary不会为null(调用getter时才会触发数据库查询获取名称)。
方案二:自定义Hibernate类型(底层通用,无DTO依赖)
如果不想依赖DTO转换,可以自定义Hibernate类型,直接将数据库中的code值映射为字典实体,同时通过注解指定group参数。
1. 创建自定义注解标记字典分组
import java.lang.annotation.*; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DictGroup { String value(); }
2. 实现自定义Hibernate类型
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.type.UserType; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.Objects; public class DictUserType implements UserType { private final DictionaryService dictionaryService; // 注入Spring服务,需确保Hibernate支持Spring bean注入(配置SpringSessionContext) public DictUserType(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } @Override public int[] sqlTypes() { return new int[]{Types.INTEGER}; // 数据库存储的是code整数 } @Override public Class<?> returnedClass() { return DictionaryEntity.class; } @Override public boolean equals(Object x, Object y) { if (x == y) return true; if (x == null || y == null) return false; DictionaryEntity dx = (DictionaryEntity) x; DictionaryEntity dy = (DictionaryEntity) y; return dx.getGroup().equals(dy.getGroup()) && dx.getCode().equals(dy.getCode()); } @Override public int hashCode(Object x) { DictionaryEntity dict = (DictionaryEntity) x; return Objects.hash(dict.getGroup(), dict.getCode()); } @Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws SQLException { Integer code = rs.getInt(names[0]); if (rs.wasNull()) return null; // 从字段注解获取分组信息 DictGroup annotation = owner.getClass().getDeclaredField(names[0]).getAnnotation(DictGroup.class); return dictionaryService.getDictReference(annotation.value(), code); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws SQLException { if (value == null) { st.setNull(index, Types.INTEGER); return; } DictionaryEntity dict = (DictionaryEntity) value; st.setInt(index, dict.getCode()); } // 以下方法按需实现,基础场景用默认逻辑即可 @Override public Object deepCopy(Object value) { return value; } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object value) { DictionaryEntity dict = (DictionaryEntity) value; return new DictionaryId(dict.getGroup(), dict.getCode()); } @Override public Object assemble(Serializable cached, Object owner) { DictionaryId id = (DictionaryId) cached; return dictionaryService.getDictReference(id.getGroup(), id.getCode()); } @Override public Object replace(Object original, Object target, Object owner) { return original; } }
3. 修改实体映射
import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import org.hibernate.annotations.Type; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @Entity public class ConfigurationEntity { @Id private Long id; @Column(name = "dayOfTheWeek") @Type(type = "com.yourpackage.DictUserType") @DictGroup("dayOfTheWeek") @JsonIgnore private DictionaryEntity dayOfTheWeekDictionary; @JsonProperty public Integer getDayOfTheWeek() { return dayOfTheWeekDictionary != null ? dayOfTheWeekDictionary.getCode() : null; } @JsonProperty public String getDayOfTheWeekDescription() { return dayOfTheWeekDictionary != null ? dayOfTheWeekDictionary.getName() : null; } // 供Spring MVC绑定客户端传入的code值 public void setDayOfTheWeek(Integer code) { this.dayOfTheWeekDictionary = dictionaryService.getDictReference("dayOfTheWeek", code); } }
为什么之前会出现null?
之前的映射没有明确DictionaryEntity的复合主键,Hibernate无法识别group是主键的一部分;同时@Where注解仅在查询阶段生效,持久化时Hibernate无法自动推断需要关联的字典分组,导致save后关联实体为null。而再次查询时,@Where触发关联查询,所以能返回正确结果。
内容的提问来源于stack exchange,提问作者Adrian Dąbek

