Spring Boot之OneToMany、ManyToOne示例分析

Spring Boot的1對多場景

在實際使用場景中存在非常的1對多場景,對於這種情況,Spring Boot中提供基於JPA+Spring Data技術方案中,可以提供@OneToMany、@ManyToOne建立單項或者雙向的依賴關係,簡潔優雅地處理此類問題。

技術方案評估

基於Spring Boot框架,結合Spring Data JPA,底層使用Hibernate、Spring Data結合使用,基於ORM映射框架,來解決此類數據映射問題。
方案優點: 簡潔明瞭,無需編寫大量的代碼,快捷方便
方案不足: 封裝性比較高,調試有一定的複雜度,定製化開發略顯複雜。

示例場景介紹

一個用戶可以購買多個產品,這裏的實體類有: 用戶和產品類。
用戶信息包括: name,location等信息。
產品信息包括: name,count,price等信息。

數據實體類定義

用戶類UserEntity定義如下:

/**
 * User DAO.
 * @author  xxx
 * @date 2019-05-04
 */
@EqualsAndHashCode(callSuper = true)
@Table(name="t_user")
@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
public class UserEntity extends BaseEntity {
    @Column
    private String name;

    @Column
    private String location;

    /**
     * Who owns the foreign Key, who will be the owner, and declare the JoinColumn.
     *
     */
    @JsonManagedReference
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name="user_ext_id", referencedColumnName = "id")
    private UserExtEntity userExtEntity;

    @JsonManagedReference
    @OneToMany(cascade = {CascadeType.ALL}, mappedBy = "user", fetch = FetchType.EAGER)
    private List<ProductEntity> products;
}

產品類定義如下:

/**
 * Product Entity
 *
 * @author xxx
 * @date 2019-05-04
 */
@Table(name="t_product")
@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
public class ProductEntity extends BaseEntity {
    @Column
    private String name;

    @Column
    private Integer count;

    @Column
    private Float price;

    @JsonBackReference
    @ManyToOne(cascade = {CascadeType.REFRESH})
    @JoinColumn(name="user_id", referencedColumnName = "id")
    private UserEntity user;
}

在ORM的雙向映射關係中存在主從兩種實體,主(Host)關係,主要是指外鍵定義所在的Entity中的屬性變量所代表的內容。在雙向映射中,除了主關係之外的實例,就是從關係。
在這個示例中,從關係是UserEntity中定義的prodcuts屬性
User DAO定義如下:

/**
 *  User Ext Repository
 *
 * @author  xxx
 * @date 2019-05-05
 */
@Repository
public interface UserExtRepository extends JpaRepository<UserExtEntity, Long> {
}

Product DAO定義如下:

/**
 * Product DAO.
 *
 * @author  chenjunfeng
 * @date  2019-05-04
 */
@Repository
public interface ProductRepository extends JpaRepository<ProductEntity, Long> {
}

BaseEntity.java實體類基類定義:

@Data
@MappedSuperclass
public class BaseEntity implements java.io.Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreatedDate
    @Column(name="created_time")
    private Date createdTime;

    @LastModifiedDate
    @Column(name="updated_time")
    private Date updatedTime;

    @CreatedBy
    @Column(name="created_by")
    private String createdBy;

    @LastModifiedBy
    @Column(name="last_modified_by")
    private String lastModifiedBy;

    @Version
    private Integer version;
}

測試示例

測試代碼定義在Controller中:

@GetMapping("/case2")
    public ResultData createUserProduct() {
       UserEntity user = new UserEntity();
       user.setLocation("TianJing");
       user.setName("WuMa");

       List<ProductEntity> products = new ArrayList<>();
       ProductEntity entity = new ProductEntity();
       entity.setCount(2);
       entity.setName("food");
       entity.setPrice(12.2f);
       entity.setUser(user);

       ProductEntity entity1 = new ProductEntity();
       entity1.setCount(2);
       entity1.setName("food");
       entity1.setPrice(12.2f);
       entity1.setUser(user);

       products.add(entity);
       products.add(entity1);

       user.setProducts(products);

       user = this.userRepo.save(user);

       ResultData resultData = ResultData.success();
       resultData.setData(user);

       return resultData;
    }

在上述示例中,每一個Product都需要設置User實例,然後基於User DAO進行數據保存,只有這樣纔可以將數據正確地保存到數據庫中。
反之,如果在Product中未曾設置User實例,則在數據庫中無法建議兩者之間的關聯關係。
其核心原因在於外鍵信息是保存在ProductEntity之中的,所以需要建立類似的映射關係。

