在JPA级联。通过协会所有者

时间:2017-02-17 13:30:26

标签: java hibernate jpa spring-data spring-data-jpa

我想知道JPA(Hibernate)是否可以通过关联所有者间接地持久化和更新实体。

我的项目中有两个数据源。我正试图找到一种在数据库之间共享某些实体的方法。为此,我只需要使用我的每个Entity Manager Factories扫描两次。根据我的想法,可以在两个数据库中使用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)中,并更新第二个数据库而不会违反。

1 个答案:

答案 0 :(得分: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();
}

RepositoryFactoryBeanSimpleJpaRepository替换为自定义实施 - 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中的@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, ?, ?)

以下是 Hibernate 在我的第一次测试

中的工作原理
$python3 mnist_1.0_softmax.py

我不确定这是最简单的解决方案,但至少它完全符合我的需要。我正在使用 Spring Boot 1.2.8 。对于较新的版本,实现可能会有所不同。