Home > OS >  Spring Boot - combine nested resources for single API calls
Spring Boot - combine nested resources for single API calls

Time:08-10

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?

CodePudding user response:

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);
  • Related