You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot 3.1中使用EmbeddedId保存含复合主键子实体失败

Spring Boot 3.1 + Spring Data JPA 保存含复合主键子实体失败

问题背景

在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。

修复步骤

  1. 修改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();
        }
    }
    
  2. 或手动初始化invoiceItemId
    调整保存逻辑中的InvoiceItem构建代码,显式创建InvoiceItemId实例:

    invoiceItems.add(
        InvoiceItem.builder()
            .invoiceItemId(new InvoiceItemId()) // 初始化复合主键
            .invoice(invoice)
            .product(product.toProduct())
            // other fields
            .build()
    )
    
  3. 可选:优化关联关系
    如果Product是持久化状态(比如从数据库查询得到),InvoiceItem和Product的关联建议改用@ManyToOne,更符合“一个产品可出现在多个发票项”的业务逻辑。

验证修复

修改后重新执行保存逻辑,Hibernate会通过@MapsId自动将Invoice和Product的主键值同步到InvoiceItemId的对应字段中,避免NullPointerException。


内容的提问来源于stack exchange,提问作者Fareez Ahamed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 05:47:01