在前面的章节已经讲述了SpringDataJpa的CRUD操作以及其底层代理实现的分析,下面介绍SpringDataJpa中的复杂查询和动态查询,多表查询。(保姆级教程)
文章字数较多,请各位按需阅读。
不清楚JPA的小伙伴可以参考这篇文章:JPA简介;
不清楚SpringDataJPA环境搭建的小伙伴可以参考这篇文章:SpringDataJPA入门案例;
想了解SpringDataJPA代理类实现过程可以参考这篇文章:SpringDadaJPA底层实现原理
如需转载,请注明出处。
1.复杂查询
i.方法名称规则查询
方法名查询:只需要按照SpringDataJpa提供的方法名称规则去定义方法,在dao接口中定义方法即可。
其中对于方法的名称有一套约定。
KeyWord | Sample | JPQL |
---|---|---|
And | findByLastnameAndFirstname | where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | where x.lastname = ?1 or x.firstname = ?2 |
Between | findByAgeBetween | where x.Age between ?1 and ?2 |
LessThan | findByAgeLessThan | where x.age < ?1 |
GreaterThan | findByAgeGreaterThan | where x.age > ?1 |
Like | findByFirstnameLike | where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | where x.firstname not like ?1 |
TRUE | findByActiveTrue() | where x.active = true |
FALSE | findByActiveFalse() | where x.active = false |
public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
/**
* 方法名的约定:
* findBy:查询
* 对象中的属性名(首字母大写):查询条件
* *默认情况:使用 =的方式查询
* 特殊的查询方式,比如模糊查询
* findByCustName-----根据客户名称查询 findBy表示要查询 CustName属性名
* springDataJpa在运行阶段
* 会根据方法名称进行解析 findBy from XXX(实体类)
* 属性名称 where custName
* 1. findBy+属性名称(根据属性名称进行完成匹配任务)
* 2. findBy+属性名称+查询方式(Like|isnull)
* 3. 多条件查询
* findBy+属性名称+查询方式+多条件连接符(and|or)+属性名+查询方式
*/
public List<Customer> findByCustName(String name);
//查询id为3且name中含有大学的用户
public Customer findByCustId(Long id);
public Customer findByCustIdAndCustNameLike(Long id,String name);
}
ii.JPQL查询
使用 Spring Data JPA 提供的查询方法已经可以解决大部分的应用场景,但是对于某些业务来 说,我们还需要灵活的构造查询条件,这时就可以使用@Query 注解,结合 JPQL 的语句方式完成 查询 。
@Query 注解的使用非常简单,只需在方法上面标注该注解,同时提供一个 JPQL 查询语句即可
注意:
通过使用 @Query 来执行一个更新操作,为此,我们需要在使用 @Query 的同时,用 @Modifying 来将该操作标识为修改查询,这样框架最终会生成一个更新的操作,而非查询 。
public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
/** * 1.根据客户名称查询客户 * jpql:from Customer where custName=? */
@Query(value="from Customer where custName =?")
public List<Customer> findCustomerJpql(String name);
/** * 2.根据客户名称和客户id查询 * 对多个占位符参数 * 默认情况下,占位符的位置需要和方法参数中的位置保持一致 * 也可以指定占位符参数的位置(注意:中间不要有空格) * ? 索引的方式,指定此占位符的取值来源 eg ?2表示此占位符对应第二个参数 */
@Query(value="from Customer where custName=?2 and custId=?1")
public Customer findByNameAndId(Long id,String name);
/** * 3.根据id更新客户的name * sql:update cst_customer set cust_name=? where cust_id=? * jpql:update Customer set custName=? where custId=? * * @query:代表的是进行查询 * 需要声明此方法是执行更新操作 * 使用 @Modifying */
@Query(value = "update Customer set custName=? where custId=?")
@Modifying
public void updateCustomerName(String name,Long id);
}
注意:在执行springDataJpa中使用jpql完成更新,删除操作时,需要手动添加事务的支持 必须的;因为默认会执行结束后,回滚事务。
@Test
@Transactional//添加事务的支持
@Rollback(value = false)
public void updateCustomerName(){
customerDao.updateCustomerName("学生公寓",4L);
}
iii.SQL查询
Spring Data JPA 同样也支持 sql 语句的查询,如下:
/**
* 查询所有用户:使用sql查询
* Sql:select * from cst_customer
* nativeQuery = true配置查询方式,true表示Sql查询,false表示Jpql查询
* 注意:返回值是一个Object[]类型的list
*/
// @Query(value = "select * from cst_customer",nativeQuery = true)
// public List<Object []>findSql();
@Query(value = "select * from cst_customer where cust_name like ?",nativeQuery = true)
public List<Object []>findSql(String name);
2.动态查询
springdatajpa的接口规范:
-
JpaRepository<操作的实体类型,实体类型中的 主键 属性的类型>
封装了基本的CRUD的操作,分页等;
-
JpaSpecificationExecutor<操作的实体类类型>
封装了复杂查询。
上述查询方法使用到的是接口JpaRepository中的方法,下面分析JpaSpecificationExecutor中的方法。
i.为什么需要动态查询
可能有些许疑惑,为什么还需要动态查询呢?有时候我们在查询某个实体的时候哦,给定的查询条件不是固定的,这个时候就需要动态构建相应的查询语句,可以理解为上述的查询条件是定义在dao接口中的,而动态查询条件定义在实现类中。
ii.JpaSpecificationExecutor中定义的方法
public interface JpaSpecificationExecutor<T> {
T findOne(Specification<T> var1);
List<T> findAll(Specification<T> var1);
Page<T> findAll(Specification<T> var1, Pageable var2);
List<T> findAll(Specification<T> var1, Sort var2);
long count(Specification<T> var1);
}
在上述方法中,我们可以看到接口Specification。可以简单理解为,Specification构造的就是查询条件。我们看看Specification中定义的方法。
/* * root :T表示查询对象的类型,代表查询的根对象,可以通过root获取实体中的属性 * query :代表一个顶层查询对象,用来自定义查询 * cb :用来构建查询,此对象里有很多条件方法 **/
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
与上述查询方法不同,复杂查询定义在dao接口中,而动态查询定义在实现类中。
1)单条件查询
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
@Test
public void conditionTest(){
/** * 自定义查询条件 * 1.实现Specification接口(提供泛型:查询对象类型,需要那个对象就写哪个泛型) * 2.实现toPredicate方法(构造查询条件) * 3.需要借书方法参数中的两个形参 * root:用于获取查询的对象属性 * CriteriaBuilder:构造查询条件,内部封装了很多的查询条件(例如:模糊匹配,精准匹配) * 需求:根据客户名称查询,查询客户名称为大学 * 查询条件 * 1.查询方法 (精准匹配,是否为空...) * CriteriaBuilder对象 * 2.比较的属性名称(与哪个字段去以什么方式去比较) * root对象 */
Specification<Customer> spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
//1.获取比较的属性(不是字段名)
Path<Object> custName = root.get("custName");
//2.构造查询条件
/** * 第一个参数:需要比较的属性(Path) * 第二个参数:当前比较的取值 */
Predicate predicate = cb.equal(custName, "三峡大学");//进行精准匹配 (比较的属性,比较的属性的取值)
return predicate;
}
};
//根据返回的对象个数选择findOne或者findAll
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
}
2)多条件查询
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 多条件查询:根据用户名和所属行业进行查询
* root:获取属性
* 用户名
* 所属行业
* cb:构造查询
* 1.构造客户名的精准匹配查询
* 2.构造所属行业的精准匹配查询
* 3,将以上两个查询联系起来
*/
@Test
public void findByNmaeAndIndustray(){
Specification<Customer> spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
//1.获取属性
Path<Object> custName = root.get("custName");
Path<Object> industry = root.get("custIndustry");
//2.构造查询
Predicate p1 = cb.equal(custName, "6测试数据-coderxz");
Predicate p2 = cb.equal(industry, "6测试数据-java工程师");
//3。将多个查询条件组合到一起(and/or)
Predicate predicate = cb.and(p1, p2);
return predicate;
}
};
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
}
3)模糊查询
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/** * 案例:根据客户名称进行模糊配置,返回客户列表 * * equal:直接的path对象(属性),然后直接进行比较即可 * * 对于gt,lt,le,like:得到path对象,根据path对象指定比较参数的类型(字符串or数字...),再进行比较 * 指定参数类型 path.as(类型的字节码对象) */
@Test
public void findVagueCustomer(){
Specification<Customer>spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path<Object> custName = root.get("custName");
Predicate predicate = criteriaBuilder.like(custName.as(String.class), "%大学%");
return predicate;
}
};
List<Customer> customers = customerDao.findAll(spec);
for(Customer c:customers){
System.out.println(c);
}
}
}
4)分页查询
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/** * 分页查询 * findAll(Pageable) 没有条件的分页查询 * findAll(Specification,Pageable) * Specification查询条件 * Pageable分页参数 查询的页码,每页查询的条件 * 返回:Pahe(StringDataJpa)为我们封装好的pageBean对象,数据列表, */
@Test
public void pageCustomer(){
Specification<Customer> spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
return null;
}
};
/** * Pageable 接口 * PageRequest是其实现类 * 第一个参数:当前查询的页数(从0开始) * 第二个参数:每页查询的数量 * 注意:在新版本的jpa中,此方法已过时,新方法是PageRequest.of(page,size) */
Pageable pageable = new PageRequest(0,1);
//分页查询 page是SpringDataJpa为我们封装的一个JavaBean
Page<Customer> page = customerDao.findAll(spec, pageable);
//获得总页数(这些数据需要分几页)
System.out.println("查询总页数:"+page.getTotalPages());
//获得总记录数(数据库的总记录数)
System.out.println("查询总记录数:"+page.getTotalElements());
//得到数据集合列表
System.out.println("数据集合列表:"+page.getContent());
}
}
5)对查询结果进行排序
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/** * 对查询结果进行排序 */
@Test
public void findSortCustomer(){
Specification<Customer>spec=new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Path<Object> custName = root.get("custName");
Predicate predicate = criteriaBuilder.like(custName.as(String.class), "%大学%");
return predicate;
}
};
/** *创建排序对象,需要调用构造方法实例化对象 * 第一个参数:排序的顺序(正序,倒序) * sort.Direction.DESC:倒序 * sort.Direction.ASC:升序 * 第二个参数:排序的属性名称 */
Sort sort = new Sort(Sort.Direction.DESC, "custId");
List<Customer> customers = customerDao.findAll(spec,sort);
for(Customer c:customers){
System.out.println(c);
}
}
}
3.多表查询
上述复杂查询和动态查询都是基于单表查询,只需要指定实体类与数据库表中一对一的映射。而多表查询需要修改实体类之间的映射关系。
在数据库中表与表之间,存在三种关系:多对多、一对多、一对一。
那么与之对应的实体映射也应该有三种关系。那么在JPA中表的关系如何分析呢?
1.建立表与表之间的关系
- 第一步:首先确定两张表之间的关系。 如果关系确定错了,后面做的所有操作就都不可能正确。
- 第二步:在数据库中实现两张表的关系
- 第三步:在实体类中描述出两个实体的关系
- 第四步:配置出实体类和数据库表的关系映射(重点)
4.JPA中的一对多
案例分析:
采用两个实体对象:公司与员工
在不考虑兼职的情况下,每名员工对应一家公司,每家公司有多名员工。
在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对 多的关系,需要使用数据库的外键约束。
**什么是外键?**指的是从表中有一列,取值参照主表中的主键,这一列就是外键。
数据库表:
CREATE TABLE `cst_customer` (
`cust_id` bigint(20) NOT NULL AUTO_INCREMENT,
`cust_address` varchar(255) DEFAULT NULL,
`cust_industry` varchar(255) DEFAULT NULL,
`cust_level` varchar(255) DEFAULT NULL,
`cust_name` varchar(255) DEFAULT NULL,
`cust_phone` varchar(255) DEFAULT NULL,
`cust_source` varchar(255) DEFAULT NULL,
PRIMARY KEY (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;
CREATE TABLE `cst_linkman` (
`lkm_id` bigint(20) NOT NULL AUTO_INCREMENT,
`lkm_email` varchar(255) DEFAULT NULL,
`lkm_gender` varchar(255) DEFAULT NULL,
`lkm_memo` varchar(255) DEFAULT NULL,
`lkm_mobile` varchar(255) DEFAULT NULL,
`lkm_name` varchar(255) DEFAULT NULL,
`lkm_phone` varchar(255) DEFAULT NULL,
`lkm_position` varchar(255) DEFAULT NULL,
`lkm_cust_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`lkm_id`),
KEY `FKh9yp1nql5227xxcopuxqx2e7q` (`lkm_cust_id`),
CONSTRAINT `FKh9yp1nql5227xxcopuxqx2e7q` FOREIGN KEY (`lkm_cust_id`) REFERENCES `cst_customer` (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
1.建立实体与表之间的映射关系
注意:使用的注解都是JPA规范的,导包需要导入javac.persistence下的包
package ctgu.pojo;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
/** *我们需要配置: * 1.实体类与表的映射关系(此pojo与数据库中的那一张表关系映射) * @ Entity * @ Table(name="cst_customer")name表示数据库中表的名称 * 2.实体类中属性与表中字段的映射关系 * @ Id声明主键的设置 * @ GeneratedValue配置主键是生成策略(自动增长) * strategy= * GenerationType.IDENTITY:自增 Mysql(底层数据库支持的自增长方式对id自增) * GenerationType.SEQUENCE:序列 Oracle(底层数据库必须支持序列) * GenerationType.TABLE:jpa提供的一种机制,通过一张数据库表的形式帮助我们完成自增 * GenerationType.AUTO:有程序自动的帮助我们选择主键生成策略 * @ Column(name = "cust_id")数据库中表中字段的名字 */
@Entity
@Table(name = "cst_customer")
public class Customer {
/** * @ Id声明主键的设置 * @ GeneratedValue配置主键是生成策略(自动增长) * GenerationType.IDENTITY * @ Column(name = "cust_id")数据库中表中字段的名字 */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "cust_id")
private Long custId;
@Column(name = "cust_name")
private String custName;
@Column(name = "cust_source")
private String custSource;
@Column(name = "cust_industry")
private String custIndustry;
@Column(name = "cust_level")
private String custLevel;
@Column(name = "cust_address")
private String custAddress;
@Column(name = "cust_phone")
private String custPhone;
/** * 配置客户与联系人之间的关系(一个客户对应多个联系人) * 使用注解的形式配置多表关系 * 1 声明关系 * @ OnetoMany:配置一对多关系 * targetEntity:对方对象的字节码对象 * 2.配置外键(中间表) * @ JoinColumn * name:外键的在从表的字段名称(不是属性,是数据库的字段名称) * referencedColumnName:参照的主表的字段名称 */
@OneToMany(targetEntity = LinkMan.class)
@JoinColumn(name = "lkm_cust_id",referencedColumnName = "cust_id")
private Set<LinkMan> linkMans=new HashSet<>();
/* get/set/toString()方法略...... */
}
package ctgu.pojo;
import javax.persistence.*;
@Entity
@Table(name="cst_linkman")
public class LinkMan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="lkm_id")
private Long lkmId;
@Column(name="lkm_name")
private String lkmName;
@Column(name="lkm_gender")
private String lkmGender;
@Column(name="lkm_phone")
private String lkmPhone;
@Column(name="lkm_mobile")
private String lkmMobile;
@Column(name="lkm_email")
private String lkmEmail;
@Column(name="lkm_position")
private String lkmPosition;
@Column(name="lkm_memo")
private String lkmMemo;
/** * 配置联系人到客户的多对一关系 * 外键字段是设置在从表中的,且该字段并未作为对象的属性去配置,而实作为外键去配置 * * 使用注解的形式配置多对一关系 * 1.配置表关系 * @ManyToOne : 配置多对一关系 * targetEntity:对方的实体类字节码 * 2.配置外键(中间表) * * * 配置外键的过程,配置到了多的一方,就会在多的一方维护外键 * */
@ManyToOne(targetEntity = Customer.class,fetch = FetchType.LAZY)
@JoinColumn(name = "lkm_cust_id",referencedColumnName = "cust_id")
private Customer customer;
/* get/set/toString略... */
}
注意:在上述实体中,均对外键进行了维护。
2.映射的注解说明
i.@OneToMany
作用:建立一对多的关系映射 属性:
- targetEntityClass:指定多的多方的类的字节码(常用)
- mappedBy:指定从表实体类中引用主表对象的名称。(常用)
- cascade:指定要使用的级联操作
- fetch:指定是否采用延迟加载
- orphanRemoval:是否使用孤儿删除
ii.@ManyToOne
作用:建立多对一的关系 属性:
- targetEntityClass:指定一的一方实体类字节码(常用)
- cascade:指定要使用的级联操作
- fetch:指定是否采用延迟加载
- optional:关联是否可选。如果设置为 false,则必须始终存在非空关系。
iii.@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。 属性:
- name:指定外键字段的名称(常用)
- referencedColumnName:指定引用主表的主键字段名称(常用)
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
3.一对多测试
i.保存公司和联系人
package ctgu.OntoMany;
import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class OntoManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/** * 保存一个客户,保存一个联系人 * 现象:从表(联系人)的外键为空 * 原因: * 主表中没有配置关系 */
@Test
@Transactional
@Rollback(value = false)
public void addTest(){
Customer customer = new Customer();
LinkMan linkMan = new LinkMan();
customer.setCustName("TBD云集中心");
customer.setCustLevel("VIP客户");
customer.setCustSource("网络");
customer.setCustIndustry("商业办公");
customer.setCustAddress("昌平区北七家镇");
customer.setCustPhone("010-84389340");
linkMan.setLkmName("小明");
linkMan.setLkmGender("male");
linkMan.setLkmMobile("13811111111");
linkMan.setLkmPhone("010-34785348");
linkMan.setLkmEmail("123456@qq.com");
linkMan.setLkmPosition("老师");
linkMan.setLkmMemo("还行吧");
/** * 配置了客户到联系人的关系 * 从客户的角度上,发送了两条insert语句,发送一条更新语句更新数据库(更新从表中的外键值) * 由于我们配置了客户到联系人的关系,客户可以对外键进行维护 */
linkMan.setCustomer(customer);
//此添加可以不写会
customer.getLinkMans().add(linkMan);
customerDao.save(customer);
linkManDao.save(linkMan);
}
}
运行结果:
Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: update cst_linkman set lkm_cust_id=? where lkm_id=?
分析:
执行了两条insert语句以及一条update语句,有一条update的语句是多余的。产生这种现象的原因是:我们在两个实体类中均对外键进行了维护,相当于维护了两次,解决的办法是放弃一方的维权。
修改:将主表中的关系映射修改为:
@OneToMany(mappedBy = "customer",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>();
ii.级联添加
级联操作:操作一个对象同时操作它的关联对象
使用方法:只需要在操作主体的注解上配置casade
/**
* 放弃外键维护权:我的一对多映射参照对方的属性就可以了
* mappedBy:对方维护关系的属性名称
* cascade = CascadeType.ALL 进行级联操作,all表示级联所有(insert,delete,update)
* .merge 更新
* .persist保存
* .remove 删除
* fetch 配置延迟加载
*/
@OneToMany(mappedBy = "customer",cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>()
一般是对配置在主表中,但是:注意:慎用CascadeType.ALL
package ctgu.OntoMany;
import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class OntoManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/** * 级联添加: * 保存一个客户的同时,保存客户的所有联系人 * 需要在操作主题的实体类上,配置casache属性 */
@Test
@Transactional
@Rollback(value = false)
public void cascadeAdd(){
Customer customer = new Customer();
LinkMan linkMan = new LinkMan();
customer.setCustName("测试公司1");
linkMan.setLkmName("测试员工张三1");
//注意此处添加
linkMan.setCustomer(customer);
customer.getLinkMans().add(linkMan);
customerDao.save(customer);
}
}
测试结果:
Hibernate: insert into cst_customer (cust_address, cust_industry, cust_level, cust_name, cust_phone, cust_source) values (?, ?, ?, ?, ?, ?)
Hibernate: insert into cst_linkman (lkm_cust_id, lkm_email, lkm_gender, lkm_memo, lkm_mobile, lkm_name, lkm_phone, lkm_position) values (?, ?, ?, ?, ?, ?, ?, ?)
iii.级联删除
删除公司的同时,删除对应公司的所有员工。
JPA中删除是先执行查询再执行删除。
/** * 级联删除:删除1号客户的同时,删除1号客户的所有联系人 * 1.需要区分操作主体(你对那个对象进行操作) * 2.需要在操作主体的实体类上,添加级联属性(需要添加到多表映射关系的注解上) * 3.cascade(配置级联) */
@Test
@Transactional
@Rollback(value = false)
public void cascadeDelete(){
// Customer customer = customerDao.findOne(1L);
customerDao.delete(40L);
}
测试结果:
Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_, linkmans1_.lkm_cust_id as lkm_cust9_1_1_, linkmans1_.lkm_id as lkm_id1_1_1_, linkmans1_.lkm_id as lkm_id1_1_2_, linkmans1_.lkm_cust_id as lkm_cust9_1_2_, linkmans1_.lkm_email as lkm_emai2_1_2_, linkmans1_.lkm_gender as lkm_gend3_1_2_, linkmans1_.lkm_memo as lkm_memo4_1_2_, linkmans1_.lkm_mobile as lkm_mobi5_1_2_, linkmans1_.lkm_name as lkm_name6_1_2_, linkmans1_.lkm_phone as lkm_phon7_1_2_, linkmans1_.lkm_position as lkm_posi8_1_2_ from cst_customer customer0_ left outer join cst_linkman linkmans1_ on customer0_.cust_id=linkmans1_.lkm_cust_id where customer0_.cust_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_customer where cust_id=?
注意:一般使用级联删除是比较危险的,在一对多的情况下。如果没有使用级联操作,应该如何删除数据?
只删除从表数据:可以任意删除。
删除主表数据:
- 有从表数据
- 在默认情况下,会将外键字段置为null,然后再执行删除。此时如果从表的结构上,外键字段存在非空约束将会报错。
- 使用级联删除。
- 应该先根据外键值,删除从表中的数据,再删除主表中的数据。
- 没有从表数据:随便删
iv.一对多删除(非级联删除)
创建方法:根据customer删除员工。(使用复杂查询中的自定义方法)
package ctgu.dao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LinkManDao extends JpaRepository<LinkMan,Long>, JpaSpecificationExecutor<LinkMan> {
//根据外键值进行删除
public void deleteByCustomer(Customer customer);
}
此时的主表的关键映射为设置级联操作:
@OneToMany(mappedBy = "customer",fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>();
测试:
package ctgu.OntoMany;
import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class OntoManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
@Test
@Transactional
@Rollback(value = false)
public void cascadeDelete(){
Customer customer = customerDao.findOne(47L);
linkManDao.deleteByCustomer(customer);
customerDao.delete(47L);
}
}
测试结果:
Hibernate: select linkman0_.lkm_id as lkm_id1_1_, linkman0_.lkm_cust_id as lkm_cust9_1_, linkman0_.lkm_email as lkm_emai2_1_, linkman0_.lkm_gender as lkm_gend3_1_, linkman0_.lkm_memo as lkm_memo4_1_, linkman0_.lkm_mobile as lkm_mobi5_1_, linkman0_.lkm_name as lkm_name6_1_, linkman0_.lkm_phone as lkm_phon7_1_, linkman0_.lkm_position as lkm_posi8_1_ from cst_linkman linkman0_ left outer join cst_customer customer1_ on linkman0_.lkm_cust_id=customer1_.cust_id where customer1_.cust_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_linkman where lkm_id=?
Hibernate: delete from cst_customer where cust_id=?
5.JPA中的多对多
案例:用户和角色。
用户:指社会上的某个人。
角色:指人们可能有多种身份信息
比如说:小明有多种身份,即使java工程师,还是后端攻城狮,也是CEO;而Java工程师除了小明,还有张三、李四等等。
所以我们说,用户和角色之间的关系是多对多。
1.建立实体类与表直接的关系映射
package ctgu.pojo;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="user_id")
private Long userId;
@Column(name="user_name")
private String userName;
@Column(name="age")
private Integer age;
/** * 配置用户到角色的 多对多 关系 * 配置多对多的映射关系 * 1.声明表关系的配置 * @ManyToMany() * targetEntity = Role.class声明对方的实体类字节码 * 2.配置中间表(两个外键) * @JoinTable * name :中间表的名称 * joinColumns,当前对象在中间表的位置 * @JoinColumn * name:外键在中间表的字段名称 * referencedColumnName:参照的主表的主键名称 * inverseJoinColumns,对方对象在中间表的位置 */
// @ManyToMany(targetEntity = Role.class,cascade = CascadeType.ALL)
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "sys_user_role",
//joinColumns,当前对象在中间表的位置
joinColumns = {@JoinColumn(name = "sys_user_id",referencedColumnName = "user_id")},
//inverseJoinColumns,对方对象在中间表的位置
inverseJoinColumns = {@JoinColumn(name = "sys_role_id",referencedColumnName = "role_id")}
)
private Set<Role> roles = new HashSet<>();
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
package ctgu.pojo;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "sys_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
@ManyToMany(targetEntity = User.class)
@JoinTable(name = "sys_user_role",
//joinColumns,当前对象在中间表的位置
joinColumns = {@JoinColumn(name = "sys_role_id",referencedColumnName = "role_id")},
//inverseJoinColumns,对方对象在中间表的位置
inverseJoinColumns ={@JoinColumn(name = "sys_user_id",referencedColumnName = "user_id")}
)
//@ManyToMany(mappedBy="roles")应该有一方放弃维护
private Set<User> users = new HashSet<>();
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
2,映射注解说明
i.@ManyToMany
作用:用于映射多对多关系 属性:
- cascade:配置级联操作。
- fetch:配置是否采用延迟加载。
- targetEntity:配置目标的实体类。映射多对多的时候不用写。
- mappedBy:指定从表实体类中引用主表对象的名称。(常用)
ii.@JoinTable
作用:针对中间表的配置 属性:
- nam:配置中间表的名称
- joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
- inverseJoinColumn:中间表的外键字段关联对方表的主键字段
iii.@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。 属性:
- name:指定外键字段的名称
- referencedColumnName:指定引用主表的主键字段名称
- unique:是否唯一。默认值不唯一
- nullable:是否允许为空。默认值允许。
- insertable:是否允许插入。默认值允许。
- updatable:是否允许更新。默认值允许。
- columnDefinition:列的定义信息。
3.多对多测试
i.保存用户和角色
数据库表:(其实可以直接由springdataJPA自动生成)
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;
dao接口:
package ctgu.dao;
import ctgu.pojo.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface RoleDao extends JpaRepository<Role,Long>, JpaSpecificationExecutor<Role> {
}
package ctgu.dao;
import ctgu.pojo.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface UserDao extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}
测试案例:
package ctgu;
import ctgu.dao.RoleDao;
import ctgu.dao.UserDao;
import ctgu.pojo.Role;
import ctgu.pojo.User;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ManyToMany {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/** * 保存一个用户,保存一个角色 * 多对多放弃维护权: * 被动的一方放弃,谁被选择谁放弃 */
@Test
@Transactional
@Rollback(false)
public void addUserAndRole(){
User user = new User();
Role role1 = new Role();
Role role2 = new Role();
Role role3 = new Role();
user.setUserName("李大明");
role1.setRoleName("后端攻城狮");
role2.setRoleName("java程序员");
role3.setRoleName("CEO");
//用户和角色都可以对中间表进行维护,添加两次就重复了
//配置角色到用户的关系,可以对中间表中的数据进行维护
role1.getUsers().add(user);
role2.getUsers().add(user);
role3.getUsers().add(user);
//配置用户到角色的关系,
user.getRoles().add(role1);
user.getRoles().add(role2);
user.getRoles().add(role3);
userDao.save(user);
roleDao.save(role1);
roleDao.save(role2);
roleDao.save(role3);
}
}
测试结果:
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
原因:
在多对多(保存)中,如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据, 中间表的 2 个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一 方放弃对中间表的维护权即可,推荐在被动的一方放弃,配置如下:
//放弃对中间表的维护权,解决保存中主键冲突的问题
@ManyToMany(mappedBy="roles")
private Set<SysUser> users = new HashSet<SysUser>(0);
正确结果:
Hibernate: insert into sys_user (age, user_name) values (?, ?)
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
Hibernate: insert into sys_user_role (sys_role_id, sys_user_id) values (?, ?)
系统会自动创建表sys_user_role并添加数据。
ii.级联保存
保存用户的同时,保存其关联角色。
只需要在操作对象的注解上配置cascade
@ManyToMany(mappedBy = "roles",cascade = CascadeType.ALL)
private Set<User> users = new HashSet<>();
package ctgu;
import ctgu.dao.RoleDao;
import ctgu.dao.UserDao;
import ctgu.pojo.Role;
import ctgu.pojo.User;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ManyToMany {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/** * 级联操作:保存一个用户的同时,保存用户的关联角色 * 只需要在操作对象的注解上配置cascade */
@Test
@Transactional
@Rollback(false)
public void addCasecade() {
User user = new User();
Role role = new Role();
user.setUserName("张三");
role.setRoleName("java程序员");
//用户和角色都可以对中间表进行维护,添加两次就重复了
//配置角色到用户的关系,可以对中间表中的数据进行维护
role.getUsers().add(user);
//配置用户到角色的关系,
user.getRoles().add(role);
roleDao.save(role);
}
}
测试结果:
Hibernate: insert into sys_role (role_name) values (?)
Hibernate: insert into sys_user (age, user_name) values (?, ?)
Hibernate: insert into sys_user_role (sys_user_id, sys_role_id) values (?, ?)
iii.级联删除
/** * 级联操作:删除id为1的用户,同时删除他的关联对象 */
@Test
@Transactional
@Rollback(false)
public void deleteCasecade() {
roleDao.delete(23L);
}
测试结果:
Hibernate: select role0_.role_id as role_id1_0_0_, role0_.role_name as role_nam2_0_0_ from sys_role role0_ where role0_.role_id=?
Hibernate: select users0_.sys_role_id as sys_role2_2_0_, users0_.sys_user_id as sys_user1_2_0_, user1_.user_id as user_id1_1_1_, user1_.age as age2_1_1_, user1_.user_name as user_nam3_1_1_ from sys_user_role users0_ inner join sys_user user1_ on users0_.sys_user_id=user1_.user_id where users0_.sys_role_id=?
Hibernate: delete from sys_user_role where sys_user_id=?
Hibernate: delete from sys_user where user_id=?
Hibernate: delete from sys_role where role_id=?
注意:
- 调用的对象是role,所有需要在role对象中配置级联cascade = CascadeType.ALL;
- 慎用!可能会清空相关联的数据;
6.SpringDataJPA中的多表查询
以下例子采用一对多的案例实现。
i.对象导航查询
对象导航查询的方式就是根据已加载的对象,导航到他的关联对象。利用实体与实体之间的关系来检索对象。例如:通过ID查询出一个Customer,可以调用Customer对象中的getLinkMans()方法来获取该客户的所有联系人。
对象导航查询使用的要求是:两个对象之间必须存在关联联系。
案例:查询公司,获取公司下所有的员工
package ctgu.QueryTest;
import ctgu.dao.CustomerDao;
import ctgu.dao.LinkManDao;
import ctgu.pojo.Customer;
import ctgu.pojo.LinkMan;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import java.util.Set;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ObjectQuery {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
/** * 测试导航查询(查询一个对象的时候,通过此查询他的关联对象) * 对于对象导航查询,默认使用的是延迟加载的形式来查询的,(需要才去查询) * 调用get方法并不会立即发送查询,而实在关联对象使用的时候才会查询 * 修改配置,将延迟加载改为立即加载 * fetch 需要配置多表映射关系发注解上 * */
@Test
@Transactional//解决在java代码中的no Session问题
public void QueryTest01(){
Customer customer = customerDao.findOne(26L);
Set<LinkMan> linkMans = customer.getLinkMans();
for(LinkMan man:linkMans){
System.out.println(man);
}
}
}
问题:我们在查询Customer时,一定要把LinkMan查出来吗?
分析:如果我们不查的话,在需要的时候需要重新写代码,调用方法查询;但是每次都查出来又会浪费服务器的内存。
解决:查询主表对象时,采用延迟加载的思想,通过配置的方式,当我们需要使用的时候才查询。
延迟加载
由于上述调用的对象为Customer,故而在Customer对象中需要配置延迟加载。Customer对象
@OneToMany(mappedBy = "customer",fetch = FetchType.LAZY)
private Set<LinkMan> linkMans=new HashSet<>();
测试结果:
Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_ from cst_customer customer0_ where customer0_.cust_id=?
Hibernate: select linkmans0_.lkm_cust_id as lkm_cust9_1_0_, linkmans0_.lkm_id as lkm_id1_1_0_, linkmans0_.lkm_id as lkm_id1_1_1_, linkmans0_.lkm_cust_id as lkm_cust9_1_1_, linkmans0_.lkm_email as lkm_emai2_1_1_, linkmans0_.lkm_gender as lkm_gend3_1_1_, linkmans0_.lkm_memo as lkm_memo4_1_1_, linkmans0_.lkm_mobile as lkm_mobi5_1_1_, linkmans0_.lkm_name as lkm_name6_1_1_, linkmans0_.lkm_phone as lkm_phon7_1_1_, linkmans0_.lkm_position as lkm_posi8_1_1_ from cst_linkman linkmans0_ where linkmans0_.lkm_cust_id=?
LinkMan{lkmId=31, lkmName='李四', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
LinkMan{lkmId=30, lkmName='张三', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
分析:我们发现其执行了两条select语句。
问题:在我们查LinkMan时,是否需要把Customer查出来?
分析:由于一个用户只属于一家公司,及每个LinkMan都有唯一的Customer与之对应。如果我们不查,在使用的时候需要额外代码查询。且查询出的是单个对象,对内存消耗较小。
解决:在从表中采用立即加载的思想,只要查询从表实体,就把主表对象同时查出来。
立即加载
@OneToMany(mappedBy = "customer",fetch = FetchType.EAGER)
private Set<LinkMan> linkMans=new HashSet<>();
测试结果:
Hibernate: select customer0_.cust_id as cust_id1_0_0_, customer0_.cust_address as cust_add2_0_0_, customer0_.cust_industry as cust_ind3_0_0_, customer0_.cust_level as cust_lev4_0_0_, customer0_.cust_name as cust_nam5_0_0_, customer0_.cust_phone as cust_pho6_0_0_, customer0_.cust_source as cust_sou7_0_0_, linkmans1_.lkm_cust_id as lkm_cust9_1_1_, linkmans1_.lkm_id as lkm_id1_1_1_, linkmans1_.lkm_id as lkm_id1_1_2_, linkmans1_.lkm_cust_id as lkm_cust9_1_2_, linkmans1_.lkm_email as lkm_emai2_1_2_, linkmans1_.lkm_gender as lkm_gend3_1_2_, linkmans1_.lkm_memo as lkm_memo4_1_2_, linkmans1_.lkm_mobile as lkm_mobi5_1_2_, linkmans1_.lkm_name as lkm_name6_1_2_, linkmans1_.lkm_phone as lkm_phon7_1_2_, linkmans1_.lkm_position as lkm_posi8_1_2_ from cst_customer customer0_ left outer join cst_linkman linkmans1_ on customer0_.cust_id=linkmans1_.lkm_cust_id where customer0_.cust_id=?
LinkMan{lkmId=30, lkmName='张三', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
LinkMan{lkmId=31, lkmName='李四', lkmGenger='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'}
分析结果:我们发现其只执行了一条select语句。
对比可以发现,立即加载是一次性将查询对象以及关联对象查出来,而延迟加载是先查询目标对象,如果未调用
Set<LinkMan> linkMans = customer.getLinkMans();
方法,则将不会执行关联对象的查询。
ii.使用 Specification 查询
/** * Specification的多表查询 */
@Test
public void testFind() {
Specification<LinkMan> spec = new Specification<LinkMan>() {
public Predicate toPredicate(Root<LinkMan> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//Join代表链接查询,通过root对象获取
//创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
//JoinType.LEFT : 左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连接
Join<LinkMan, Customer> join = root.join("customer",JoinType.INNER);
return cb.like(join.get("custName").as(String.class),"传智播客1");
}
};
List<LinkMan> list = linkManDao.findAll(spec);
for (LinkMan linkMan : list) {
System.out.println(linkMan);
}
}
今天的文章SpringDataJpa中的复杂查询和动态查询,多表查询。(保姆级教程)分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15679.html