JPA复杂实体关系的构建

原创
2015/11/20 10:47
阅读数 719

User story:

假设有如下的业务逻辑:


其中:t_customer对t_address:N-1关系,即一个地址有多个用户(假设是送快递的情形,你不能告诉快递员送到两个地点);t_customer对t_phone:1-N关系,即可能你的手机是双卡双待的(强弱关系不作讨论,“一人一定要有个手机号码吗?”);t_customer对t_project:N-M关系,即你有很多项目要做,但一个项目是由多个人同时完成的;t_project对t_project:1-N关联,这个比较常见,一个项目关联其他的项目。

Analysis:

抽象:我们需要从所有实体中抽取一些公共的部分出来。

访问性:应该考虑如何实现实体之间的关联,实体之间如何获取数据,如何更新数据。

扩展维护:如何后期有更改或者数据库字段变更,我应该如何维护,如何扩展。是否有必要实现公共的组件进行访问。

Solutions:

分析实际问题后,最常见的抽象实现形式就是分层,分层有物理架构、逻辑架构、业务架构等等,还应该考虑如何分层,分多少层。

这里对所有实体抽取一些公共属性作为父类,因为所有实体都一定有一个Id,这里实现一个公共的生成策略。

@MappedSuperclass
public abstract class BaseEntity implements Serializable, Cloneable {

//    @Id
//    @GeneratedValue(generator = "system-uuid")
//    @GenericGenerator(name = "system-uuid", strategy = "uuid")
//    @Column(name = "id", length = 32)
//    private String id;

//    @Id
//    @Column(name = "id", length = 32)
//    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "generator")
//    @GenericGenerator(name = "generator", strategy = "org.hibernate.id.enhanced.TableGenerator")

    @Id
    @Column(name = "id", length = 32)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

接着只需要实体继承就可以了,但是... 还有一部分没有抽象出来,就是每个实体都有CURD的操作,所以,这里再抽象多一层。

@MappedSuperclass
@EntityListeners({AudiTableListener.class,LoggingTableListener.class})
public abstract class AudiTableEntity extends BaseEntity implements Serializable {
    // TODO -

添加两个监听器进行控制,和AOP有点类型,但是既然JPA规范是这样写,我们就这样实现。

/**
 * @author Barudisshu
 */
public class AudiTableListener {

    @PreUpdate
    @PrePersist
    private void preUpdate(AudiTableEntity audiTableEntity) {

    }
}

这样,我就可以在实体update和persist触发自定义的事件了。

OK,写到这里,我就可以大胆写自己的实体了,首先定义最复杂的Customer类

/**
 * @author Barudisshu
 */
@Entity
@Table(name = "t_customer", schema = "", catalog = "db_test")
@AttributeOverride(name = "id", column = @Column(name = "cus_id", length = 32))
public class Customer extends AudiTableEntity {
    @Basic
    @Column(name = "cus_name")
    private String cusName;
    @Basic
    @Column(name = "cus_pwd")
    private String cusPwd;
    @Basic
    @Column(name = "cus_address", insertable = false, updatable = false)
    private Long cusAddress;
    @Basic
    @Column(name = "cus_project", insertable = false, updatable = false)
    private Long cusProject;

    @ManyToOne
    @JoinColumn(name = "cus_address", referencedColumnName = "address_id")
    private Address address;

    @OneToMany(mappedBy = "customer",fetch=FetchType.EAGER, cascade = CascadeType.ALL,targetEntity = Phone.class)
    private Collection<Phone> phones;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "t_cp", joinColumns = @JoinColumn(name = "cp_project_id", referencedColumnName = "cus_id"),
            inverseJoinColumns = @JoinColumn(name = "cp_cus_id", referencedColumnName = "project_id"))
    private Collection<Project> projects;
}

定义了外键关联的属性,就要相应设置它的访问限制insertable = false, updatable = false;另外在实现1-N关系时,要添加mappedBy,否则会生成一个中间表。

/**
 * @author Barudisshu
 */
@Entity
@Table(name = "t_address", schema = "", catalog = "db_test")
@AttributeOverride(name = "id", column = @Column(name = "address_id", length = 32))
public class Address extends AudiTableEntity {

    @Basic
    @Column(name = "address_name")
    private String addressName;
    @Basic
    @Column(name = "address_type")
    private String addressType;
    @OneToMany(mappedBy = "address")
    private Collection<Customer> customers;

其中,N-1关系中,N的一方要添加@JoinColumn或@JoinColumns注解。

/**
 * @author Barudisshu
 */
@Entity
@Table(name = "t_phone", schema = "", catalog = "db_test")
@AttributeOverride(name = "id", column = @Column(name = "phone_id", length = 32))
public class Phone extends AudiTableEntity {

