FailedShack
FailedShack

Reputation: 91

JPA: Mapping crossed OneToOne and ManyToOne relations

I have two entities, which we'll call A and B. B always has A as a parent with a ManyToOne relation. However, I need A to have a OneToOne relation with the latest record inserted in table B. This is because I need to save multiple versions of B but 99% of the time will only need to use the most recent one.

This looks something like this:

@Data
@Entity
public class A {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Setter(AccessLevel.NONE)
    private Long id;

    /* Properties
       ...
    */

    @OneToOne(optional = false)
    private B latest;
}
@Data
@Entity
public class B {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Setter(AccessLevel.NONE)
    private Long id;

    /* Properties
       ...
    */

    @Column(nullable = false)
    private Date lastModified;

    @ManyToOne(optional = false)
    private A parent;
}

Now, the issue at hand is that I cannot seem to persist these entities as one always appears to be transient:

Attempting to do so results in:

java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : B.parent -> A

I tried wrapping the code responsible for persisiting them in a @Transactional method but the same happens:

@Transactional
public void saveAB(A parent, B child) {
    parent.setLatest(child);
    child.setParent(parent);
    Arepository.save(parent);
    Brepository.save(child);
}

I also thought of disregarding the OneToOne relation from A to B, instead having latest as a transient @Formula field which would query B to take the most recent record. However, @Formula seems to be limited to primitives, not full entities.

What would be the proper way to do this with JPA? Am I approaching this the wrong way?

Upvotes: 0

Views: 344

Answers (2)

FailedShack
FailedShack

Reputation: 91

The solution was to apply @JoinFormula as explained here.

@Data
@Entity
public class A {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Setter(AccessLevel.NONE)
    private Long id;

    /* Properties
       ...
    */

    @ManyToOne
    @JoinFormula(value = "(SELECT b.id FROM b " +
            "WHERE b.id = id ORDER BY b.lastModified DESC LIMIT 1)")
    private B latest;
}

Then on B:

    @ManyToOne(optional = false)
    private A parent;

Upvotes: 0

Jens Schauder
Jens Schauder

Reputation: 81970

Since A and B depend on each other they should probably be considered a single aggregate with A being the aggregate root.

This means you'd have only an ARepository and also CascadeType.ALL on the relationships.

Upvotes: 0

Related Questions