1

In my OpenXava application I had an entity with an @OneToMany collection of entities to create a master-detail structure. The main entity, Invoice, has a total persistent property that I want to update everytime that the user adds, removes or changes a detail. The user interface generated by OpenXava is this:

@OneToMany details collection

The total is on the footer of the amount column and it is updated when a line is added, modified, or removed. I achieved this effect using JPA callback methods, specifically @PostPersist, @PostUpdate and @PostRemove in the detail class.

This is the code for my Invoice entity:

@Entity @Getter @Setter
@View(members=
    "year, number, date;" +
    "customer;" +
    "details;" 
)
@Tab(properties="year, number, date, customer.name, total")
public class Invoice extends Identifiable {
    
    @DefaultValueCalculator(CurrentYearCalculator.class)
    @Column(length=4) @Required
    int year;
    
    @Column(length=6) @Required
    int number;
    
    @Required @DefaultValueCalculator(CurrentDateCalculator.class) 
    Date date;
    
    @ManyToOne(optional=false, fetch=FetchType.LAZY)
    Customer customer;
    
    @Stereotype("MONEY")
    BigDecimal total;
    
    @OneToMany(mappedBy="invoice", cascade = CascadeType.ALL)
    @ListProperties("product.number, product.description, unitPrice, quantity, amount[invoice.total]")
    List<InvoiceDetail> details;
            
    public void recalculateTotal() {
        BigDecimal sum = BigDecimal.ZERO;
        for (InvoiceDetail detail: details) {
            sum = sum.add(detail.getAmount());
        }
        total = sum;
    }
            
}

And this for the InvoiceDetail entity:

@Entity @Getter @Setter
public class InvoiceDetail extends Identifiable {
    
    @ManyToOne
    Invoice invoice;
        
    @ManyToOne(optional=false, fetch=FetchType.LAZY)
    Product product;

    @Required 
    BigDecimal unitPrice;
        
    @Required
    int quantity;

    @Depends("unitPrice, quantity") 
    public BigDecimal getAmount() {
        return new BigDecimal(getQuantity()).multiply(getUnitPrice()); 
    }
    
    @PostPersist @PostUpdate @PostRemove
    private void recalculateInvoiceTotal() {
        invoice.recalculateTotal();
    }
    
}

The above code worked nicely. However, I refactored it in order to use @ElementCollection instead of @OneToMany collection, so OpenXava will generate a UI where the user can edit the details inline, in this way:

@ElementCollection details collection

For that I changed the collection definition in Invoice entity to this:

@ElementCollection
@ListProperties("product.number, product.description, unitPrice, quantity, amount[invoice.total]")
List<InvoiceDetail> details;

And I refactored InvoiceDetail as an @Embeddable:

@Embeddable @Getter @Setter
public class InvoiceDetail {
    
    @ManyToOne(optional=false, fetch=FetchType.LAZY)
    Product product;

    @Required 
    BigDecimal unitPrice;
        
    @Required
    int quantity;

    @Depends("unitPrice, quantity") 
    public BigDecimal getAmount() {
        return new BigDecimal(getQuantity()).multiply(getUnitPrice()); 
    }
    
    @PostPersist @PostUpdate @PostRemove
    private void recalculateInvoiceTotal() {
        // invoice.recalculateTotal(); I no longer can access to invoice
        System.out.println("[InvoiceDetail.recalculateInvoiceTotal()] "); // NEVER PRINTED
    }
    
}

The first problem is that I have not access to invoice from InvoiceDetail, but still worse the recalculateInvoiceTotal() method is not executed, never. That is JPA callback methods are not executed in the @Embeddable for @ElementCollection.

Can JPA callback method be executed in @Embeddable? Is there a way to solve this case?

javierpaniza
  • 677
  • 4
  • 10
  • To be clear, are you using Hibernate as JPA provider? I had simillar issue month ago with entity with `@MappedSupperclass` annotation. I only found [that](https://hibernate.atlassian.net/browse/HHH-13366) issue but I didn't find solution. Probably in all cases problem is in the same logic inside implementation. – M. Dudek Apr 30 '21 at 13:27

1 Answers1

1

The JPA callback methods are for refining the entity lifecycle. The @Embeddable objects have not their own lifecycle but their cycle is associated to their container entity.

The JPA 2.2 specification states in section 3.5.2:

"Entity lifecycle callback methods can be defined on an entity listener class and/or directly on an entity class or mapped superclass."

Note it does not say anything about embeddables.

The solution is to move the callback method logic from the @Embeddable to the container @Entity. For your case, for example remove the recalculateInvoiceTotal() from your InvoiceDetail embeddable:

@Embeddable @Getter @Setter
public class InvoiceDetail {
    
    // ...      

    // REMOVE THIS METHOD 
    // @PostPersist @PostUpdate @PostRemove
    // private void recalculateInvoiceTotal() {
    //  invoice.recalculateTotal();
    // }
        
}

Then mark the already existing method recalculateTotal() in Invoice entity with @PrePersist @PreUpdate, thus:

@PrePersist @PreUpdate 
public void recalculateTotal() {
    BigDecimal sum = BigDecimal.ZERO;
    for (InvoiceDetail detail: details) {
        sum = sum.add(detail.getAmount());
    }
    total = sum;
}

And the total property will be synchronized like in your previous code with the @OneToMany collection.

javierpaniza
  • 677
  • 4
  • 10