    @Basic
    @Column(name = "phone_number")
    private String phoneNumber;
    @Basic
    @Column(name = "phone_belong",insertable = false,updatable = false)
    private Long phoneBelong;
    @ManyToOne(cascade = CascadeType.PERSIST,targetEntity = Customer.class)
    @JoinColumn(name = "phone_belong",referencedColumnName = "cus_id")
    private Customer customer;

接下来的t_project稍微复杂,因为要实现自身的关联,这种关联关系一定是1-N(想一想,如果是1-1,这种设计是否有问题?)

/**
 * @author Barudisshu
 */
@Entity
@Table(name = "t_project", schema = "", catalog = "db_test")
@AttributeOverride(name = "id", column = @Column(name = "project_id", length = 32))
public class Project extends AudiTableEntity {

    @Basic
    @Column(name = "project_subject")
    private String projectSubject;

    @ManyToMany
    @JoinTable(name = "t_cp", joinColumns = @JoinColumn(name = "cp_cus_id",referencedColumnName = "project_id"),inverseJoinColumns = @JoinColumn(name = "cp_project_id",referencedColumnName = "cus_id"))
    private Collection<Customer> customers;

    @ManyToOne(cascade={CascadeType.ALL})
    @JoinColumn(name = "project_apd")
    private Project parentProject;

    @OneToMany(mappedBy = "parentProject")
    private Set<Project> projectApds = new HashSet<>();

Project实体也要加上@JoinTable注解,并且和customer刚好相反;因为N-M关系中,需要有一个中间表。

OK,实体构建后,只需要配置一下spring就可以了。

<!-- JPA EntityManagerFactory -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="packagesToScan" value="ls.jpa.chapterC.domain"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="generateDdl" value="true"/>
            <property name="showSql" value="true"/>
        </bean>
    </property>
    <property name="jpaPropertyMap">
        <map>
            <entry key="hibernate.hbm2ddl.auto" value="${hibernate.hbm2ddl.auto}"/>
            <entry key="hibernate.format_sql" value="${hibernate.format_sql}"/>
            <entry key="hibernate.dialect" value="${hibernate.dialect}"/>
            <!--配置二级缓存-->
            <entry key="hibernate.cache.use_second_level_cache" value="true"/>
            <entry key="hibernate.cache.region.factory_class"
                   value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
            <!--原来hibernate3的配置方式-->
            <!--<entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>-->
            <!--开启查询缓存-->
            <entry key="hibernate.cache.use_query_cache" value="true"/>
        </map>
    </property>
</bean>

<bean id="entityManager" factory-bean="entityManagerFactory" factory-method="createEntityManager"/>

<!-- JPA TransactionManager --> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" p:entityManagerFactory-ref="entityManagerFactory"/> <!-- enables scanning for @Transactional annotations --> <tx:annotation-driven transaction-manager="transactionManager"/> <!-- JPA Data Repository --> <jpa:repositories base-package="ls.jpa.chapterC.repository" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager" query-lookup-strategy="create-if-not-found" repository-impl-postfix="Impl"/> <!-- 开启AOP监听 只对当前配置文件有效 --> <aop:aspectj-autoproxy expose-proxy="true"/>

Embedded:

JPA为我们提供了一种实体嵌入式的技术支持,这或许是对可变状态和副作用(side effect)的一种控制。好比如有个经常修改的实体类Address。有些字段需要经常改来改去的(某程序员:“又改需求?”)。因此可以这样实现:

/**
 * @author Barudisshu
 */
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "AddressType")
@DiscriminatorValue(value = "ADD")
@Table(name = "t_add")
public class Address implements Serializable {


    private String addressCountry;
    private String addressCity;
    @EmbeddedId
    private AddressPK addressPK;

然后创建一个嵌入的POJO

/**
 * @author Barudisshu
 */
@Embeddable
public class AddressPK implements Serializable {

    @Column(name = "AddressCountryId")
    private int addressCountryId;
    @Column(name = "AddressCityId")
    private int addressCityId;
    @Column(name = "AddressId")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int addressId;

这种方案或许在某些场景很有用,@Embeddable、@EmbeddedId和@SecondaryTable一样比较不常用。这样做的好处是,我所写的DAO和Services可以不用改,甚至不用重构。所有更改只需要交给应用层去处理就可以了,就好比如这里的Address,我所写的方法中,只需要接收一个AddressPK参数就可以了。

当然这里还是有side effect的,因为Address包含AddressPK,若要实现Address和其他实体的业务交互,还需要考虑AddressPK,所以在抽象(Abstraction)和组合(composition)时,抽象出公共的不变的因素,并提供一个共同的method;组合时则尽量只组合相关的常用的内容,并多处重构。

说了这么多,其实如何会Python、Haskell等函数式编程语言的人应该比较明白。好比如Haskell,我只需要实现一个Monad设计模式就可以搞定这种诸如此类的问题(当然,你可能还需要Actor)。

啰嗦多一下,关于JPA这种设计,着实有不少人在抱怨的,因为要设计一种公共的、统一的处理各种不同数据库结构的规范协议,实在太难了...... 国内和国外都有不少好的处理解决方案。总而言之,统而言之,基于最小功能模块实现的设计就是好的设计。


展开阅读全文
JPA
打赏
0
1 收藏
分享
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部