Search code examples
javaspringhibernatejpa

Java Spring JPA: Why does it try to insert an uninitialized row when I explicitly updated a managed entity?


Consider the following entity:

@Entity
@Table(name = "my_entity")
public class MyEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "myEntitySeq")
    @SequenceGenerator(name = "myEntitySeq", sequenceName = "my_entity_seq")
    private Long id;

    @OneToOne(optional = false)
    @JoinColumn(name = "other_id")
    private OtherEntity other;

    @Column(nullable = false)
    private String fieldA;

    @Column(nullable = false)
    private byte[] fieldB;

    public MyEntity() {}

    public MyEntity(OtherEntity other) {
        this.other = other;
    }

    // public getter/setters for all fields...
}

All database fields should be non-null.

I then have a transactional method that should create or update this entity:

    @Transactional
    public void createOrUpdate(OtherEntity other, String fieldA, byte[] fieldB) {
        var myEntity = myEntityRepository.findByOther(other)
            // Create and get a new managed entity, missing fields will be updated right after:
            .orElseGet(() -> entityManager.merge(new MyEntity(other)))
        myEntity.setFieldA(fieldA);
        myEntity.setFieldB(fieldB);
        // Once the transaction ends I expect the new entity to be INSERTed, otherwise UPDATEd
    }

But this code fails with an error saying it cannot insert the new entity because it tries to insert null fields into the table (fieldA and fieldB are both null when calling merge, even if I update the state later).

My understanding was that since SQL queries are deferred until the end of a transaction, calling merge first to get a managed instance followed by updates to the state would properly defer a complete insertion at the end.

Instead it seems to defer an incomplete INSERT followed by an UPDATE, which trips the database NOT NULL constraints on the incomplete insert...

Removing the NOT NULL constraint allow the previous code to not fail because it allows the incomplete insert to go through, and somehow it also updates the row with the properties set after. But this means my database schema allows null values in places where I don't want them.

Am I wrong to expect entityManager.merge(...) followed by sets to group everything into a single INSERT statement? Is it expected that doing merge will schedule an INSERT with the state provided at this point, and only later UPDATE using the state as changed later in the transaction?


Solution

  • What I ended up writing to avoid the partial INSERT was to defer the merge of the uninitialized entity until the fields were all set:

        @Transactional
        public void createOrUpdate(OtherEntity other, String fieldA, byte[] fieldB) {
            var myEntity = myEntityRepository.findByOther(other)
                // Create a new unmanaged entity
                .orElseGet(() -> new MyEntity(other))
            myEntity.setFieldA(fieldA);
            myEntity.setFieldB(fieldB);
            // Merge into the persistence context only once all fields are set
            entityManager.merge(myEntity);
        }
    

    Alternatively it seems like we can use myEntityRepository.save(myEntity) in place of entityManager.merge(myEntity) in this example.