2017-02-17 4 views
0

Мне интересно, возможно ли с JPA (Hibernate) сохранять и обновлять объекты косвенно через владельца ассоциации.Каскадирование в JPA. Персистировать и объединять сущности через владельца ассоциации

В моем проекте есть два источника данных. Я пытаюсь найти способ обмена некоторыми объектами между базами данных. Для этого мне просто нужно отсканировать его дважды с каждым из моих застройщиков-менеджеров. Согласно моей идее, в обеих базах данных можно использовать объект Employee. Для этого мне просто нужно создать объект Phone во втором источнике данных, и все его поля будут перенесены через Hibernate в мою вторую базу данных.

Вот пример кода (я использовал lombok и удален импорт, чтобы упростить его)

@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; 
    } 
} 

Я хотел бы использовать Spring Data JPA PhoneRepository настроен для работы с моим вторым источником данных

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

И EmployeeRepository, как я думаю, можно использовать только один раз для настройки с помощью первого источника данных. Все отношения во второй базе данных могут создаваться автоматически с помощью Spring/Hibernate. По крайней мере, мне хотелось бы этого. В моих тестах ниже он сконфигурирован с моим вторым источником данных только для иллюстративных целей.

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

Вот некоторые тесты

@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")); 
} 

В идеале я хотел бы взять объект (Employee) из моего первого источника данных, поместите его в оберточную сущности (Phone) от моего второго источника данных и обновления вторая база данных без нарушений.

ответ

0

После небольшого исследования я пришел к следующему коду. Прежде всего, мне нужно было создать пользовательский интерфейс репозитория и класс реализации, который расширяет SimpleJpaRepository и полностью дублирует функциональность SimpleJpaRepository, за исключением метода Save.

@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 вот простая аннотация. Он описывает, какие поля следует объединить, прежде чем продолжать сущность.

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

    String[] value(); 
} 

RepositoryFactoryBean заменяет SimpleJpaRepository с пользовательской реализации - 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; 
     } 
    } 
} 

... а затем он должен быть помещен во второй конфигурации источника данных

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

Некоторые объекты будут взяты из моего первого источника данных. При желании можно использовать какой-то пользовательский генератор для полей id, например, в 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; 
} 

И Phone сущность "заворачивает" их можно использовать с моим вторым источником данных

@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> {} 

Оба теста проходит успешно

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"); 
} 

Вот как Hibernate работает в моей первой испытания

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, ?, ?) 

Я не уверен, что это самое простое решение, но по крайней мере оно делает именно то, что мне нужно. Я использую Spring Boot 1.2.8. С более новыми версиями реализация может отличаться.

Смежные вопросы