Issue
Suppose you have two resources, User and Account. They are stored in separate tables but have a one-to-one relationship, and all API calls should work with them both together. For example a POST request to create a User with an Account would send this data:
{ "name" : "Joe Bloggs", "account" : { "title" : "My Account" }}
to /users
rather than have multiple controllers with separate routes like users/1/account
. This is because I need the User object to be just one, regardless of how it is stored internally.
Let's say I create these Entity classes
@Table(name = "user")
public class User {
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@NotNull
Account account;
@Column(name = "name")
String name;
}
@Table(name = "account")
public class Account {
@OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
@NotNull
User user;
@Column(name = "title")
String title;
}
The problem is when I make that POST request above, it throws an error because user_id
is missing, since that's required for the join, but I cannot send the user_id
because the User has not yet been created.
Is there a way to create both entities in a single API call?
Solution
Since it is a bi-directional relation, and one-to-one
is a mandatory in this case, you should persist a user entity and only then persist an account. And one more thing isn't clear here is db schema. What are the pk's of entities? I coukd offer to use user.id
as a single identity for both of tables. If so, entities would be as:
User(id, name), Account(user_id, title)
and its entities are:
@Table(name = "account")
@Entity
public class Account {
@Id
@Column(name = "user_id", insertable = false, updatable = false)
private Long id;
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, optional = false)
@MapsId
private User user;
@Column(name = "title")
private String title;
}
@Table(name = "user")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "id", referencedColumnName = "user_id")
private Account account;
@Column(name = "name")
private String name;
}
at the service layer you must save them consistently:
@Transactional
public void save(User userModel) {
Account account = user.getAccount();
user.setAccount(null);
userRepository.save(user);
account.setUser(user);
accountRepository.save(account);
}
it will be done within a single transaction. But you must save the user first, coz the user_id
is a PK
of the account
table. @MapsId
shows that user's id is used as an account's identity
Another case is when account's id
is stored in the user
table:
User(id, name, account_id), Account(id, title)
and entities are:
@Table(name = "account")
@Entity
public class Account {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "account")
private User user;
@Column(name = "title")
private String title;
}
@Table(name = "user")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "account_id", insertable = false, updatable = false)
private Long accountId;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "account_id", referencedColumnName = "id", unique = true)
private Account account;
@Column(name = "name")
private String name;
}
in this case an Account
entity will be implisitly persisted while User
entity saving:
@Transactional
public void save(User userModel) {
userRepository.save(user);
}
will cause an insertion into the both of tables. Since cascade and orphane are declared, for deletion would be enough to set null
for the account reference:
user.setAccount(null);
userRepository.save(user);
Answered By - Yuriy Tsarkov
Answer Checked By - Robin (JavaFixing Admin)