Search code examples
javaspring-data-jpaone-to-onebatching

Spring 5 Data Jpa - OneToOne, new entity, a single insert operation


I have table User entity/table with id as primary key / string / uuid generated value. I also have UserDetails entity/table with userId as primary key / string / foreign key to user.id. Note that in the UserDetails java entity I don't need the User entity, but user_id is enough.

I'm trying to define the entities in a way that inserting new User entity will also create a new UserDetails row, with the same id.

Any ideas how to implement this?

To clarify, what I want can be achieved in sql by doing this:

BEGIN

INSERT INTO user (id, name) VALUES ('auto-generated-uuid-31212', 'Bobby');
INSERT INTO user_details (user_id) VALUES ('auto-generated-uuid-31212');

COMMIT

My last attempt was to have 2 columns on UserDetails - String userId with @Id (no other mapping), and User user with OneToOne/JoinColumn(name="userId")/MapsId annotations.

However it seems like spring/hibernate attempts to then save the UserDetails info first rather than together with the User, and I end up getting foreign key constraint fails on the foreign key constraint.

Quick code:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Entity
public class User {
    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;

    @Column
    private String name;

    @JoinColumn(name = "id")
    @OneToOne(cascade = CascadeType.ALL)
    @NotNull
    @Builder.Default
    private UserDetails userDetails = new UserDetails();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
@Entity
public class UserDetails {
    @Id
    private String userId;

    @OneToOne
    @JoinColumn(name = "userId")
    @MapsId
    private User user;
}

Edit: I forgot to add the UserDetails property on the User to this question.


Solution

  • I finally got it working. Since you have two tables you will need at least two inserts. Another option is to embed the user details (@Embeddable instead of @Entity on UserDetails and some other changes) but this requires the columns in the user-table. Then you have only one table and can go with a single insert.

    public class User {
        ...
        @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
        @PrimaryKeyJoinColumn
        private UserDetails details;
    
        public User(String name)
        {
            userDetails = new UserDetails(this);
            this.name = name;
        }
    }
    

    with UserDetails:

    public class UserDetails {
       @Id
       @Column(name = "user_id")
       private String userId;
    
       @OneToOne
       @MapsId
       @JoinColumn(name = "user_id")
       private User user;
    
       public UserDetails(User user)
       {
           this.user = user;
       }
    }
    

    Now if you create a user the details are automatically created too and will be persisted to the database along with the user.