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信息:
spring.datasource.driver-class-name=@database.driver.name@
spring.datasource.username=@database.username@
spring.datasource.password=@database.password@
spring.datasource.url=@database.url@
spring.jpa.hibernate.ddl-auto=@database.ddl.mode@
spring.datasource.platform=@database.platform@
spring.jpa.properties.hibernate.dialect=@database.dialect@
在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基础之上的。
参考资料