【问题标题】:Cascading in JPA. Persist and Merge entities through the owner of associationJPA 中的级联。通过关联所有者持久化和合并实体
【发布时间】:2017-02-17 13:30:25
【问题描述】:

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

我的项目中有两个数据源。我正在尝试找到一种在数据库之间共享某些实体的方法。为此,我只需要使用我的每个实体管理器工厂对其进行两次扫描。根据我的想法,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) 中,然后更新第二个数据库而不发生冲突。

【问题讨论】:

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


    【解决方案1】:

    经过一番研究,我得出了以下代码。首先,我需要创建一个自定义存储库接口和实现类,以扩展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 字段使用一些自定义生成器,例如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。对于较新的版本,实现可能会有所不同。

    【讨论】:

      猜你喜欢
      • 2012-06-17
      • 2013-08-24
      • 2015-11-02
      • 2013-10-26
      • 2018-05-15
      • 1970-01-01
      • 1970-01-01
      • 2011-11-23
      • 2013-05-18
      相关资源
      最近更新 更多