Spring Boot 3.1中使用EmbeddedId保存含复合主键子实体失败
问题背景
在Spring Boot 3.1搭配Spring Data JPA Starter环境下,保存包含复合主键子实体的主实体时出错。定义了Invoice、Product和InvoiceItem三个实体,InvoiceItem采用invoice_id和product_id组成的复合主键,用EmbeddedId实现映射,但保存首个Invoice时抛出错误:
Could not set value of type [java.lang.Long] :
info.fareez.goldin.entities.embeddables.InvoiceItemId.invoiceId(setter)
尝试过@IdClass方案,但因为需要重复字段名,所以更倾向于用EmbeddedId解决。
实体代码
Invoice.java
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor @Entity public class Invoice { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "customer_id", nullable = true) private Customer customer; @OneToMany(mappedBy = "invoice", cascade = CascadeType.PERSIST) private List<InvoiceItem> invoiceItems; private Date timestamp; private double price; }
Product.java
@Builder @Entity @AllArgsConstructor @NoArgsConstructor @Setter @Getter public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String code; private String name; private double purity; private double weight; private int stock; @OneToMany(mappedBy = "product") private List<InventoryTransaction> inventoryTransactions; }
InvoiceItem 相关代码
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Embeddable public class InvoiceItemId { private Long invoiceId; private Long productId; } @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor @Entity public class InvoiceItem { @EmbeddedId private InvoiceItemId invoiceItemId; @MapsId("invoiceId") @ManyToOne private Invoice invoice; @MapsId("productId") @OneToOne private Product product; private Double price; }
保存逻辑代码
List<InvoiceItem> invoiceItems = new ArrayList<>(); var invoice = Invoice.builder() .customer(customer) .timestamp(new Date()).build(); for (var itemDto : invoiceRequest.getInvoiceItems()) { // prepare invoice items here invoiceItems.add( InvoiceItem.builder() .invoice(invoice) .product(product.toProduct()) // other fields .build()) } invoice.setInvoiceItems(invoiceItems); invoice.setPrice(totalPrice); invoiceRepository.save(invoice);
完整错误栈
Caused by: org.hibernate.PropertyAccessException: Could not set value of type [java.lang.Long] : `info.fareez.goldin.entities.embeddables.InvoiceItemId.invoiceId` (setter) at org.hibernate.property.access.spi.SetterFieldImpl.set(SetterFieldImpl.java:81) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.mapping.Component$ValueGenerationPlan.execute(Component.java:538) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.id.CompositeNestedGeneratedValueGenerator.generate(CompositeNestedGeneratedValueGenerator.java:113) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:114) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:184) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:129) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:118) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:779) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:727) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.spi.CascadingActions$7.cascade(CascadingActions.java:301) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.spi.CascadingActions$7.cascade(CascadingActions.java:291) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:513) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:434) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:547) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:477) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:437) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:153) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:474) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:298) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:192) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:122) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:184) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:129) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:53) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:737) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:721) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na] at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311) ~[spring-orm-6.0.7.jar:6.0.7] at jdk.proxy3/jdk.proxy3.$Proxy122.persist(Unknown Source) ~[na:na] at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:613) ~[spring-data-jpa-3.0.4.jar:3.0.4] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na] at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:288) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:136) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:120) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.7.jar:6.0.7] at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.7.jar:6.0.7] at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:77) ~[spring-data-commons-3.0.4.jar:3.0.4] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.7.jar:6.0.7] at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-6.0.7.jar:6.0.7] at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:391) ~[spring-tx-6.0.7.jar:6.0.7] at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.0.7.jar:6.0.7] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.7.jar:6.0.7] at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-6.0.7.jar:6.0.7] ... 35 common frames omitted Caused by: java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "o" is null
解决方案
核心问题
错误根源是**InvoiceItem的invoiceItemId字段为null**。在使用@MapsId时,Hibernate需要通过EmbeddedId实例来设置主键值,但你通过Builder创建InvoiceItem时没有初始化invoiceItemId,导致Hibernate尝试给null对象的字段赋值,触发NullPointerException。
修复步骤
修改
InvoiceItem的Builder,自动初始化invoiceItemId
在InvoiceItem类中添加自定义Builder逻辑,默认创建InvoiceItemId实例:@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor @Entity public class InvoiceItem { @EmbeddedId private InvoiceItemId invoiceItemId; @MapsId("invoiceId") @ManyToOne private Invoice invoice; @MapsId("productId") @OneToOne private Product product; private Double price; // 自定义Builder,自动初始化复合主键实例 public static class InvoiceItemBuilder { private InvoiceItemId invoiceItemId = new InvoiceItemId(); } }或手动初始化
invoiceItemId
调整保存逻辑中的InvoiceItem构建代码,显式创建InvoiceItemId实例:invoiceItems.add( InvoiceItem.builder() .invoiceItemId(new InvoiceItemId()) // 初始化复合主键 .invoice(invoice) .product(product.toProduct()) // other fields .build() )可选:优化关联关系
如果Product是持久化状态(比如从数据库查询得到),InvoiceItem和Product的关联建议改用@ManyToOne,更符合“一个产品可出现在多个发票项”的业务逻辑。
验证修复
修改后重新执行保存逻辑,Hibernate会通过@MapsId自动将Invoice和Product的主键值同步到InvoiceItemId的对应字段中,避免NullPointerException。
内容的提问来源于stack exchange,提问作者Fareez Ahamed

