Raphael Amoedo Raphael Amoedo - 1 year ago 89
Java Question

@OneToOne from two classes to same class

For better understanding, I want to achieve this:

External Account example

Note: Buyer may not have ExternalAccount but Seller must have it. What I have/tried:

Buyer Class:

@Entity
public class Buyer extends User {

@OneToOne(optional=true, cascade= {CascadeType.MERGE})
@JoinColumn(nullable=true)
private ExternalAccount externalAccount;
//getters and setters

}


Seller Class:

@Entity
public class Seller extends User {

@OneToOne(optional=false, cascade= {CascadeType.MERGE})
@MapsId
@JoinColumn(nullable=false)
private ExternalAccount externalAccount;
//getters and setters and other properties

}


ExternalAccount class:

@Entity
public class ExternalAccount {

@Id
@PrimaryKeyJoinColumn
private Long id;
//getters and setters

}


I am using Spring Data JPA with Spring Boot and I want that:


  • If there's no Buyer related but ExternalAccount exists (associated with Seller), associate it.

  • If there's no Seller related but ExternalAccount exists (associated with Buyer), associate it.

  • If no ExternalAccount exists, when saving Buyer/Seller, creates the ExternalAccount.



I could achieve similar behavior with
CascadeType.MERGE
(after reading a lot of posts of Stackoverflow), but using this it doesn't respect
@OneToOne
mapping. It allows to create a lot of Buyers related to the same ExternalAccount.


  • I've created a github project with the database tests to reproduce the issue.



https://github.com/ralphavalon/jpa-mapping

There, I have my example rest controllers (MappingController):

//Creating buyer example
@RequestMapping(value = "/newBuyer", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object newBuyer() {
Buyer buyer = new Buyer();
buyer.setBirthdate(LocalDateTime.now());
buyer.setEmail("[email protected]");
buyer.setName("Buyer Name");
ExternalAccount external = new ExternalAccount();
external.setId(123L);
buyer.setExternalAccount(external);
buyerDao.save(buyer);
return buyer;
}

//Creating seller example
@RequestMapping(value = "/newSeller", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object newSeller() {
Seller seller = new Seller();
seller.setBirthdate(LocalDateTime.now());
seller.setEmail("[email protected]");
seller.setName("Seller Name");
ExternalAccount external = new ExternalAccount();
external.setId(123L);
seller.setExternalAccount(external);
sellerDao.save(seller);
return seller;
}


When I call
/newBuyer
at the first time, it saves. Now, if I call
/newSeller
after calling
/newBuyer
it returns this:

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.EXTERNAL_ACCOUNT(ID)"; SQL statement:
insert into external_account (id) values (?) [23505-196]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause

org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.EXTERNAL_ACCOUNT(ID)"; SQL statement:
insert into external_account (id) values (?) [23505-196]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345) ~[h2-1.4.196.jar:1.4.196]

Answer Source

I solved the issue with these changes:

  • Changing the mapping of the classes:

Buyer class:

@Entity
public class Buyer extends User {

    @OneToOne(optional=true, cascade= {CascadeType.MERGE})
    @JoinColumn(nullable=true, unique=true)
    private ExternalAccount externalAccount;
    //getters and setters

}

Seller class:

@Entity
public class Seller extends User {

    @OneToOne(optional=false, cascade= {CascadeType.MERGE})
    @JoinColumn(nullable=false, unique=true)
    private ExternalAccount externalAccount;
    //getters and setters

}

ExternalAccount class:

@Entity
public class ExternalAccount {

    @Id
    private Long id;
    //getters and setters

}

And the most important part: Override Spring Data JPA save method to use entityManager.merge

@Service
public class BuyerService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public Buyer save(Buyer buyer) {
        return entityManager.merge(buyer);
    }

}

And the same thing with SellerService.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download