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&&useSSL=false&&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基礎之上的。