AuditorListener

在BaseEntity中定義了所有Entity類通用的字段屬性信息,其中createdTime、updatedTime、createdBy和lastModifiedBy四個字段分別使用了註解,來進行說明。
與之相對應的註解爲: @CreatedDate、@LastModifiedDate、@CreatedBy和@LastModifiedBy四個註解。
這些主機都是從屬於AuditorListener模塊中定義的註解內容,用來監聽Entity的變化以及記錄其中的變化內容。
所以在Entity的定義中,需要使用聲明:

@EntityListeners(AuditingEntityListener.class)

用以將相應的字段進行更新和寫入。

對於Auditor特性還需要在系統層面進行啓動和配置,具體配置如下:

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class PersistenceConfig {
    @Bean
    public AuditorAware auditorProvider() {
        return new AuditorAwareImpl();
    }
}

@EnableJpaAuditing用來啓用Auditor特性
auditorAwareRef用來提取對應的auditor關於人的信息。這個將會單獨定義:

public class AuditorAwareImpl implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of("system");
    }
}

在AuditorAwareImpl中,實現了getCurrentAuditor方法,用以給提供提供當前用戶的信息。

系統配置信息

在系統層面還需要配置application.properties信息:

[email protected]@
[email protected]@
[email protected]@
[email protected]@
[email protected]@
[email protected]@
[email protected]@

在pom.xml文件中,支持在不同的profile下選擇不同的數據庫系統:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.bitsu.jpa</groupId>
    <artifactId>mapping</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mapping</name>
    <description>Dependency in tables.</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>

            <properties>
                <database.username>sa</database.username>
                <database.password></database.password>
                <!--
          <database.url>jdbc:h2:file:/Users/zhansan/test</database.url>
                -->
                <database.url>jdbc:h2:mem:testdb</database.url>
                <database.driver.name>org.h2.Driver</database.driver.name>
                <database.ddl.mode>update</database.ddl.mode>
                <database.platform>H2</database.platform>
                <database.dialect>org.hibernate.dialect.H2Dialect</database.dialect>
            </properties>

            <dependencies>
                <dependency>
                    <groupId>com.h2database</groupId>
                    <artifactId>h2</artifactId>
                    <scope>runtime</scope>
                </dependency>
            </dependencies>
        </profile>

        <profile>
            <id>prod</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <properties>
                <database.username>root</database.username>
                <database.password>123456</database.password>
                <database.url>jdbc:mysql://localhost:3306/mytest?characterEncoding=UTF-8&amp;&amp;useSSL=false&amp;&amp;serverTimezone=Asia/Shanghai</database.url>
                <database.driver.name>com.mysql.cj.jdbc.Driver</database.driver.name>
                <database.ddl.mode>update</database.ddl.mode>
                <database.platform>MYSQL</database.platform>
                <database.dialect>org.hibernate.dialect.MySQL5InnoDBDialect</database.dialect>
            </properties>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <scope>runtime</scope>
                </dependency>
            </dependencies>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

在pom.xml中定義了兩個profile:dev、prod。dev使用了H2作爲默認開發數據庫,在prod環境下使用mysql作爲開發數據庫。

Maven下的執行操作

執行dev下的Spring boot應用:

mvn spring-boot:run -Dmaven.test.skip=true -Pdev

-PprofileName: 指定profile的名稱
-Dmaven.test.skip=true: 關閉自動化測試的執行
spring-boot:run 啓動spring boot應用

基於IDE,目前無法很容易地切換profile,所以推薦使用命令行來操作。

MySQL配置信息

對於使用com.mysql.cj.jdbc.Driver驅動的MySQL連接信息來說,需要配置一下其serverTimeZone,具體配置如下:

jdbc:mysql://localhost:3306/mytest?characterEncoding=UTF-8&&useSSL=false&&serverTimezone=Asia/Shanghai

此爲在pom.xml文件中定義的,所以&需要轉化爲&

& --> & amp;

關於H2數據庫

在開發過程中,使用文件數據庫是非常輕便和快捷的。這裏同時提供了基於文件的H2數據庫的配置信息。
對於H2其默認的數據庫用戶名/密碼爲sa和空。

總結

在本示例中,提供了dev、prod兩種profile,分別使用不同的數據庫:H2和MySQL。基於命令行來動態切換profile,並自動連接不同的數據庫信息。
OneToMany、ManyToOne分別用於建立1對多的映射關係,還可以用於建立雙向關聯的數據關係,這些都是構建在JPA+Spring Data基礎之上的。

參考資料

  1. Auditor Listener
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章