2017-02-17 45 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数据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: 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。随着更新版本的实现可能会有所不同。