Im working on an app to track customer accounts. Each customer can have multiple accounts.
The challenges is that the account object is polymorphic. There is an abstract class Account
with a 2-3 methods, and then there are currently 2 sub-types SavingsAccount
and CreditAccount
.
What I have tried is to have the Account
object use the @MappedSuperclass
annotation, and then each concrete class has the @Entity
annotations as normal.
The problem seems to be the mapping my Customer object
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name="customer_id", referencedColumnName = "id")
List<Account> accounts = new ArrayList<>();
When I use that I get a warning from Spring in my IDE,but at runtime I get this error.
com.example.models.Customer.accounts' targets the type 'com.example.models.Account' which is not an '@Entity' type
Is there no way to have a @OneToMany mapping against a list of polymorphic objects? Even if they were stored in the same table?
@MappedSuperclass
is used for reusing common properties across @Entity
-annotated classes and does not apply directly to your current case. However, a correct usage example for @MappedSuperclass
in your case would be:
@MappedSuperclass
public abstract class MyMappedSuperclass {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// getter/setter
}
and let Customer
inherit MyMappedSuperclass
.
You need to annotate your base Account
class with @Entity
and @Inheritance
. This enables polymorphic queries, where you can query the base class and retrieve all its subclasses. If using a single table for all subclasses, the Account
class can look like this:
import jakarta.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "account_type")
public abstract class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
// getters and setters
}
Your sub-classes will then look like this
import jakarta.persistence.DiscriminatorValue;
@DiscriminatorValue("SAVINGS")
public class SavingsAccount extends Account {
}
@DiscriminatorValue("CREDIT")
public class CreditAccount extends Account {
}
I verified the above code with this Customer implementation:
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Account> accounts = new ArrayList<>();
public void addAccount(Account account) {
account.setCustomer(this);
accounts.add(account);
}
// getters/setters
}
CustomerRepository
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}
Flyway migration (Postgresql)
CREATE TABLE customer
(
id SERIAL PRIMARY KEY
);
CREATE TABLE account
(
id SERIAL PRIMARY KEY,
account_type VARCHAR(16) NOT NULL,
customer_id INT NOT NULL,
CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customer (id)
);
PostgresTestContainerConfig
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
@TestConfiguration
public class PostgresTestContainerConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer<>("postgres:15-alpine");
}
}
Test
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
@AutoConfigureTestDatabase
@Import(no.vicx.database.PostgresTestContainerConfig.class)
class CustomerRepositoryTest {
@Autowired
TestEntityManager entityManager;
@Autowired
CustomerRepository sut;
@Test
void testInserts() {
Customer customer = new Customer();
customer.addAccount(new SavingsAccount());
customer.addAccount(new CreditAccount());
sut.save(customer);
assertEquals(2, getAccountCountInDb());
var customerInDb = entityManager.find(Customer.class, customer.getId());
assertInstanceOf(SavingsAccount.class, customerInDb.getAccounts().getFirst());
assertInstanceOf(CreditAccount.class, customerInDb.getAccounts().getLast());
}
long getAccountCountInDb() {
return (long) entityManager.getEntityManager()
.createQuery("SELECT COUNT(1) FROM Account ")
.getSingleResult();
}
}