Spring Data JPA学习
一、spring data jpa
1、简介
(1)官网地址:
https://spring.io/projects/spring-data-jpa
参考文档:
https://docs.spring.io/spring-data/jpa/docs/2.2.3.release/reference/html/#preface
(2)基本介绍:
spring data jpa 是 spring 基于 orm 框架、jpa 规范封装的一套 jpa 框架。使开发者通过极简的代码实现对数据库的访问和操作。
注:
orm 框架:指的是 object relational mapping,即对象关系映射。采用元数据来描述对象和关系映射的细节。
元数据:一般采用 xml 文件的形式。常见 orm 框架如:mybatis、hibernate。
jpa:指的是 java persistence api,即 java 持久层 api。通过 xml 或注解的映射关系将运行期的实体对象持久化到数据库中。
2、sping boot 项目中使用
(1)在 pom.xml 文件中引入依赖
【pom.xml】 <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency>
(2)在 application.properties 中配置
【application.properties】 # jpa 配置 # 配置数据库为 mysql spring.jpa.database=mysql # 在控制台打印 sql 语句 spring.jpa.show-sql=true # 每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新 spring.jpa.hibernate.ddl-auto=update # 每次运行该程序,没有表格会新建表格,表内有数据会清空 #spring.jpa.hibernate.ddl-auto=create
二、基本注解
1、@entity
@entity 写在类上,用于指明一个类与数据库表相对应。 属性: name,可选,用于自定义映射的表名。若没有,则默认以类名为表名。 【举例1:默认类名为表名】 import javax.persistence.entity; @entity public class blog { } 【举例2:自定义表名】 import javax.persistence.entity; @entity(name="t_blog") public class blog { }
2、@table
@table 写在类上,一般与 @entity 连用,用于指定数据表的相关信息。 属性: name, 对应数据表名。 catalog, 可选,对应关系数据库中的catalog。 schema,可选,对应关系数据库中的schema。 【举例:】 import javax.persistence.entity; import javax.persistence.table; @entity(name = "blog") @table(name = "t_blog") public class blog { } 注:若 @entity 与 @table 同时定义了 name 属性,那以 @table 为主。
3、@id、@generatedvalue
@id 写在类中的变量上,用于指定当前变量为主键 id。一般与 @generatedvalue 连用。 @generatedvalue 与 @id 连用,用于设置主键生成策略(自增主键,依赖数据库)。 注: @generatedvalue(strategy = generationtype.auto) 主键增长方式由数据库自动选择,当数据 库选择auto方式时就会自动生成hibernate_sequence表。 @generatedvalue(strategy = generationtype.identity) 要求数据库选择自增方式,oracle不 支持此种方式,mysql支持。 @generatedvalue(strategy = generationtype.sequence) 采用数据库提供的sequence机制生 成主键,mysql不支持。 【举例:】 package com.lyh.blog.bean; import lombok.data; import javax.persistence.*; @entity @table(name = "t_blog") @data public class blog { @id @generatedvalue(strategy = generationtype.identity) private integer id; }
4、@column
@column 写在类的变量上,用于指定当前变量映射到数据表中的列的属性(列名,是否唯一,是否允许为空,是否允许更新等)。 属性: name: 列名。 unique: 是否唯一 nullable: 是否允许为空 insertable: 是否允许插入 updatable: 是否允许更新 length: 定义长度 【举例:】 import lombok.data; import javax.persistence.*; @entity @table(name = "t_blog") @data public class blog { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 36, unique = false, nullable = false, insertable = true, updatable = true) private string name; }
5、@temporal
@temporal 用于将 java.util 下的时间日期类型转换 并存于数据库中(日期、时间、时间戳)。 属性: temporaltype.date java.sql.date日期型,精确到年月日,例如“2019-12-17” temporaltype.time java.sql.time时间型,精确到时分秒,例如“2019-12-17 00:00:00” temporaltype.timestamp java.sql.timestamp时间戳,精确到纳秒,例如“2019-12-17 00:00:00.000000001” 【举例:】 package com.lyh.blog.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "t_blog") @data public class blog { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 36, unique = false, nullable = false, insertable = true, updatable = true) private string name; @temporal(temporaltype.timestamp) private date createtime; @temporal(temporaltype.date) private date updatetime; }
6、级联(cascade)
对于 @onetoone、@manytomany、@onetomany等映射关系,涉及到级联的操作。 cascadetype[] cascade() default {}; 定义级联用于 给当前设置的实体 操作 另一个关联的实体的权限。 【级联的类型:】 package javax.persistence; public enum cascadetype { all, persist, merge, remove, refresh, detach; private cascadetype() { } } cascadetype.all 拥有所有级联操作的权限。 cascadetype.persist 当前实体类进行保存操作时,同时保存其关联的实体。 cascadetype.merge 当前实体数据合并时,会影响其关联的实体。 cascadetype.remove 删除当前实体,与其相关联的实体也会被删除。 cascadetype.refresh 刷新当前实体,与其相关联的实体也会被刷新。 cascadetype.detach 去除外键关联,当删一个实体时,存在外键无法删除,使用此级联可以去除外键。
7、mappedby
只有 @onetoone, @onetomany, @manytomany上才有 mappedby 属性,@manytoone不存在该属性。 该属性的作用: 设置关联关系。单向关联关系不需要设置,双向关系必须设置,避免双方都建立外键字段。 对于 一对多 的关系,外键总是建立在多的一方(用到@joincolumn),而 mappedby 存在相反的一方。 比如: 部门(department)与 员工(employee) 一个部门对应多个员工。一个员工属于一个部门。 即部门与员工间的关系是 一对多 的关系。 【举例:】 public class department { @onetomany(mappedby = "bookcategory", cascade = cascadetype.all) private list<employee> employee; } public class employee { @manytoone private department department; }
8、@onetoone
@onetoone 用于描述两个数据表间 一对一的关联关系。 【属性:】 cascade, 用于定义级联属性 fetch, 用于定义 懒加载(lazy,不查询就不加载)、热加载(eager,默认) mappedby, 用于定义 被维护的表(相关联的表) optional, 用于定义 是否允许对象为 null。
三、jpa 实现 crud(以单个实体类为例)
1、搭建环境(以spring boot 2.0为例)
(1)添加依赖信息
【pom.xml】 <?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 https://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.2.2.release</version> <relativepath/> <!-- lookup parent from repository --> </parent> <groupid>com.lyh.demo</groupid> <artifactid>jpa</artifactid> <version>0.0.1-snapshot</version> <name>jpa</name> <description>jpa demo project for spring boot</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-devtools</artifactid> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> <version>8.0.18</version> </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> <exclusions> <exclusion> <groupid>org.junit.vintage</groupid> <artifactid>junit-vintage-engine</artifactid> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project>
(2)配置连接
【application.properties】 # 数据库连接配置 spring.datasource.url=jdbc:mysql://localhost:3306/lyh?useunicode=true&characterencoding=utf8 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.driver # jpa 配置 spring.jpa.database=mysql spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update
2、编写实体类以及映射关系
【com.lyh.demo.jpa.bean.employee】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data @proxy(lazy = false) public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; @temporal(temporaltype.timestamp) private date createdate; }
3、编写dao层
不需要编写实现类。只需要继承两个接口(jparepository、jpaspecificationexecutor)。
package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.employee; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.stereotype.component; /** * jparepository<操作的实体类型, 实体类中主键的类型>, 封装了 crud 基本操作。 * jpaspecificationexecutor<操作的实体类型>,封装了复杂的操作,比如 分页。 */ @component public interface employeedao extends jparepository<employee, integer>, jpaspecificationexecutor<employee> { }
4、编写测试类
【com.lyh.demo.jpa.jpaapplicationtests】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import java.util.date; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; /** * 使用 save 方法时,若没有 id,则直接进行 添加操作。 */ @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatedate(new date()); employeedao.save(employee); } /** * 使用 save 方法,若存在 id,会先进行一次查询操作,若存在数据,则更新数据,否则保存数据。 */ @test void testupdate() { employee employee = new employee(); employee.setid(10); employee.setname("tom"); employee.setage((int)(math.random() * 100 + 1)); employee.setcreatedate(new date()); employeedao.save(employee); } /** * 根据 id 查询某条数据 */ @test void testfindone() { system.out.println(employeedao.getone(1)); } /** * 查询所有的数据 */ @test void testfindall() { system.out.println(employeedao.findall()); } /** * 根据id删除数据 */ @test void testdelete() { employeedao.deletebyid(1); } }
测试 save 插入
测试 save 更新。
测试 查询。
测试删除。
5、遇到的坑
(1)执行测试(getone())的时候报错:
org.hibernate.lazyinitializationexception: could not initialize proxy [com.lyh.demo.jpa.bean.employee#1] - no session
原因:
getone() 内部采用懒加载的方式执行,什么时候用,什么时候才会去触发获取值。
解决办法一:
在实体类前加上 @proxy(lazy = false) 用于取消懒加载
【即】 package com.lyh.demo.jpa.bean; import lombok.data; import org.hibernate.annotations.proxy; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data @proxy(lazy = false) public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; @temporal(temporaltype.timestamp) private date createdate; }
解决方法二:
在方法执行前,加上 @transactional。
【即】 /** * 根据 id 查询某条数据 */ @test @transactional void testfindone() { system.out.println(employeedao.getone(2)); }
四、jpa 编写sql语句 -- jpql
1、简介
java persistence query language,可以理解为 jpa 使用的 sql 语句,用于操作实体类以及实体类的属性。
2、使用
(1)在 dao 接口中定义相关方法,并通过 @query 注解来定义 sql 语句。
需要更新数据时,需要使用 @modifying 注解。测试的时候,需要使用 @transactional 注解。
若方法参数为实体类对象,则通过 :#{#实体类名.实体类属性名} 获取。且方法参数需要使用 @param声明。
【com.lyh.demo.jpa.dao.employeedao】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.employee; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.data.jpa.repository.modifying; import org.springframework.data.jpa.repository.query; import org.springframework.data.repository.query.param; import org.springframework.stereotype.component; import java.util.list; /** * jparepository<操作的实体类型, 实体类中主键的类型>, 封装了 crud 基本操作。 * jpaspecificationexecutor<操作的实体类型>,封装了复杂的操作,比如 分页。 * 其中,使用到了方法命名规则写法。 */ @component public interface employeedao extends jparepository<employee, integer>, jpaspecificationexecutor<employee> { public list<employee> getemployeebyage(integer age); @query("from employee where age = ?1") public list<employee> getemployeebyage1(integer age); public list<employee> getemployeebyageandname(integer age, string name); @query("from employee where name = ?2 and age = ?1") public list<employee> getemployeebyageandname1(integer age, string name); @query("update employee set age = :#{#employee.age} where name = :#{#employee.name}") @modifying public void updateempagebyname(@param("employee") employee employee); }
(2)测试
【com.lyh.demo.jpa.jpaapplicationtestjsqls】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.test.annotation.rollback; import javax.transaction.transactional; @springboottest class jpaapplicationtestjsqls { @autowired private employeedao employeedao; @test void testgetemployeebyage() { system.out.println(employeedao.getemployeebyage(40)); } @test void testgetemployeebyage1() { system.out.println(employeedao.getemployeebyage1(40)); } @test void testgetemployeebyageandname() { system.out.println(employeedao.getemployeebyageandname(40, "tom")); } @test void testgetemployeebyageandname1() { system.out.println(employeedao.getemployeebyageandname1(41, "tom")); } @test @transactional void testupdateempagebyname() { employee employee = new employee(); employee.setname("tom"); employee.setage(11); employeedao.updateempagebyname(employee); } }
测试 getemployeebyage
测试 getemployeebyage1,与getemployeebyage 的区别在于 getemployeebyage1 是自定义查询方法。
测试 getemployeebyageandname
测试 getemployeebyageandname1,同样属于自定义查询方法。
测试 updateempagebyname,采用对象传参的方式。进行更新操作 需要使用 @modifying 注解。
3、遇到的坑
(1)报错:(jdbc style parameters (?) are not supported for jpa queries.)
org.springframework.beans.factory.beancreationexception: error creating bean with name 'employeedao': invocation of init method failed; nested exception is java.lang.illegalargumentexception: jdbc style parameters (?) are not supported for jpa queries.
解决: 在占位符上指定匹配的参数位置(从1开始)
【com.lyh.demo.jpa.dao.employeedao】 @query("from employee where age = ?1") public list<employee> getemployeebyage1(integer age);
(2)使用 实体类对象 作为参数进行 jpql 查询,获取实体类某个参数报错。
解决办法:使用 :#{#employee.age} 获取参数。
@query("update employee set age = :#{#employee.age} where name = :#{#employee.name}") @modifying public void updateempagebyname(@param("employee") employee employee);
(3)对于 update、delete 操作,需要使用 @transactional 、 @modifying 注解,否则会报错。
五、jpa 编写 sql 语句 -- sql、方法规则命名查询
1、sql 语法规则
写法类似于 jpql,需要使用 @query 注解,但是需要使用 nativequery = true。
若 nativequery = false,则使用 jpql。
若 nativequery = true,则使用 sql。
(1)配置
【application.properties】 # 数据库连接配置 spring.datasource.url=jdbc:mysql://localhost:3306/lyh?useunicode=true&characterencoding=utf8 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.driver # jpa 配置 spring.jpa.database=mysql spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.mysql5innodbdialect
(2)dao层
【com/lyh/demo/jpa/dao/employeedao.java】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.employee; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.data.jpa.repository.query; import org.springframework.stereotype.component; import java.util.list; @component public interface employeedao extends jparepository<employee, integer>, jpaspecificationexecutor<employee> { @query(value = "select * from emp", nativequery = true) public list<employee> getemployee(); }
(3)bean
实体类。
【com/lyh/demo/jpa/bean/employee.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; @temporal(temporaltype.timestamp) private date createtime; }
(4)test
【com/lyh/demo/jpa/jpaapplicationtests.java】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import java.util.date; import java.util.list; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatetime(new date()); employeedao.save(employee); } @test void testgetemployee() { list<employee> employeelist = employeedao.getemployee(); for (employee employee: employeelist) { system.out.println(employee); } } }
测试截图:
执行 两次 testsave() 方法,添加几条测试数据。
测试 testgetemployee() 方法,测试 sql 语句。
2、方法命名规则查询
是对 jpql 的进一步封装。只需要根据 springdatajpa 提供的方法名规则去定义方法名,从而不需要配置 jpql 语句,会自动根据方法名去解析成 sql 语句。
(1)关键字定义:
https://docs.spring.io/spring-data/jpa/docs/2.2.3.release/reference/html/#repository-query-keywords
详细文档:
https://blog.csdn.net/qq_32448349/article/details/89445216
(2)举例:
findemployeesbyageandname 等价于 select * from emp where age = ? and name = ? 根据属性名称进行查询。 findemployeesbynamelike 等价于 select * from emp where name like ? 根据属性进行模糊查询
(3)测试:
【com/lyh/demo/jpa/dao/employeedao.java】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.employee; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.data.jpa.repository.query; import org.springframework.stereotype.component; import java.util.list; @component public interface employeedao extends jparepository<employee, integer>, jpaspecificationexecutor<employee> { @query(value = "select * from emp", nativequery = true) public list<employee> getemployee(); public list<employee> findemployeesbyageandname(integer age, string name); public list<employee> findemployeesbynamelike(string name); } 【com/lyh/demo/jpa/jpaapplicationtests.java】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import java.util.date; import java.util.list; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatetime(new date()); employeedao.save(employee); } @test void testgetemployee() { list<employee> employeelist = employeedao.getemployee(); for (employee employee: employeelist) { system.out.println(employee); } } @test void testfindemployeesbyageandname() { list<employee> employeelist = employeedao.findemployeesbyageandname(22, "tom"); for (employee employee: employeelist) { system.out.println(employee); } } @test void testfindemployeesbynamelike() { list<employee> employeelist = employeedao.findemployeesbynamelike("t%"); for (employee employee: employeelist) { system.out.println(employee); } } }
基本查询:
模糊查询:
六、动态查询(jpaspecificationexecutor、specification)
1、jpaspecificationexecutor
jpaspecificationexecutor 是一个接口。查询语句都定义在 specification 中。
package org.springframework.data.jpa.repository; import java.util.list; import java.util.optional; import org.springframework.data.domain.page; import org.springframework.data.domain.pageable; import org.springframework.data.domain.sort; import org.springframework.data.jpa.domain.specification; import org.springframework.lang.nullable; public interface jpaspecificationexecutor<t> { // 查询单个对象 optional<t> findone(@nullable specification<t> var1); // 查询对象列表 list<t> findall(@nullable specification<t> var1); // 查询对象列表,并返回分页数据 page<t> findall(@nullable specification<t> var1, pageable var2); // 查询对象列表,并排序 list<t> findall(@nullable specification<t> var1, sort var2); // 统计查询的结果 long count(@nullable specification<t> var1); }
2、specification
定义 sql 语句。同样是一个接口,需要自定义实现类。需要重写 topredicate() 方法。
// root 指查询的根对象,可以获取任何属性。 // criteriaquery 标准查询,可以自定义查询方式(一般不用) // criteriabuilder 指查询的构造器,封装了很多查询条件 predicate topredicate(root<t> var1, criteriaquery<?> var2, criteriabuilder var3);
package org.springframework.data.jpa.domain; import java.io.serializable; import javax.persistence.criteria.criteriabuilder; import javax.persistence.criteria.criteriaquery; import javax.persistence.criteria.predicate; import javax.persistence.criteria.root; import org.springframework.lang.nullable; public interface specification<t> extends serializable { long serialversionuid = 1l; static <t> specification<t> not(@nullable specification<t> spec) { return spec == null ? (root, query, builder) -> { return null; } : (root, query, builder) -> { return builder.not(spec.topredicate(root, query, builder)); }; } @nullable static <t> specification<t> where(@nullable specification<t> spec) { return spec == null ? (root, query, builder) -> { return null; } : spec; } @nullable default specification<t> and(@nullable specification<t> other) { return specificationcomposition.composed(this, other, (builder, left, rhs) -> { return builder.and(left, rhs); }); } @nullable default specification<t> or(@nullable specification<t> other) { return specificationcomposition.composed(this, other, (builder, left, rhs) -> { return builder.or(left, rhs); }); } @nullable predicate topredicate(root<t> var1, criteriaquery<?> var2, criteriabuilder var3); }
3、基本使用
(1)步骤:
step1:实现 specification 接口(定义泛型,为查询的对象类型),重写 topredicate() 方法。
step2:定义 criteriabuilder 查询条件。
(2)普通查询
package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.data.jpa.domain.specification; import javax.persistence.criteria.*; import java.util.date; import java.util.list; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatetime(new date()); employeedao.save(employee); } @test void testspecification() { // 定义内部类,泛型为 查询的对象 specification<employee> specification = new specification<employee>() { @override public predicate topredicate(root root, criteriaquery criteriaquery, criteriabuilder criteriabuilder) { // 获取比较的属性 path<object> name = root.get("name"); // 构建查询条件, select * from emp where name = "tom"; predicate predicate = criteriabuilder.equal(name, "tom"); return predicate; } }; list<employee> employeelist = employeedao.findall(specification); for (employee employee : employeelist) { system.out.println(employee); } } }
(3)多条件拼接、模糊查询
package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.data.jpa.domain.specification; import javax.persistence.criteria.*; import java.util.date; import java.util.list; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatetime(new date()); employeedao.save(employee); } @test void testspecification() { // 定义内部类,泛型为 查询的对象 specification<employee> specification = new specification<employee>() { @override public predicate topredicate(root root, criteriaquery criteriaquery, criteriabuilder criteriabuilder) { // 获取比较的属性 path<string> name = root.get("name"); path<integer> age = root.get("age"); // 构建查询条件, select * from emp where name like "to%" and age >= 22; predicate predicate1 = criteriabuilder.like(name, "to%"); predicate predicate2 = criteriabuilder.ge(age, 22); predicate predicate = criteriabuilder.and(predicate1, predicate2); return predicate; } }; list<employee> employeelist = employeedao.findall(specification); for (employee employee : employeelist) { system.out.println(employee); } } }
(4)排序
在上例 多条件拼接 代码的基础上增加排序,使数据按照 id 降序输出。
package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.data.domain.sort; import org.springframework.data.jpa.domain.specification; import javax.persistence.criteria.*; import java.util.date; import java.util.list; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatetime(new date()); employeedao.save(employee); } @test void testspecification() { // 定义内部类,泛型为 查询的对象 specification<employee> specification = new specification<employee>() { @override public predicate topredicate(root root, criteriaquery criteriaquery, criteriabuilder criteriabuilder) { // 获取比较的属性 path<string> name = root.get("name"); path<integer> age = root.get("age"); // 构建查询条件, select * from emp where name like "to%" and age >= 22; predicate predicate1 = criteriabuilder.like(name, "to%"); predicate predicate2 = criteriabuilder.ge(age, 22); predicate predicate = criteriabuilder.and(predicate1, predicate2); return predicate; } }; // 定义排序(sort.direction.desc,降序; sort.direction.asc,升序) sort sort = sort.by(sort.direction.desc, "id"); list<employee> employeelist = employeedao.findall(specification, sort); for (employee employee : employeelist) { system.out.println(employee); } } }
(5)分页
在上例 多条件拼接 代码的基础上增加分页。如下例,按每页一条数据分页,取第2页数据。
package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.data.domain.page; import org.springframework.data.domain.pagerequest; import org.springframework.data.domain.pageable; import org.springframework.data.jpa.domain.specification; import javax.persistence.criteria.*; import java.util.date; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); employee.setcreatetime(new date()); employeedao.save(employee); } @test void testspecification() { // 定义内部类,泛型为 查询的对象 specification<employee> specification = new specification<employee>() { @override public predicate topredicate(root root, criteriaquery criteriaquery, criteriabuilder criteriabuilder) { // 获取比较的属性 path<string> name = root.get("name"); path<integer> age = root.get("age"); // 构建查询条件, select * from emp where name like "to%" and age >= 22; predicate predicate1 = criteriabuilder.like(name, "to%"); predicate predicate2 = criteriabuilder.ge(age, 22); predicate predicate = criteriabuilder.and(predicate1, predicate2); return predicate; } }; // 定义分页,其中 第一个参数指的是 当前查询的页数(从0开始),第二个参数指的是每页的数量 pageable pageable = pagerequest.of(1, 1); page<employee> page = employeedao.findall(specification, pageable); // 获取当前查询数据的集合 system.out.println(page.getcontent()); // 获取总条数 system.out.println(page.gettotalelements()); // 获取总页数 system.out.println(page.gettotalpages()); } }
七、多表操作
1、一对一(@onetoone)
表的某条数据,对应另外一张表的某条数据。
2、一对多(@onetomany,@manytoone)
(1)基本概念:
表的某条数据,对应另外一张表的多条数据。
将 “一” 的一方称为 :主表。
将 “多” 的一方称为 :从表。
通常将 外键 置于从表上,即 从表上增加一列作为外键,并依赖于主表的某列。
(2)sql 语句建表
【举例:】 员工与部门间的关系。 一个部门可以有多个员工,而一个员工属于一个部门。此时部门与员工间为 一对多 的关系。 部门表为主表,员工表为从表。外键建立在 员工表(从表)上。 create table dept ( deptid int primary key auto_increment, deptname varchar(20) ); create table emp ( id int primary key auto_increment, name varchar(32), age int, deptid int, foreign key(deptid) references dept(deptid) );
(3)jpa建表
【步骤:】 step1:明确两表之间的关系 step2:确定表之间的关系,一对多(外键)还是多对多(中间表)关系。 step3:编写实体类,在实体类中建立表关系(声明相应的属性)。 step4:配置映射关系
step1、step2:
部门表 与 员工表间 属于 一对多的关系,所以需要在员工表上建立外键。
【com/lyh/demo/jpa/bean/employee.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; } 【com/lyh/demo/jpa/bean/department.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; @entity @table(name = "dept") @data public class department { @id @generatedvalue(strategy = generationtype.identity) private int deptid; private string deptname; }
step3、step4:
在实体类间建立联系,并添加映射关系。
【com/lyh/demo/jpa/bean/employee.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; /** * 员工表与部门表间 属于 多对一的关系。所以在员工类中应定义 一个普通属性去保存部门信息。 * 并使用 @manytoone 去定义映射关系(多对一). * 使用 @joincolumn 定义外键(在从表上定义,name指的是 外键名,referencedcolumnname指的是依赖的主表的主键)。 */ @manytoone(targetentity = department.class) @joincolumn(name = "deptid", referencedcolumnname = "deptid") private department department; } 【com/lyh/demo/jpa/bean/department.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.hashset; import java.util.set; @entity @table(name = "dept") @data public class department { @id @generatedvalue(strategy = generationtype.identity) private int deptid; private string deptname; /** * 部门表与员工表是一对多的关系,所以部门实体类中 应定义集合 去保存员工信息。 * 并使用 @onetomany 去指定映射关系(一对多)。 * 可以使用 @joincolumn 去建立外键,此时可以对外键进行维护(一的一方),若对此外键赋值,相对于多的一方,会多出一条 update。 * 若放弃外键维护,可以使用 mapperby 指定关联关系,其值为对应的类维护的属性名称。 */ // @onetomany(targetentity = employee.class) // @joincolumn(name = "id", referencedcolumnname = "deptid") @onetomany(mappedby = "department") private set<employee> employees = new hashset<employee>(); }
(4)测试
文件结构如下图:
代码:
【application.properties】 # 数据库连接配置 spring.datasource.url=jdbc:mysql://localhost:3306/lyh?useunicode=true&characterencoding=utf8 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.driver # jpa 配置 spring.jpa.database=mysql spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.mysql5innodbdialect 【com/lyh/demo/jpa/bean/department.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.hashset; import java.util.set; @entity @table(name = "dept") @data public class department { @id @generatedvalue(strategy = generationtype.identity) private int deptid; private string deptname; /** * 部门表与员工表是一对多的关系,所以部门实体类中 应定义集合 去保存员工信息。 * 并使用 @onetomany 去指定映射关系(一对多)。 * 可以使用 @joincolumn 去建立外键,此时可以对外键进行维护(一的一方),若对此外键赋值,相对于多的一方,会多出一条 update。 * 若放弃外键维护,可以使用 mapperby 指定关联关系,其值为对应的类维护的属性名称。 */ // @onetomany(targetentity = employee.class) // @joincolumn(name = "id", referencedcolumnname = "deptid") @onetomany(mappedby = "department") private set<employee> employees = new hashset<employee>(); } 【com/lyh/demo/jpa/bean/employee.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; /** * 员工表与部门表间 属于 多对一的关系。所以在员工类中应定义 一个普通属性去保存部门信息。 * 并使用 @manytoone 去定义映射关系(多对一). * 使用 @joincolumn 定义外键(在从表上定义,name指的是 外键名,referencedcolumnname指的是依赖的主表的主键)。 */ @manytoone(targetentity = department.class) @joincolumn(name = "deptid", referencedcolumnname = "deptid") private department department; } 【com/lyh/demo/jpa/dao/departmentdao.java】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.department; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.stereotype.component; @component public interface departmentdao extends jparepository<department, integer>, jpaspecificationexecutor<department> { } 【com/lyh/demo/jpa/dao/employeedao.java】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.employee; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.data.jpa.repository.query; import org.springframework.stereotype.component; import java.util.list; @component public interface employeedao extends jparepository<employee, integer>, jpaspecificationexecutor<employee> { } 【com/lyh/demo/jpa/jpaapplicationtests.java】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.department; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.departmentdao; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.data.domain.page; import org.springframework.data.domain.pagerequest; import org.springframework.data.domain.pageable; import org.springframework.data.jpa.domain.specification; import org.springframework.transaction.annotation.transactional; import javax.persistence.criteria.*; import java.util.date; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @autowired private departmentdao departmentdao; @test void testsave1() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); department department = new department(); department.setdeptid(1); department.setdeptname("开发"); employeedao.save(employee); departmentdao.save(department); } @test void testsave2() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); department department = new department(); department.setdeptid(1); department.setdeptname("开发"); // 维护外键,即添加值(此处执行顺序可能会导致出错) employee.setdepartment(department); // departmentdao.save(department); employeedao.save(employee); departmentdao.save(department); } }
测试截图:
测试 testsave1(),由于没有维护外键,所以外键为 null。
测试 testsave2(),维护外键,外键有值。
(5)级联操作
注意,上例操作,需要对每个表进行一次操作,这样有时候会很繁琐。
此时级联就可以派上用场了,级联用于 操作一个实体类的同时 操作其关联的另一个实体类。
上例 testsave2() 可能会出现的问题:当数据为空时,由于先执行了 employeedao.save(employee);
再执行的 departmentdao.save(department); 此时由于 主表没有数据, 从表添加外键会出错。
解决方法一:
调换执行 sql 的顺序。
解决方法二:
采用级联属性(cascade = cascadetype.all)。
修改上例代码。
【com/lyh/demo/jpa/bean/department.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.hashset; import java.util.set; @entity @table(name = "dept") public class department { @id @generatedvalue(strategy = generationtype.identity) private int deptid; private string deptname; /** * 部门表与员工表是一对多的关系,所以部门实体类中 应定义集合 去保存员工信息。 * 并使用 @onetomany 去指定映射关系(一对多)。 * 可以使用 @joincolumn 去建立外键,此时可以对外键进行维护(一的一方),若对此外键赋值,相对于多的一方,会多出一条 update。 * 若放弃外键维护,可以使用 mapperby 指定关联关系,其值为对应的类维护的属性名称。 * 使用 cascade 用于定义级联属性。 */ // @onetomany(targetentity = employee.class) // @joincolumn(name = "id", referencedcolumnname = "deptid") @onetomany(mappedby = "department", cascade = cascadetype.all) private set<employee> employees = new hashset<employee>(); public int getdeptid() { return deptid; } public void setdeptid(int deptid) { this.deptid = deptid; } public string getdeptname() { return deptname; } public void setdeptname(string deptname) { this.deptname = deptname; } public set<employee> getemployees() { return employees; } public void setemployees(set<employee> employees) { this.employees = employees; } } 【com/lyh/demo/jpa/jpaapplicationtests.java】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.department; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.departmentdao; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.data.domain.page; import org.springframework.data.domain.pagerequest; import org.springframework.data.domain.pageable; import org.springframework.data.jpa.domain.specification; import org.springframework.transaction.annotation.transactional; import javax.persistence.criteria.*; import java.util.date; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @autowired private departmentdao departmentdao; @test void testsave1() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); department department = new department(); department.setdeptid(1); department.setdeptname("开发"); employeedao.save(employee); departmentdao.save(department); } @test void testsave2() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); department department = new department(); department.setdeptid(1); department.setdeptname("开发"); // 维护外键,即添加值 employee.setdepartment(department); department.getemployees().add(employee); departmentdao.save(department); } }
注:
使用级联遇到的坑(堆栈溢出 java.lang.*error)。去除 @data,手动 getter、setter。或者重写 tostring() 方法,让其不输出 外键关联的属性。
(6)对象导航查询
通过查询一个对象,可以查询到其关联的对象。
对于 一对多 关系,若从 一 的对象 去 查询 多的对象,则默认采用延迟加载的形式。
若从 多 的对象 去 查询 一的对象,则默认采用立即加载的形式。
对上例代码进行修改。 【com/lyh/demo/jpa/bean/department.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.hashset; import java.util.set; @entity @table(name = "dept") public class department { @id @generatedvalue(strategy = generationtype.identity) private int deptid; private string deptname; /** * 部门表与员工表是一对多的关系,所以部门实体类中 应定义集合 去保存员工信息。 * 并使用 @onetomany 去指定映射关系(一对多)。 * 可以使用 @joincolumn 去建立外键,此时可以对外键进行维护(一的一方),若对此外键赋值,相对于多的一方,会多出一条 update。 * 若放弃外键维护,可以使用 mapperby 指定关联关系,其值为对应的类维护的属性名称。 * 使用 cascade 用于定义级联属性。 */ // @onetomany(targetentity = employee.class) // @joincolumn(name = "id", referencedcolumnname = "deptid") @onetomany(mappedby = "department", cascade = cascadetype.all) private set<employee> employees = new hashset<employee>(); public int getdeptid() { return deptid; } public void setdeptid(int deptid) { this.deptid = deptid; } public string getdeptname() { return deptname; } public void setdeptname(string deptname) { this.deptname = deptname; } public set<employee> getemployees() { return employees; } public void setemployees(set<employee> employees) { this.employees = employees; } @override public string tostring() { return "department{" + "deptid=" + deptid + ", deptname='" + deptname + '}'; } } 【com/lyh/demo/jpa/jpaapplicationtests.java】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.department; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.dao.departmentdao; import com.lyh.demo.jpa.dao.employeedao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.transaction.annotation.transactional; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @autowired private departmentdao departmentdao; /** * 测试级联添加数据 */ @test void testsave() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); department department = new department(); department.setdeptid(1); department.setdeptname("开发"); // 维护外键,即添加值 employee.setdepartment(department); department.getemployees().add(employee); departmentdao.save(department); } /** * 测试对象查询(获取多 的一方的对象,并获取其关联的对象。其默认加载方式为 立即加载。) */ @test @transactional void testobjectqueryonefrommany() { employee employee = employeedao.getone(1); system.out.println(employee.getdepartment()); } /** * 测试对象查询(获取一 的一方的对象,并获取其关联的对象。其默认加载方式为 延迟加载。) */ @test @transactional void testobjectquerymanyfromone() { department department = departmentdao.getone(1); system.out.println(department.getemployees()); } }
测试 testobjectqueryonetomany()。
测试 testobjectquerymanyfromone()。
3、多对多(@manytomany)
(1)基本概念:
两张表之间互为一对多的关系。
采用中间表来维护 两表间的关系。中间表至少由两个字段组成,且这两个字段作为外键指向两张表的主键,形成联合主键。
(2)sql 建表
类似于 一对多关系。
【举例:】 员工表 与 角色表。 一个员工可以对应多个角色,一个角色可以对应多个员工。员工与角色之间是多对多关系。 需要建立中间表。 drop table emp_and_role; drop table emp; drop table role; create table role ( roleid int primary key auto_increment, rolename varchar(32) ); create table emp ( id int primary key auto_increment, name varchar(32), age int ); create table emp_and_role ( emp_id int, role_id int, primary key(emp_id, role_id), foreign key(emp_id) references emp(id), foreign key(role_id) references role(roleid) );
(3)jpa 建表
【步骤:】 step1:明确两表之间的关系 step2:确定表之间的关系,一对多(外键)还是多对多(中间表)关系。 step3:编写实体类,在实体类中建立表关系(声明相应的属性)。 step4:配置映射关系
step1、step2:
员工表、角色表为多对多关系,所以需建立中间表。
【com/lyh/demo/jpa/bean/employee.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; @entity @table(name = "emp") @data public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; } 【com/lyh/demo/jpa/bean/role.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; @entity @table(name = "role") @data public class role { @id @generatedvalue(strategy = generationtype.identity) private integer roleid; @column(length = 32) private string rolename; } 【com/lyh/demo/jpa/dao/employeedao.java】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.employee; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.stereotype.component; @component public interface employeedao extends jparepository<employee, integer>, jpaspecificationexecutor<employee> { } 【com/lyh/demo/jpa/dao/roledao.java】 package com.lyh.demo.jpa.dao; import com.lyh.demo.jpa.bean.role; import org.springframework.data.jpa.repository.jparepository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.stereotype.component; @component public interface roledao extends jparepository<role, integer>, jpaspecificationexecutor<role> { }
step3、step4:
配置映射关系。
【com/lyh/demo/jpa/bean/employee.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.date; import java.util.hashset; import java.util.set; @entity @table(name = "emp") @data public class employee { @id @generatedvalue(strategy = generationtype.identity) private integer id; @column(name = "name", length = 32) private string name; @column(name = "age") private integer age; /** * 配置多对多关系。 * @jointable 为配置中间表。 * 其中: * name:中间表名。 * joincolumns:定义外键,并关联于当前类的主键。 * inversejoincolumns:定义外键,并关联于另一个类的主键。 */ @manytomany(targetentity = role.class) @jointable(name = "emp_and_role", joincolumns = {@joincolumn(name = "emp_id", referencedcolumnname = "id")}, inversejoincolumns = {@joincolumn(name = "role_id", referencedcolumnname = "roleid")}) private set<role> roleset = new hashset<>(); } 【com/lyh/demo/jpa/bean/role.java】 package com.lyh.demo.jpa.bean; import lombok.data; import javax.persistence.*; import java.util.hashset; import java.util.set; @entity @table(name = "role") public class role { @id @generatedvalue(strategy = generationtype.identity) private integer roleid; @column(length = 32) private string rolename; /** * 放弃外键维护权。 * 并定义级联属性。 */ @manytomany(mappedby = "roleset", cascade = cascadetype.all) private set<employee> employeeset = new hashset<>(); public integer getroleid() { return roleid; } public void setroleid(integer roleid) { this.roleid = roleid; } public string getrolename() { return rolename; } public void setrolename(string rolename) { this.rolename = rolename; } public set<employee> getemployeeset() { return employeeset; } public void setemployeeset(set<employee> employeeset) { this.employeeset = employeeset; } @override public string tostring() { return "role{" + "roleid=" + roleid + ", rolename='" + rolename + '\'' + ", employeeset=" + employeeset + '}'; } }
(4)测试(使用级联赋值)
【com/lyh/demo/jpa/jpaapplicationtests.java】 package com.lyh.demo.jpa; import com.lyh.demo.jpa.bean.employee; import com.lyh.demo.jpa.bean.role; import com.lyh.demo.jpa.dao.employeedao; import com.lyh.demo.jpa.dao.roledao; import org.junit.jupiter.api.test; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; @springboottest class jpaapplicationtests { @autowired private employeedao employeedao; @autowired private roledao roledao; @test void testsave1() { employee employee = new employee(); employee.setname("tom"); employee.setage(22); role role = new role(); role.setrolename("经理"); // 维护外键 employee.getroleset().add(role); role.getemployeeset().add(employee); // 使用级联赋值 roledao.save(role); } }
上一篇: tp5.1批量删除商品