Search code examples
spring-bootspring-data-r2dbcr2dbcspring-data-jdbc

Designing one-to-one and one-to-many relationships in Spring Data R2DBC


I am exploring possible ideas when it comes to designing the one-to-one and one-to-many relationships while using Spring Data R2DBC.

As Spring Data R2DBC still do not support relationships natively there is still a need to handle those on our own (unlike Spring Data JDBC).

What I would imagine that when it comes to one-to-one mapping, the implementation could look like this:

@Table("account")
public class Account {
    
    @Id
    private Long id;
    
    @Transient // one-to-one
    private Address address;
    }
@Table("address")
public class Address {
    
    @Id
    private Integer id;
}

while the database schema would be defined as follows:

--address
CREATE TABLE address
(
    id                  SERIAL PRIMARY KEY
)
    
--account
CREATE TABLE account
(
    id                     SERIAL PRIMARY KEY,
    address_id             INTEGER REFERENCES address(id)
)

As the Account object is my aggregate root what I would imagine is that I am supposed to load the Address object with it following the advice of Jens Schaduer:

An aggregate is a cluster of objects that form a unit, which should always be consistent. Also, it should always get persisted (and loaded) together. source: Spring Data JDBC, References, and Aggregates

This leads me to thinking that in case of one-to-one relationships like this one I in fact should have my Account entity defined like this:

@Table("account")
public class Account {
    
    @Id
    private Long id;
    
    @Transient // one-to-one
    private Address address;

    @Column("address_id")
    private Integer addressId;
}

and later on to recreate the full Account aggregate entity with an Address I would write something like:

@Service
public class AccountServiceImpl implements AccountService {
    
    private final AccountRepository accountRepository;
    private final AddressRepository addressRepository;
    
    public AccountServiceImpl(AccountRepository accountRepository,
                              AddressRepository addressRepository) {
        this.accountRepository = accountRepository;
        this.addressRepository = addressRepository;
    }
    
    @Override
    public Mono<Account> loadAccount(Integer id) {
        return accountRepository.getAccountById(id)
            .flatMap(account ->
                 Mono.just(account)
                 .zipWith(addressRepository.getAddressByAccountId(account.getAddressId()))
                 .map(result -> {
                     result.getT1().setAddress(result.getT2());
                         return result.getT1();
                 })
        );
    }
}

If that is not the case, how else should I handle one-to-one relationships while using Spring Data R2DBC?


Solution

  • I think your approach is reasonable. There are just a couple of nitpicks:

    • Do you need the flatMap -> Mono.just ? Can't you just use map directly?
    • I wouldn't consider this a service, but a repository (it's just not implemented by Spring Data directly.
    • You might be able to that code in a after load callback.