1

I'm wondering is it possible with JPA (Hibernate) to persist and update entities indirectly through the owner of association.

There are two datasources in my project. I'm trying to find a way to share some entities between databases. For that I just have to scan it twice with each of my Entity Manager Factories. According to my idea, an Employee entity could be used in both databases. For that, I just need to create a Phone entity in the second datasource and all it fields will be migrated via Hibernate to my second database.

Here is a sample of code (I've used lombok and removed imports to simplify it)

@Entity
@Table(uniqueConstraints = {
    @UniqueConstraint(columnNames = {"name"})})
@lombok.NoArgsConstructor(access = PROTECTED)
@lombok.AllArgsConstructor
@lombok.Data
public class Employee {

    @Id
    private Long id;

    private String name;
}

@Entity
@lombok.Data
@lombok.NoArgsConstructor(access = PROTECTED)
public class Phone {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {PERSIST, MERGE, REFRESH})
    @JoinColumn(name = "em_id")
    private Employee employee;

    private String number;

    public Phone(Employee employee, String number) {
        this.employee = employee;
        this.number = number;
    }
}

I would like to use a Spring Data Jpa PhoneRepository configured to work with my second datasource

public interface PhoneRepository extends JpaRepository<Phone, Long> {}

And an EmployeeRepository, as I think, could be used only once to be configured with first datasource. All relations in the second database could be created automatically by Spring/Hibernate. At least, I would like this. In my tests below it configured with my second datasource for illustrative purposes only.

public interface EmployeeRepository extends JpaRepository<Employee, Long> {}

Here are some tests

@Autowired
EmployeeRepository employeeRepository;

@Autowired
PhoneRepository phoneRepository;

/**
 * Passes successfully.
 */
@Test
public void shouldPersitPhonesCascaded() {

    phoneRepository.save(new Phone(new Employee(1L, "John Snow"), "1101"));

    phoneRepository.save(new Phone(new Employee(2L, "Hans Schnee"), "1103"));
}

/**
 * Causes <blockquote><pre>
 * org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.EMPLOYEE(ID)"; SQL statement:
 * insert into employee (name, id) values (?, ?) [23505-190]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
 *        at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
 * ...
 * </pre></blockquote>
 */
@Test
public void shouldMergePhonesCascaded() {
    Employee employee = new Employee(1L, "John Snow");

    phoneRepository.save(new Phone(employee, "1101"));

    phoneRepository.save(new Phone(employee, "1102"));
}

/**
 * Works with changed Phone entity's field.
 * <blockquote><pre>
 * {@literal @}ManyToOne
 * {@literal @}JoinColumn(name = "em_id")
 *  private Employee employee;
 * </pre></blockquote>
 */
@Test
public void shouldAllowManualMerging() {
    Employee employee = new Employee(1L, "John Snow");
    employeeRepository.save(employee);

    phoneRepository.save(new Phone(employee, "1101"));

    phoneRepository.save(new Phone(employee, "1102"));
}

Ideally I would like to take an object (Employee) from my first datasource, put it into a wrapping entity (Phone) from my second datasource and update the second database without violations.

ytterrr
  • 3,036
  • 6
  • 23
  • 32

1 Answers1

0

After a little research, I came to the following code. First of all I needed to create a custom repository interface and implementation class that extends the SimpleJpaRepository and fully duplicates functionality of SimpleJpaRepository except for a Save method.

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {}

public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements BaseRepository<T, ID> {

    private final EntityManager em;
    private final JpaEntityInformation<T, ?> entityInformation;

    public BaseRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
        this.em = entityManager;
        this.entityInformation = JpaEntityInformationSupport.getMetadata(domainClass, entityManager);
    }

    private static void mergeFieldsRecursively(EntityManager em, Object entity) {
        MergeColumns merge = entity.getClass().getDeclaredAnnotation(MergeColumns.class);
        if (merge != null) {
            for (String columnName : merge.value()) {
                Field field = ReflectionUtils.findField(entity.getClass(), columnName);
                ReflectionUtils.makeAccessible(field);
                Object value = ReflectionUtils.getField(field, entity);

                mergeFieldsRecursively(em, value);

                em.merge(value);
            }
        }
    }

    @Transactional
    @Override
    public <S extends T> S save(S entity) {

        mergeFieldsRecursively(em, entity);

        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
}

MergeColumns here is a simple annotation. It describes which fields should be merged before persisting an entity.

@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface MergeColumns {

    String[] value();
}

RepositoryFactoryBean replaces SimpleJpaRepository with custom implementation - BaseRepositoryImpl.

public class BaseRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {

    @Override
    @SuppressWarnings("unchecked")
    protected RepositoryFactorySupport createRepositoryFactory(EntityManager em) {
        return new BaseRepositoryFactory<>(em);
    }

