Search code examples
springhibernatejpaflywayone-to-one

Spring JPA: Issue with shared PK in one-to-one relationship


I am trying to create a relationship one-to-one with a shared PK, but I am straggled after trying many things...

I will try to provide all the possible information:

Technologies that I am using:

  • Spring Boot 2.1.5
  • Spring JPA
  • Flyway

Datasource configuration:

spring.datasource.url = jdbc:mysql://localhost:3306/customers?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username = admin
spring.datasource.password = root

spring.jpa.database-platform = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql = true

Database models:

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "customer", schema = "customers")
public class Customer {

    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "uuid2")
    @Column(name = "customer_id", columnDefinition = "BINARY(16)")
    private UUID customerId;

    @OneToOne(mappedBy = "customer", cascade = CascadeType.ALL)
    private Address address;
}

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "address", schema = "customers")
public class Address {

    @Id
    @Column(name = "address_id", columnDefinition = "BINARY(16)")
    private UUID addressId;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Customer customer;
}

Flyway migration files:

CREATE SCHEMA IF NOT EXISTS CUSTOMERS;

CREATE TABLE CUSTOMERS.CUSTOMER
(
    customer_id BINARY(16) NOT NULL PRIMARY KEY,
) ENGINE = InnoDB;

CREATE TABLE CUSTOMERS.ADDRESS
(
    address_id BINARY(16) NOT NULL PRIMARY KEY,
) ENGINE = InnoDB;

ALTER table CUSTOMERS.ADDRESS
ADD CONSTRAINT fk_customer_address
FOREIGN KEY (address_id)
REFERENCES CUSTOMERS.CUSTOMER (customer_id);

Integration test:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerRepositoryIntegrationTest {

    @Autowired
    private CustomerRepository cut;

    @Test
        public void testSaveAndDeleteCustomer() {
            Address address = new Address();
            Customer customer = new Customer();
            customer.setAddress(address);

            cut.save(customer);

            Customer retrievedCustomer = cut.findAll().get(0);
            assertEquals(customer, retrievedCustomer);
        }
}

Error:

ERROR] testSaveAndDeleteCustomer(com.foxhound.customers.repositories.CustomerRepositoryIntegrationTest)  Time elapsed: 0.034 s  <<< ERROR!
org.springframework.orm.jpa.JpaSystemException: attempted to assign id from null one-to-one property [com.foxhound.customers.models.Address.customer]; nested exception is org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property [com.foxhound.customers.models.Address.customer]
    at com.foxhound.customers.repositories.CustomerRepositoryIntegrationTest.testSaveAndDeleteCustomer(CustomerRepositoryIntegrationTest.java:47)
Caused by: org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property [com.foxhound.customers.models.Address.customer]
    at com.foxhound.customers.repositories.CustomerRepositoryIntegrationTest.testSaveAndDeleteCustomer(CustomerRepositoryIntegrationTest.java:47)

Thank you in advance for the help.

Cheers!


Solution

  • You don't understand how bidirectional mapping is working, so you need to think through that. The code

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Customer customer;
    

    in Address makes the mapping unidirectional. Normally in order to save this you will need to set the customer field and save it.

    address.setCustomer(customer);        
    addressRepo.save(address);
    

    However you have defined a bidirectional mapping and provided a cascading annotation.

    @OneToOne(mappedBy = "customer", cascade = CascadeType.ALL)
    private Address address;
    

    The cascading annotation alleviates you from having to do two persistence operations (in your code), but it does not mean you do not have to set the customer field in address. Additionally, as you have noticed, in order for the cascade operation to work you need to set the address field of customer.

    customer.setAddress(address);
    

    So, for the code to work properly you need to change it to set the customer in address.

    Address address = new Address();
    Customer customer = new Customer();
    customer.setAddress(address);
    address.setCustomer(customer);
    customerRepo.save(customer);
    

    With a bidirectional mapping you have to manage both sides of the relationship or use it in a unidirectional manner for persisting and a bidirectional manner for retrieving. If you add a cascade annotation then you have to manage both sides of the relationship for persisting as well.