    private static class BaseRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {

        private final EntityManager em;

        public BaseRepositoryFactory(EntityManager em) {
            super(em);
            this.em = em;
        }

        @Override
        @SuppressWarnings("unchecked")
        protected Object getTargetRepository(RepositoryMetadata metadata) {
            return new BaseRepositoryImpl<T, I>((Class<T>) metadata.getDomainType(), em);
        }

        @Override
        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
            return BaseRepositoryImpl.class;
        }
    }
}

...and then it should be placed in the second datasource configuration

@EnableJpaRepositories(
        entityManagerFactoryRef = SECOND_ENTITY_MANAGER_FACTORY,
        transactionManagerRef = SECOND_PLATFORM_TX_MANAGER,
        basePackages = {"com.packages.to.scan"},
        repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class)
public class SecondDataSourceConfig { /*...*/ }

Some entities will be taken from my first datasource. Optionally it is possible to use some custom generator for id fields like in this answer.

@Entity
@Table(uniqueConstraints = {
    @UniqueConstraint(columnNames = {"name"})})
@lombok.NoArgsConstructor(access = PROTECTED)
@lombok.AllArgsConstructor
@lombok.Data
public class Department {

    @Id
    private Long id;

    private String name;
}

@Entity
@Table(uniqueConstraints = {
    @UniqueConstraint(columnNames = {"name"})})
@MergeColumns({"department"})
@lombok.NoArgsConstructor(access = PROTECTED)
@lombok.AllArgsConstructor
@lombok.Data
public class Employee {

    @Id
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "dept_id")
    private Department department;
}

And a Phone entity "wraps" them to be used with my second datasource

@Entity
@lombok.Data
@lombok.NoArgsConstructor(access = PROTECTED)
@MergeColumns({"employee"})
public class Phone {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "em_id")
    private Employee employee;

    private String number;

    public Phone(Employee employee, String number) {
        this.employee = employee;
        this.number = number;
    }
}

public interface PhoneRepository extends BaseRepository<Phone, Long> {}

Both tests passes successfully

import static org.assertj.core.api.Assertions.assertThat;
...

@Autowired
PhoneRepository phoneRepository;

@Test
public void shouldPersitPhonesCascaded() {

    phoneRepository.save(new Phone(new Employee(1L, "John Snow", new Department(1L, "dev")), "1101"));
    phoneRepository.save(new Phone(new Employee(2L, "Hans Schnee", new Department(2L, "qa")), "1103"));

    assertThat(phoneRepository.findAll()).extracting(p -> p.getEmployee().getName()).containsExactly("John Snow", "Hans Schnee");
}

@Test
public void shouldMergePhonesCascaded() {
    Employee employee = new Employee(1L, "John Snow", new Department(1L, "dev"));

    phoneRepository.save(new Phone(employee, "1101"));
    phoneRepository.save(new Phone(employee, "1102"));

    assertThat(phoneRepository.findAll()).extracting(p -> p.getEmployee().getName()).containsExactly("John Snow", "John Snow");
}

Here is how Hibernate works in my first test

Hibernate: select department0_.id as id1_3_0_, department0_.name as name2_3_0_ from department department0_ where department0_.id=?
Hibernate: select employee0_.id as id1_4_0_, employee0_.dept_id as dept_id3_4_0_, employee0_.name as name2_4_0_ from employee employee0_ where employee0_.id=?
Hibernate: insert into department (name, id) values (?, ?)
Hibernate: insert into employee (dept_id, name, id) values (?, ?, ?)
Hibernate: select employee_.id, employee_.dept_id as dept_id3_4_, employee_.name as name2_4_ from employee employee_ where employee_.id=?
Hibernate: insert into phone (id, em_id, number) values (null, ?, ?)
Hibernate: select department0_.id as id1_3_0_, department0_.name as name2_3_0_ from department department0_ where department0_.id=?
Hibernate: select employee0_.id as id1_4_0_, employee0_.dept_id as dept_id3_4_0_, employee0_.name as name2_4_0_ from employee employee0_ where employee0_.id=?
Hibernate: insert into department (name, id) values (?, ?)
Hibernate: insert into employee (dept_id, name, id) values (?, ?, ?)
Hibernate: select employee_.id, employee_.dept_id as dept_id3_4_, employee_.name as name2_4_ from employee employee_ where employee_.id=?
Hibernate: insert into phone (id, em_id, number) values (null, ?, ?)

I'm not sure that this is the simplest solution, but at least it does exactly what I needed. I'm using a Spring Boot 1.2.8. With newer versions the realization may differ.

Community
  • 1
  • 1
ytterrr
  • 3,036
  • 6
  • 23
  • 32