终于到第三天了,相信很多读者都会猜到今天都有些什么学,没错就是多表查询了,前两天我们学的,测试的基本上都是在单表里操作,那多表操作怎么操作呢?还有动态查询又怎么做呢?不急,今天我们就一起来深入探索一下JpaSpecificationExecutor
第一章Specifications动态查询
昨天初步介绍了一下是复杂查询以及动态查询的意思,而JpaSpecificationExecutor即jpa动态操作执行器,或者说是复杂操作执行器,而Specification即为动态条件的意思,而Pageable则是分页对象,可通过pageable完成分页,这个是spring data jpa提供的。
T findOne(Specification<T> spec); //查询单个对象
List<T> findAll(Specification<T> spec); //查询列表
//查询全部,分页
//pageable:分页参数
//返回值:分页pageBean(page:是springdatajpa提供的)
Page<T> findAll(Specification<T> spec, Pageable pageable);
//查询列表
//Sort:排序参数
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);//统计查询
看看Specification接口:
Specification :查询条件
自定义我们自己的Specification实现类
实现
//root:查询的根对象(查询的任何属性都可以从根对象中获取)
//CriteriaQuery:顶层查询对象,自定义查询方式(了解:一般不用)
//CriteriaBuilder:查询的构造器,封装了很多的查询条件
Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb); //封装查询条件
ok话不多说先搭建环境开始吧
搭建使用环境
老样子new一个module,
生成好后配置成如下图这样:
pom.xml加入:
<properties>
<spring.version>4.2.4.RELEASE</spring.version>
<hibernate.version>5.0.7.Final</hibernate.version>
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
<c3p0.version>0.9.1.2</c3p0.version>
<mysql.version>5.1.6</mysql.version>
</properties>
<dependencies>
<!-- junit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
<!-- spring beg -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring对ORM的支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring end -->
<!-- hibernate beg -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.1.Final</version>
</dependency>
<!-- hibernate end -->
<!-- c3p0 beg -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>${c3p0.version}</version>
</dependency>
<!-- c3p0 end -->
<!-- log end -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- log end -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.9.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- el beg 使用spring data jpa 必须引入 -->
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.4</version>
</dependency>
<!-- el end -->
</dependencies>
Customer类:
package com.tho.domain;
import javax.persistence.*;
/**
*
* * 所有的注解都是使用JPA的规范提供的注解,
* * 所以在导入注解包的时候,一定要导入javax.persistence下的
*/
@Entity //声明实体类
@Table(name="cst_customer") //建立实体类和表的映射关系
public class Customer {
@Id//声明当前私有属性为主键
@GeneratedValue(strategy= GenerationType.IDENTITY) //配置主键的生成策略
@Column(name="cust_id") //指定和表中cust_id字段的映射关系
private Long custId;
@Column(name="cust_address")//指定和表中cust_address字段的映射关系
private String custAddress;
@Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
private String custIndustry;
@Column(name="cust_level")//指定和表中cust_level字段的映射关系
private String custLevel;
@Column(name="cust_name") //指定和表中cust_name字段的映射关系
private String custName;
@Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
private String custPhone;
@Column(name="cust_source")//指定和表中cust_source字段的映射关系
private String custSource;
public Long getCustId() {
return custId;
}
public void setCustId(Long custId) {
this.custId = custId;
}
public String getCustAddress() {
return custAddress;
}
public void setCustAddress(String custAddress) {
this.custAddress = custAddress;
}
public String getCustIndustry() {
return custIndustry;
}
public void setCustIndustry(String custIndustry) {
this.custIndustry = custIndustry;
}
public String getCustLevel() {
return custLevel;
}
public void setCustLevel(String custLevel) {
this.custLevel = custLevel;
}
public String getCustName() {
return custName;
}
public void setCustName(String custName) {
this.custName = custName;
}
public String getCustPhone() {
return custPhone;
}
public void setCustPhone(String custPhone) {
this.custPhone = custPhone;
}
public String getCustSource() {
return custSource;
}
public void setCustSource(String custSource) {
this.custSource = custSource;
}
public Customer(String custAddress, String custIndustry, String custLevel, String custName, String custPhone, String custSource) {
this.custAddress = custAddress;
this.custIndustry = custIndustry;
this.custLevel = custLevel;
this.custName = custName;
this.custPhone = custPhone;
this.custSource = custSource;
}
public Customer() {
}
@Override
public String toString() {
return "Customer{" +
"custId=" + custId +
", custAddress='" + custAddress + '\'' +
", custIndustry='" + custIndustry + '\'' +
", custLevel='" + custLevel + '\'' +
", custName='" + custName + '\'' +
", custPhone='" + custPhone + '\'' +
", custSource='" + custSource + '\'' +
'}';
}
}
Customer接口:
package com.tho.dao;
import com.tho.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
}
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<!--spring和spring data jpa的配置-->
<!-- 1.dataSource 配置数据库连接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/jpa" />
<property name="user" value="root" />
<property name="password" value="root3306" />
</bean>
<!--2.创建entityManagerFactory对象交给spring容器管理-->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--配置扫描的包-->
<property name="packagesToScan" value="com.tho.domain"/>
<!--jpa实现的厂家包-->
<property name="persistenceProvider">
<bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
</property>
<!--jpa的供应商-->
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--配置是否自动创建数据库表-->
<property name="generateDdl" value="false" />
<!--指定数据库类型-->
<property name="database" value="MYSQL" />
<!--数据库方言:支持特有语法-->
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
<!--是否显示sql-->
<property name="showSql" value="true" />
</bean>
</property>
<!--jpa的方言-->
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
</property>
</bean>
<!-- 3.事务管理器-->
<!-- JPA事务管理器 -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!--整合spring data Jpa-->
<jpa:repositories base-package="com.tho.dao" transaction-manager-ref="transactionManager"
entity-manager-factory-ref="entityManagerFactory"/>
<!--<!– 4.txAdvice–>-->
<!--<tx:advice id="txAdvice" transaction-manager="transactionManager">-->
<!--<tx:attributes>-->
<!--<tx:method name="save*" propagation="REQUIRED"/>-->
<!--<tx:method name="insert*" propagation="REQUIRED"/>-->
<!--<tx:method name="update*" propagation="REQUIRED"/>-->
<!--<tx:method name="delete*" propagation="REQUIRED"/>-->
<!--<tx:method name="get*" read-only="true"/>-->
<!--<tx:method name="find*" read-only="true"/>-->
<!--<tx:method name="*" propagation="REQUIRED"/>-->
<!--</tx:attributes>-->
<!--</tx:advice>-->
<!--<!– 5.aop–>-->
<!--<aop:config>-->
<!--<aop:pointcut id="pointcut" expression="execution(* com.tho.service.*.*(..))" />-->
<!--<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut" />-->
<!--</aop:config>-->
<!---->
<context:component-scan base-package="com.tho"/>
</beans>
SpecTest测试类:
package com.tho.test;
import com.tho.dao.CustomerDao;
import com.tho.domain.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 根据条件,查询单个对象
*/
@Test
public void testSpec(){
//匿名内部类
/**
* 自定义查询条件:
* 1.实现Specification接口(提供泛型,查询的对象类型)
* 2.实现toPredicate方法(构造查询条件)
* 3.需要借助方法参数中的两个参数(
* root:获取需要查询的对象属性
* CriteriaBuilder:构造查询条件的,内部封装了很多的查询条件(模糊匹配,精准匹配)
* )
*/
Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return null;
}
};
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
}
ok基本的环境已经搭建完成,现在是一个没有条件的查询。
使用Specifications完成条件查询
精准匹配
案例需求:根据客户名称查询,查询客户名为calmtho的客户
修改testSpec方法:
package com.tho.test;
import com.tho.dao.CustomerDao;
import com.tho.domain.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.persistence.criteria.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpecTest {
@Autowired
private CustomerDao customerDao;
/**
* 根据条件,查询单个对象
*/
@Test
public void testSpec(){
//匿名内部类
/**
* 自定义查询条件:
* 1.实现Specification接口(提供泛型,查询的对象类型)
* 2.实现toPredicate方法(构造查询条件)
* 3.需要借助方法参数中的两个参数(
* root:获取需要查询的对象属性
* CriteriaBuilder:构造查询条件的,内部封装了很多的查询条件(模糊匹配,精准匹配)
* )
*
* 案例:根据客户名称查询,查询客户名为calmtho的客户
* 查询条件
* 1.查询方式
* cb对象
* 2.比较的属性名称
* root对象
*/
Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//1.获取比较属性
Path<Object> custName = root.get("custName");
//2.构造查询条件 select * from cst_customer where cust_name="calmtho"
/**
* 第一个参数:需要比较的属性(path)
* 第二个参数:当前需要比较的取值
*/
Predicate predicate = cb.equal(custName, "calmtho");//进行精准匹配(比较的属性的取值)
return predicate;
}
};
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
}
注意:如果没有返回predicate,默认是null,就是无条件查询,即查询所有元素,与查找出一个结果不符,所以就会报错,我们从spring的API官方文档也可查找到。
如果使用没返回值我们可以调用findAll方法:
多条件拼接实现动态查询
需求:查询条件为根据客户名(calm)和客户所属行业查询(IT)的客户
先研究一下方法:
由上面的案例我们知道构造条件都是由cb进行操作,所以我们试着用cd调用看看有没有or和and方法,我们发现确实有,而且是一个可变参数。
上代码吧,加一个拼接条件测试
方法:
/**
* 多条件查询
* 案例:根据客户名(calm)和客户所属行业查询(IT)
*/
@Test
public void testSpecJoin(){
/**
* root:获取属性
* 客户名
* 所属行业
* cb:构造查询
* 1.构造客户名的精准匹配查询
* 2.构造所属行业的精准匹配查询
* 3.将以上两个查询联系起来
*/
Specification<Customer> specJoin = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Object> custName = root.get("custName");
Path<Object> custIndustry = root.get("custIndustry");
//构造查询
//1.构造客户名的精准查询
Predicate p1 = cb.equal(custName, "calm");
//2.构造所属行业的精准匹配查询
Predicate p2 = cb.equal(custIndustry, "IT");
//3.将多个条件组合到一起,组合(满足条件一并且满足条件二;与关系,满足条件一或者条件二即可)
Predicate predicate = cb.and(p1, p2);
//cb.or();
return predicate;
}
};
Customer customer = customerDao.findOne(specJoin);
System.out.println(customer);
}
dubug启动:
模糊查询
测试模糊查询方法:
/**
* 案例:完成根据客户名称的模糊匹配,返回客户列表
* 客户名称以:‘calm’开头
*
* equal:直接得到path对象(属性),然后进行比较即可
* gt,lt,ge,like:得到path对象,根据path指定比较类型,再去比较
* 指定参数类型:path.as(类型字节码对象)
*/
@Test
public void testSpecLike(){
Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//查询属性:客户名
Path<Object> custName = root.get("custName");
//查询方式:模糊查询
Predicate predicate = cb.like(custName.as(String.class), "calm%");
return predicate;
}
};
List<Customer> list = customerDao.findAll(spec);
for (Customer customer:list){
System.out.println(customer);
}
}
模糊查询比起equal不一样的地方在于穿的参数在于得到path对象(属性),不是直接传入,而是要指定path对象的类型,即传入path.as(类型字节码对象)
排序查询
基于上面案例进行排序改造,改为结果进行降序排列:
即构造有参Sort对象然后传入findAll方法里调用。改造代码如下
Sort sort = new Sort(Sort.Direction.DESC,"custId");
List<Customer> list = customerDao.findAll(spec,sort);
for (Customer customer:list){
System.out.println(customer);
}
基于Specifications的分页查询
从上面Page对象方法点击进入Page接口,进入接口后ctrl+F12像上图那样勾选,就能看到Page所有可以使用的方法,勾选了包含父类方法,没勾选左上角第一个则是显示本类里的方法!!
分页一般是查询所有,或者是条件查完使用集合来装之后,封装成分页对象给到前端,从而实现分页显示:那么来看看findAll可以调用什么和分页有关的参数方法:
编写分页测试代码:
**
* 分页查询
* Specification:查询条件
* Pageable:分页参数
* 分页参数:查询的页码,煤业查询的条数
* findAll(Specification,Pageable)带有条件的查询
* findAll(Pageable)没有条件的分页
*
* 返回:Page(SpringDataJpa为我们封装好的pageBean对象,数据列表,共条数)
*/
@Test
public void testSpecPage(){
/* Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return null;
}
};
Specification spec=null;和上面效果一样
*/
Specification spec=null;
Pageable pageable = new PageRequest(0, 2);
//分页查询
Page<Customer> page= customerDao.findAll(spec,pageable);
System.out.println("共有"+page.getTotalElements()+" 条,总页数为:"+page.getTotalPages()+"页");
}
打断点Debug运行一下:
我们发现在发起分页请求的时候会有三个参数,分别是排序,起始页,每页大小,说明我们这个方法也会有可以传排序的方法,仔细想想为什么要这样写这样的方法?其实是为了在不加条件查询的时候也可以进行分页并且排序。
统计查询
查表有多少记录:
/**
* 统计查询
*/
@Test
public void testCount(){
Specification spec=null;
long count = customerDao.count(spec);
System.out.println(count);
}
总结
:Specification动态条件查询需要通过实现类去实现,以返回得对象作为条件,而条件则主要是由root通过get方法获取对象(属性)即path对象,通过path处理后通过CriteriaBuilder条件构造者去构造条件,然后返回构造出得对象作为条件参数给到JpaSpecificationExecutor接口得方法作为参数使用!!再看一遍拼接得方法代码就能回忆起来了。
/**
* 多条件查询
* 案例:根据客户名(calm)和客户所属行业查询(IT)
*/
@Test
public void testSpecJoin(){
/**
* root:获取属性
* 客户名
* 所属行业
* cb:构造查询
* 1.构造客户名的精准匹配查询
* 2.构造所属行业的精准匹配查询
* 3.将以上两个查询联系起来
*/
Specification<Customer> specJoin = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Object> custName = root.get("custName");
Path<Object> custIndustry = root.get("custIndustry");
//构造查询
//1.构造客户名的精准查询
Predicate p1 = cb.equal(custName, "calm");
//2.构造所属行业的精准匹配查询
Predicate p2 = cb.equal(custIndustry, "IT");
//3.将多个条件组合到一起,组合(满足条件一并且满足条件二;与关系,满足条件一或者条件二即可)
Predicate predicate = cb.and(p1, p2);
//cb.or();
return predicate;
}
};
Customer customer = customerDao.findOne(specJoin);
System.out.println(customer);
}
### 方法对应关系
>方法名称 Sql对应关系
equle 等价于 filed = value
gt(greaterThan )等价于 filed > value
lt(lessThan )等价于 filed < value
ge(greaterThanOrEqualTo )等价于 filed >= value
le( lessThanOrEqualTo)等价于 filed <= value
notEqule等价于 filed != value
like 等价于 filed like value
notLike 等价于filed not like value
第二章多表设计
多表之间的关系和操作多表的操作步骤
多表关系复习
- 表关系
-
一对一:可以把字段放在一个表,如果分表外键任意,没有绝对得主从表,根据业务决定外键位置
-
一对多:
一
的一方:主
表
多
的一方:从
表- 外键:需要再从表上新建一列作为外键,他的取值来源于主表的主键
- 外键:需要再从表上新建一列作为外键,他的取值来源于主表的主键
-
多对多:
- 中间表:中间表中最少应该由两个字段组成,这两个字段做为外键指向两张表的主键,
又组成了联合主键
- 中间表:中间表中最少应该由两个字段组成,这两个字段做为外键指向两张表的主键,
-
从图可以看出,系统设计的三种实体关系分别为:多对多、一对多和一对一关系。注意:一对多关系可以看为两种: 即一对多,多对一。所以说四种更精确。
明确: 我们今天只涉及实际开发中常用的关联关系,一对多和多对多。而一对一的情况,在实际开发中几乎不用。因为一对一完全可以放在一个表里,没必要多维护一个表
讲师对学员:一对多关系
实体类的关系:
包含关系:可以通过实体类的包含关系
继承关系
分析步骤
多表分析步骤
1.明确表关系
2.确定表关系(描述 外键|中间表)
3.编写实体类,再实体类中描述表关系(包含关系)
4.配置映射关系
通过案例分析:
第三 完成多表操作
i.一对多操作
案例:客户和联系人的案例(一对多关系)
客户:一家公司
联系人:这家公司的员工
一个客户可以具有多个联系人
一个联系人从属于一家公司
分析步骤
1.明确表关系
一对多关系
2.确定表关系(描述 外键|中间表)
主表:客户表
从表:联系人表
* 再从表上添加外键
3.编写实体类,再实体类中描述表关系(包含关系)
客户:再客户的实体类中包含一个联系人的集合
联系人:在联系人的实体类中包含一个客户的对象
4.配置映射关系
* 使用jpa注解配置一对多映射关系
搭建环境
同样的使用数据库为jpa的本地库,使用navicat或者sqlyog连接上数据库后,然后执行脚本:
/*创建客户表*/
CREATE TABLE cst_customer (
cust_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '客户编号(主键)',
cust_name varchar(32) NOT NULL COMMENT '客户名称(公司名称)',
cust_source varchar(32) DEFAULT NULL COMMENT '客户信息来源',
cust_industry varchar(32) DEFAULT NULL COMMENT '客户所属行业',
cust_level varchar(32) DEFAULT NULL COMMENT '客户级别',
cust_address varchar(128) DEFAULT NULL COMMENT '客户联系地址',
cust_phone varchar(64) DEFAULT NULL COMMENT '客户联系电话',
PRIMARY KEY (`cust_id`)
) ENGINE=InnoDB AUTO_INCREMENT=94 DEFAULT CHARSET=utf8;
/*创建联系人表*/
CREATE TABLE cst_linkman (
lkm_id bigint(32) NOT NULL AUTO_INCREMENT COMMENT '联系人编号(主键)',
lkm_name varchar(16) DEFAULT NULL COMMENT '联系人姓名',
lkm_gender char(1) DEFAULT NULL COMMENT '联系人性别',
lkm_phone varchar(16) DEFAULT NULL COMMENT '联系人办公电话',
lkm_mobile varchar(16) DEFAULT NULL COMMENT '联系人手机',
lkm_email varchar(64) DEFAULT NULL COMMENT '联系人邮箱',
lkm_position varchar(16) DEFAULT NULL COMMENT '联系人职位',
lkm_memo varchar(512) DEFAULT NULL COMMENT '联系人备注',
lkm_cust_id bigint(32) NOT NULL COMMENT '客户id(外键)',
PRIMARY KEY (`lkm_id`),
KEY `FK_cst_linkman_lkm_cust_id` (`lkm_cust_id`),
CONSTRAINT `FK_cst_linkman_lkm_cust_id` FOREIGN KEY (`lkm_cust_id`) REFERENCES `cst_customer` (`cust_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
在idea中,新建模块:
搭建成上图的结构:
com.tho.dao:
Customer:
package com.tho.domain;
import javax.persistence.*;
/**
*
* * 所有的注解都是使用JPA的规范提供的注解,
* * 所以在导入注解包的时候,一定要导入javax.persistence下的
*/
@Entity //声明实体类
@Table(name="cst_customer") //建立实体类和表的映射关系
public class Customer {
@Id//声明当前私有属性为主键
@GeneratedValue(strategy= GenerationType.IDENTITY) //配置主键的生成策略
@Column(name="cust_id") //指定和表中cust_id字段的映射关系
private Long custId;
@Column(name="cust_address")//指定和表中cust_address字段的映射关系
private String custAddress;
@Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
private String custIndustry;
@Column(name="cust_level")//指定和表中cust_level字段的映射关系
private String custLevel;
@Column(name="cust_name") //指定和表中cust_name字段的映射关系
private String custName;
@Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
private String custPhone;
@Column(name="cust_source")//指定和表中cust_source字段的映射关系
private String custSource;
public Long getCustId() {
return custId;
}
public void setCustId(Long custId) {
this.custId = custId;
}
public String getCustAddress() {
return custAddress;
}
public void setCustAddress(String custAddress) {
this.custAddress = custAddress;
}
public String getCustIndustry() {
return custIndustry;
}
public void setCustIndustry(String custIndustry) {
this.custIndustry = custIndustry;
}
public String getCustLevel() {
return custLevel;
}
public void setCustLevel(String custLevel) {
this.custLevel = custLevel;
}
public String getCustName() {
return custName;
}
public void setCustName(String custName) {
this.custName = custName;
}
public String getCustPhone() {
return custPhone;
}
public void setCustPhone(String custPhone) {
this.custPhone = custPhone;
}
public String getCustSource() {
return custSource;
}
public void setCustSource(String custSource) {
this.custSource = custSource;
}
public Customer(String custAddress, String custIndustry, String custLevel, String custName, String custPhone, String custSource) {
this.custAddress = custAddress;
this.custIndustry = custIndustry;
this.custLevel = custLevel;
this.custName = custName;
this.custPhone = custPhone;
this.custSource = custSource;
}
public Customer() {
}
@Override
public String toString() {
return "Customer{" +
"custId=" + custId +
", custAddress='" + custAddress + '\'' +
", custIndustry='" + custIndustry + '\'' +
", custLevel='" + custLevel + '\'' +
", custName='" + custName + '\'' +
", custPhone='" + custPhone + '\'' +
", custSource='" + custSource + '\'' +
'}';
}
}
LinkMan:
package com.tho.domain;
import javax.persistence.*;
@Entity
@Table(name = "cust_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; //联系人备注
public Long getLkmId() {
return lkmId;
}
public void setLkmId(Long lkmId) {
this.lkmId = lkmId;
}
public String getLkmName() {
return lkmName;
}
public void setLkmName(String lkmName) {
this.lkmName = lkmName;
}
public String getLkmGender() {
return lkmGender;
}
public void setLkmGender(String lkmGender) {
this.lkmGender = lkmGender;
}
public String getLkmPhone() {
return lkmPhone;
}
public void setLkmPhone(String lkmPhone) {
this.lkmPhone = lkmPhone;
}
public String getLkmMobile() {
return lkmMobile;
}
public void setLkmMobile(String lkmMobile) {
this.lkmMobile = lkmMobile;
}
public String getLkmEmail() {
return lkmEmail;
}
public void setLkmEmail(String lkmEmail) {
this.lkmEmail = lkmEmail;
}
public String getLkmPosition() {
return lkmPosition;
}
public void setLkmPosition(String lkmPosition) {
this.lkmPosition = lkmPosition;
}
public String getLkmMemo() {
return lkmMemo;
}
public void setLkmMemo(String lkmMemo) {
this.lkmMemo = lkmMemo;
}
}
com.tho.domain:
CustomerDao:
package com.tho.dao;
import com.tho.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface CustomerDao extends JpaRepository<Customer,Long>, JpaSpecificationExecutor<Customer> {
}
LinkManDao:
package com.tho.dao;
import com.tho.domain.LinkMan;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LinkManDao extends JpaRepository<LinkMan,Long>, JpaSpecificationExecutor<LinkMan> {
}
resources下配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<!--spring 和 spring data jpa的配置-->
<!-- 1.创建entityManagerFactory对象交给spring容器管理-->
<bean id="entityManagerFactoty" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--配置的扫描的包(实体类所在的包) -->
<property name="packagesToScan" value="com.tho.domain" />
<!-- jpa的实现厂家 -->
<property name="persistenceProvider">
<bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
</property>
<!--jpa的供应商适配器 -->
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--配置是否自动创建数据库表 -->
<property name="generateDdl" value="false" />
<!--指定数据库类型 -->
<property name="database" value="MYSQL" />
<!--数据库方言:支持的特有语法 -->
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
<!--是否显示sql -->
<property name="showSql" value="true" />
</bean>
</property>
<!--jpa的方言 :高级的特性 -->
<property name="jpaDialect" >
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
</property>
</bean>
<!--2.创建数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="root"/>
<property name="password" value="root3306"/>
<property name="jdbcUrl" value="jdbc:mysql:///jpa" />
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</bean>
<!--3.整合spring dataJpa-->
<jpa:repositories base-package="com.tho.dao" transaction-manager-ref="transactionManager"
entity-manager-factory-ref="entityManagerFactoty" />
<!--4.配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactoty"/>
</bean>
<!-- 4.txAdvice-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 5.aop-->
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.tho.service.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut" />
</aop:config>
<!--5.声明式事务 -->
<!-- 6. 配置包扫描-->
<context:component-scan base-package="com.tho" />
</beans>
创建com.tho.test文件夹以及测试类,ok基本上环境就搭建完毕!
第三章一对多和多对一
使用外键
1.首先为什么有一对多或者多对一这种说法呢?其实这和我们的生活实际以及业务设计有关系,实体类或者说表之间存在着这样的关系。
2.比如我们推导的客户是实际中对于我们来说一般客户指的是合作公司,而一个合作公司必然有不同的联系人,所以客户和联系人从这个维度来看,一个客户对应多个联系人,而一般工作都是要求不能一人多公司工作,所以联系人对客户来说是多对一而不是多对多。Ok分析清楚后看看代码层面怎么处理吧。
3.通过分析对应关系可推导出,以包含关系来说,Customer属性应该包含一个集合类型的联系人,而联系人属性应该是一个对象而不是集合。
@OneToMany(targetEntity = LinkMan.class)
@JoinColumn(name = "lkm_cust_id",referencedColumnName = "cust_id")
private Set<LinkMan> linkMans=new HashSet<LinkMan>();
而LinkMan:
@ManyToOne(targetEntity = Customer.class)
@JoinColumn(name = "lkm_cust_id",referencedColumnName = "cust_id")
private Customer customer;
添加两个domain类的getter和setter方法以及toString方法。
为了演示更明显我们配置加多jpa自动生成表这一块设置:
<!--注入jpa的配置信息
加载jpa的基本配置信息和jpa实现方式(hibernate)的配置信息
hibernate.hbm2ddl.auto : 自动创建数据库表
create : 每次都会重新创建数据库表
update:有表不会重新创建,没有表会重新创建表
-->
<property name="jpaProperties" >
<props>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
协商我们的
package com.tho.test;
import com.tho.dao.CustomerDao;
import com.tho.dao.LinkManDao;
import com.tho.domain.Customer;
import com.tho.domain.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(locations = "classpath:applicationContext.xml")
public class OneToManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private LinkManDao linkManDao;
@Test
@Transactional
@Rollback(value = false)
public void testAdd(){
//创建一个客户
Customer customer = new Customer();
customer.setCustName("baidu");
//创建一个联系人
LinkMan linkMan = new LinkMan();
linkMan.setLkmName("zhangsan");
customerDao.save(customer);
linkManDao.save(linkMan);
}
}
debug运行一下:
虽然表结构创建没问题,但是我们外键并实际关联导主表的主键,可见其实是有问题的,那为什么呢?回过去看我们的测试类会发现:
我们虽然在实体类已经配置了映射,但是实际调用的时候并没有让两个类形成包含关系,所以生成的外键是null。
我们从执行语句可以看出是多了更新对象这个操作并且出现了主表主键约束作为条件。实际上它变成sql就是把从表的外键更新的成主表主键的意思。
这里可以这样写其实因为配置了客户也可以维护外键,正常来说就是linkMan加入customer。注释了debug运行,再修改成linkMan加入customer。
修改一下
执行语句和上面的主表维护外键方式不一样,是因为走的是多对一
而对于多对一来说,即使是单条插入就相当于一对一,就有直接的映射关系,把一的对象放进去就是相当于直接更新外键的值。
放弃外键维护
放开注释,运行结果会和一对多的执行语句一样,是因为配置了双向的维护关系,且编写代码的时候也用了两种关系的建立,既即一对多,多对一都会走,而一对多会多一条更新语句,相当于多执行一次关联,可以测试看看。
其实相当于出现的冗余,多了一条没有的实际作用的update,我们怎么让他不出现这种现象呢,那就是使用放弃维护权,即主表放弃维护权:
即OneToMany不配目标实体类和建立外键,而是直接配置参照对象的属性映射,此配置的键为mappedBy,值为从表实体类外键映射字段的属性名
debug运行,ok配置没问题。
一对多删除说明
正常来说当我们建立了外键,我们删除数据只能删除外键一方的数据,即从表数据,因为我们要删除主表数据,只能删除从表没有引用主键的外键的数据,直接删主表数据无法保证主表数据没有被引用,即假如在可以直接删除的主表数据的情况下,从表本身是通外键链接相关的从表数据,表达一种关系,而主表都没有这个信息了,其实上从表引用的被删除的主键数据的行数据就会变得没有意义,这也就是外键约束的作用!!!
对上面说的是传统的设计删除的要求,而Spring Data JPA对于删除这一块做了一点延伸补充,一起来看看规则吧:
删除操作的说明如下:
删除从表数据:可以随时任意删除。
删除主表数据:
有从表数据
1、在默认情况下,它会把外键字段置为null,然后删除主表数据。
如果在数据库的表结构上,外键字段有非空约束,默认情况就会报错了。
2、如果配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null,
没有关系)因为在删除时,它根本不会去更新从表的外键字段了。
3、如果还想删除,使用级联删除引用
没有从表数据引用:随便删
在实际开发中,级联删除请慎用!(在一对多的情况下)
可以测试一下是不是这样,现在customer是被引用的,我们直接删除id为1的数据试试,先将自动生成表配置修改成update。
@Test
@Transactional
@Rollback(value = false)
public void testRemove(){
customerDao.delete(1l);
}
因为被外键引用不能删除或者更新
级联操作
级联增加
级联:操作一个对象的同时操作他的关联对象
级联操作步骤:
- 1.需要区分操作主体
- 2.需要在操作主体的实体类上,添加级联属性(需要添加到多表映射关系的注解上)
- 3.cascade(配置级联)
首先明确需求,我们会给两个对象,一个是客户,一个是联系人,当我使用保存客户的时候,自动级联,即同时保存联系人。
CascadeType源码:
package javax.persistence;
public enum CascadeType {
/** Cascade all operations */
ALL,
/** Cascade persist operation */
PERSIST,
/** Cascade merge operation */
MERGE,
/** Cascade remove operation */
REMOVE,
/** Cascade refresh operation */
REFRESH,
/**
* Cascade detach operation
*
* @since Java Persistence 2.0
*
*/
DETACH
}
为了演示级联的多种操作我们类型先选ALL,同时是操作客户完成所以加在客户上面。
测试代码(在update的生成表配置下):
/**
* 测试级联添加,添加一个客户的时候添加一个联系人
* 需要在操作主体上的实体类上,配置casade属性
*/
@Test
@Transactional
@Rollback(value = false)
public void testCascadeAdd(){
//创建一个客户
Customer customer = new Customer();
customer.setCustName("baidu1");
//创建一个联系人
LinkMan linkMan = new LinkMan();
linkMan.setLkmName("lisi");
customer.getLinkMans().add(linkMan);
//联系人关联客户,即从表对象加入主表对象,即添加外键
linkMan.setCustomer(customer);
customerDao.save(customer);
//linkManDao.save(linkMan);
}
级联删除
Ok,需求是在只写删除一号客户的同时,将一号客户关联的联系人也删除了:
步骤先要查询对象,才能执行增删改,所以先查再删对象。
/**
* 级联删除,只写在删除一号客户的情况下,将一号客户关联的联系人也删了
*/
@Test
@Transactional
@Rollback(value = false)
public void testCascadeDel(){
//查询一号客户
Customer one = customerDao.findOne(1l);
//删除一号客户
customerDao.delete(one);
}
OK再来回顾一遍!!
- 放弃维护外键权:使用mappedBy 配置对方配置了映射的属性名
- cascade级联操作:
* CascadeType.ALL :所有
* CascadeType.PERSIST :保存
* CascadeType.MERGE :修改
* CascadeType.REMOVE :删除
第四章多对多
4.1示例分析
我们采用的示例为用户和角色。
用户:指的是咱们班的每一个同学。
角色:指的是咱们班同学的身份信息。
比如A同学,它是我的学生,其中有个身份就是学生,还是家里的孩子,那么他还有个身份是子女。
同时B同学,它也具有学生和子女的身份。
那么任何一个同学都可能具有多个身份。同时学生这个身份可以被多个同学所具有。
所以我们说,用户和角色之间的关系是多对多。
4.2表关系建立
多对多的表关系建立靠的是中间表,其中用户表和中间表的关系是一对多,角色表和中间表的关系也是一对多,如下图所示:
环境搭建
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<!--spring 和 spring data jpa的配置-->
<!-- 1.创建entityManagerFactory对象交给spring容器管理-->
<bean id="entityManagerFactoty" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--配置的扫描的包(实体类所在的包) -->
<property name="packagesToScan" value="com.tho.domain" />
<!-- jpa的实现厂家 -->
<property name="persistenceProvider">
<bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
</property>
<!--jpa的供应商适配器 -->
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!--配置是否自动创建数据库表 -->
<property name="generateDdl" value="false" />
<!--指定数据库类型 -->
<property name="database" value="MYSQL" />
<!--数据库方言:支持的特有语法 -->
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
<!--是否显示sql -->
<property name="showSql" value="true" />
</bean>
</property>
<!--jpa的方言 :高级的特性 -->
<property name="jpaDialect" >
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
</property>
<!--注入jpa的配置信息
加载jpa的基本配置信息和jpa实现方式(hibernate)的配置信息
hibernate.hbm2ddl.auto : 自动创建数据库表
create : 每次都会重新创建数据库表
update:有表不会重新创建,没有表会重新创建表
-->
<property name="jpaProperties" >
<props>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>
<!--2.创建数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="root"/>
<property name="password" value="root3306"/>
<property name="jdbcUrl" value="jdbc:mysql:///jpa" />
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
</bean>
<!--3.整合spring dataJpa-->
<jpa:repositories base-package="com.tho.dao" transaction-manager-ref="transactionManager"
entity-manager-factory-ref="entityManagerFactoty" />
<!--4.配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactoty"/>
</bean>
<!-- 4.txAdvice-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 5.aop-->
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.tho.service.*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut" />
</aop:config>
<!--5.声明式事务 -->
<!-- 6. 配置包扫描-->
<context:component-scan base-package="com.tho" />
</beans>
UserDao:
package com.tho.dao;
import com.tho.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface UserDao extends JpaRepository<User,Long>, JpaSpecificationExecutor<User> {
}
RoleDao:
package com.tho.dao;
import com.tho.domain.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface RoleDao extends JpaRepository<Role,Long>, JpaSpecificationExecutor<Role> {
}
Role实体类:
package com.tho.domain;
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;
/**
* 多对多
* 1.声明表关系
* @ManyToMany(targetEntity = User.class)
* targetEntity表示对方的实体类字节码
* 2.配置中间表(包含两个表)
* @JoinTable
* name :中间表的名称
* joinColumns,当前对象在中间表的外键
* inverseJoinColumns 对方对象在中间表的外键
*/
@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")}
)
private Set<User> users=new HashSet<User>();
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;
}
@Override
public String toString() {
return "Role{" +
"roleId=" + roleId +
", roleName='" + roleName + '\'' +
", users=" + users +
'}';
}
}
User:
package com.tho.domain;
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 = User.class)
* targetEntity表示对方的实体类字节码
* 2.配置中间表(包含两个表)
* @JoinTable
* name :中间表的名称
* joinColumns,当前对象在中间表的外键
* inverseJoinColumns 对方对象在中间表的外键
*/
@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> users= new HashSet<Role>();
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> getUsers() {
return users;
}
public void setUsers(Set<Role> users) {
this.users = users;
}
@Override
public String toString() {
return "User{" +
"userId=" + userId +
", userName='" + userName + '\'' +
", age=" + age +
", users=" + users +
'}';
}
}
注意现在的多对多也是双向的:
偷一下懒,让其自动生成实体类:
编写测试类:
package com.tho.test;
import com.tho.dao.RoleDao;
import com.tho.dao.UserDao;
import com.tho.domain.Role;
import com.tho.domain.User;
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(locations = "classpath:applicationContext.xml")
public class ManytoManyTest {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/**
* 保存一个用户,保存一个角色
*/
@Test
@Transactional
@Rollback(false)
public void testAdd(){
User user=new User();
user.setUserName("xiaowu");
Role role=new Role();
role.setRoleName("javaDev");
userDao.save(user);
roleDao.save(role);
}
}
已经学上手的同学已经马上就知道问题了,还是测试没有配置关联关系。
ok修改一下,加入关系:
测试多对多
运行一下:
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry ‘1-1’ for key ‘PRIMARY’
意思是主键已经有了,不要重复插入1-1作为主键,要怎么处理呢,我们可以选择通过一个关联起来,又或者一方放弃维护主键。
也可以根据需求设置的让一方放弃维护主键。
多对多放弃主键维护
先两个关系都注释了,生成一个没有关联的中间表,然后再修改放弃维护主键测试
和一对多或者多对一一样:属性使用mappedBy,这次测试让角色放弃维护主键
放开运行一下:
ok也成功了,因为role实体类已经放弃维护了,只用它进行关联是不可以生成中间的数据的:
同理也可以配置用户这样,这里就不做演示,想必各位读者也明白了映射关系和使用了。
多对多级联
ok级联,需求:在保存用户的时候,保存角色,以及与其关系。还是一样使用cascade。加的地方是谁级联出另一对象,则配置在谁上面。
级联添加测试:
@Test
@Transactional
@Rollback(false)
public void testCascadeAdd(){
User user=new User();
user.setUserName("xiaoMing");
Role role=new Role();
role.setRoleName("C++Dev");
//配置用户到角色的多对多关系,可以对中间表进行维护 1-1
user.getRoles().add(role);
//配置角色到用户的多对多关系,,可以对中间表进行维护 1-1
role.getUsers().add(user);
userDao.save(user);
}
刷新一下数据库:
测试级联删除
需求:根据id删除对象,同时将对象的角色也删除。
实现步骤:
- 先查询id为1的对象
- 删除id为1的user
/**
* 删除user用户,级联删除role角色
*/
@Test
@Transactional
@Rollback(false)
public void testCascadeDel(){
//先查询出id为1的user
User one = userDao.findOne(1l);
//删除id为1的user,通过级联删除
userDao.delete(one);
}
org.springframework.dao.InvalidDataAccessApiUsageException: The entity must not be null!; nested exception is java.lang.IllegalArgumentException: The entity must not be null!
Caused by: java.lang.IllegalArgumentException: The entity must not be null!
报错了,为什么呢?看到这个错误还没马上反应过来证明还没入门噢,还不够熟悉噢,说实体类不能为空,怎么实体类就为空呢?嗯,没错,熟悉的小伙伴就想起来了我们使用的自动生成表,没有改成update,同时演示这个的原因是,因为这里会抛异常,所以我们在实际开发的过程中,切记要异常处理
!!!
改为update,再使用我们的联级添加,再联级删除看看。
再跑联级删除:
OK,回顾一下使用联级,首先要先多表关系注解中加入联级属性,值根据需求选是全部联级还是具体什么操作可以联级。加的地方是谁级联出另一对象,则配置在谁上面。
@ManyToMany(targetEntity = 目标实体类.class,cascade = CascadeType.ALL)
最后展示一下多对多的源码,我们按下载源码的话,其实注释都是可以看见使用样例,我们懒加载没演示,可以通过配置打印多的一方的对象语句测试,运行debug断点查看。
/*
* Copyright (c) 2008, 2009, 2011 Oracle, Inc. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution. The Eclipse Public License is available
* at http://www.eclipse.org/legal/epl-v10.html and the Eclipse Distribution License
* is available at http://www.eclipse.org/org/documents/edl-v10.php.
*/
package javax.persistence;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static javax.persistence.FetchType.LAZY;
/**
* Defines a many-valued association with many-to-many multiplicity.
*
* <p> Every many-to-many association has two sides, the owning side
* and the non-owning, or inverse, side. The join table is specified
* on the owning side. If the association is bidirectional, either
* side may be designated as the owning side. If the relationship is
* bidirectional, the non-owning side must use the <code>mappedBy</code> element of
* the <code>ManyToMany</code> annotation to specify the relationship field or
* property of the owning side.
*
* <p> The join table for the relationship, if not defaulted, is
* specified on the owning side.
*
* <p> The <code>ManyToMany</code> annotation may be used within an
* embeddable class contained within an entity class to specify a
* relationship to a collection of entities. If the relationship is
* bidirectional and the entity containing the embeddable class is the
* owner of the relationship, the non-owning side must use the
* <code>mappedBy</code> element of the <code>ManyToMany</code>
* annotation to specify the relationship field or property of the
* embeddable class. The dot (".") notation syntax must be used in the
* <code>mappedBy</code> element to indicate the relationship
* attribute within the embedded attribute. The value of each
* identifier used with the dot notation is the name of the respective
* embedded field or property.
*
* <pre>
*
* Example 1:
*
* // In Customer class:
*
* @ManyToMany
* @JoinTable(name="CUST_PHONES")
* public Set<PhoneNumber> getPhones() { return phones; }
*
* // In PhoneNumber class:
*
* @ManyToMany(mappedBy="phones")
* public Set<Customer> getCustomers() { return customers; }
*
* Example 2:
*
* // In Customer class:
*
* @ManyToMany(targetEntity=com.acme.PhoneNumber.class)
* public Set getPhones() { return phones; }
*
* // In PhoneNumber class:
*
* @ManyToMany(targetEntity=com.acme.Customer.class, mappedBy="phones")
* public Set getCustomers() { return customers; }
*
* Example 3:
*
* // In Customer class:
*
* @ManyToMany
* @JoinTable(name="CUST_PHONE",
* joinColumns=
* @JoinColumn(name="CUST_ID", referencedColumnName="ID"),
* inverseJoinColumns=
* @JoinColumn(name="PHONE_ID", referencedColumnName="ID")
* )
* public Set<PhoneNumber> getPhones() { return phones; }
*
* // In PhoneNumberClass:
*
* @ManyToMany(mappedBy="phones")
* public Set<Customer> getCustomers() { return customers; }
* </pre>
*
* @see JoinTable
*
* @since Java Persistence 1.0
*/
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface ManyToMany {
/**
* (Optional) The entity class that is the target of the
* association. Optional only if the collection-valued
* relationship property is defined using Java generics. Must be
* specified otherwise.
*
* <p> Defaults to the parameterized type of
* the collection when defined using generics.
*/
Class targetEntity() default void.class;
/**
* (Optional) The operations that must be cascaded to the target
* of the association.
*
* <p> When the target collection is a {@link java.util.Map
* java.util.Map}, the <code>cascade</code> element applies to the
* map value.
*
* <p> Defaults to no operations being cascaded.
*/
CascadeType[] cascade() default {};
/** (Optional) Whether the association should be lazily loaded or
* must be eagerly fetched. The EAGER strategy is a requirement on
* the persistence provider runtime that the associated entities
* must be eagerly fetched. The LAZY strategy is a hint to the
* persistence provider runtime.
*/
FetchType fetch() default LAZY;
/**
* The field that owns the relationship. Required unless
* the relationship is unidirectional.
*/
String mappedBy() default "";
}
第五章Spring Data JPA中的多表查询
5.1对象导航查询
对象图导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。例如:我们通过ID查询方式查出一个客户,可以调用Customer类中的getLinkMans()方法来获取该客户的所有联系人。对象导航查询的使用要求是:两个对象之间
必须存在关联关系
。
使用一对多测试,因为一对多可能是单一对象属性,也有可能是集合
需求:查询一个客户,获取该客户下的所有联系人
先数据库表构造一下数据:
/**
* 测试对象导航查询
*/
@Test
public void testQuery1(){
Customer customer = customerDao.findOne(1l);
//对象导航查询,此用户下的所有联系人,get方法获得
Set<LinkMan> linkMans = customer.getLinkMans();
for (LinkMan linkMan:linkMans) {
System.out.println(linkMan);
}
}
测试一下:
报错:org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.tho.domain.Customer.linkMans, could not initialize proxy - no Session
没有session,为什么呢?其实是因为在测试代码中事务环境不统一,我们开启事务注解即可解决这个问题
我记得此前的案例里面联系人没有写toString方法,我们加一下:
@Override
public String toString() {
return "LinkMan{" +
"lkmId=" + lkmId +
", lkmName='" + lkmName + '\'' +
", lkmGender='" + lkmGender + '\'' +
", lkmPhone='" + lkmPhone + '\'' +
", lkmMobile='" + lkmMobile + '\'' +
", lkmEmail='" + lkmEmail + '\'' +
", lkmPosition='" + lkmPosition + '\'' +
", lkmMemo='" + lkmMemo + '\'' +
", customer=" + customer +
'}';
}
运行测试:又报错
spring data jpa多表关系,使用对象导航时候,出现内存溢出现象:
java.lang.StackOverflowError
内存溢出,为什么呢?其实是执行查询语句的时候获得对象,对象互相嵌套设置出现StackOverflowError异常,为什么嵌套了呢因为一对多两边都对象的对象作为属性,而两边又都有toString方法,就导致你toString我,我toString你,出现递归现象,最后就内存溢出了。
那本例解决的方法,最好最快的方法当然是把客户的toString注释了
再思考个问题对象导航采取的是获得对象是使用的什么加载机制呢?是立即加载吗还是延迟加载,我相信答案大家都猜得到。那我们就来验证一下。
/**
* 测试对象导航查询是否默认为懒加载?
* 即调用get方法并不会立即发送,而是在使用关联对象使用的时候才加载
*
*/
@Test
@Transactional//解决在java代码中的no session问题
public void testQuery2(){
Customer customer = customerDao.findOne(1l);
//对象导航查询,此用户下的所有联系人,get方法获得
Set<LinkMan> linkMans = customer.getLinkMans();
System.out.println(linkMans.size());
}
然后断点调试
get的时候输出的查询语句:
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=?
执行语句并没有查联系人,到使用了才有发送查询联系人
查看发现一对多关联确实默认是懒加载
测试立即加载测试:
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=?
很明显区别
多对一的对象导航
我们也先推测一下,默认是什么,对一的感觉立即加载也并不会对性能造成太大影响,那他是不是呢?
测试代码:
/**
* 测试对多一,对象导航查询是否默认为懒加载?
* 联系人对象导航客户的时候,即get的时候是不是立即查询客户
*/
@Test
@Transactional//解决在java代码中的no session问题
public void testQuery3(){
LinkMan linkMan = linkManDao.findOne(2l);
//对象导航查询
Customer customer = linkMan.getCustomer();
System.out.println(customer);
}
为了控制变量,把一对多的立即加载去了
可见是立即加载的,其实在多对一使用id查询的时候就相关于变相的一对一了
多对一配置懒加载
我们是通过联系人导航过来的是,所以懒加载开启是加在多对一这一注解下。
select
linkman0_.lkm_id as lkm_id1_1_0_,
linkman0_.lkm_cust_id as lkm_cust9_1_0_,
linkman0_.lkm_email as lkm_emai2_1_0_,
linkman0_.lkm_gender as lkm_gend3_1_0_,
linkman0_.lkm_memo as lkm_memo4_1_0_,
linkman0_.lkm_mobile as lkm_mobi5_1_0_,
linkman0_.lkm_name as lkm_name6_1_0_,
linkman0_.lkm_phone as lkm_phon7_1_0_,
linkman0_.lkm_position as lkm_posi8_1_0_
from cst_linkman linkman0_ where linkman0_.lkm_id=?
没有查询customer,打开输出注释
ok,确实是懒加载。
5.2使用Specification查询多表查询
/**
* 使用Specification查询多表查询
*/
@Test
@Transactional
public void testFind(){
Specification<LinkMan> specification = new Specification<LinkMan>() {
public Predicate toPredicate(Root<LinkMan> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//Join代表链接查询,通过root对象获取
Join<LinkMan, Customer> lJoincPath = root.join("customer", JoinType.INNER);
//创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
//JoinType.LEFT : 左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连
return cb.like(lJoincPath.<String>get("custName"), "baidu%");
}
};
LinkMan linkMan = linkManDao.findOne(specification);
System.out.println(linkMan);
}
总结回顾
知识量有点大,最后回顾一下几点关键点:
- 条件查询通过构造Specification,而构造则是使用cb对象来构造,不管单表还是多表。
- 维护主键,即指类对表外键或者中间表的维护,即通过类创建或者删除外键字段,有时候需要放弃尤其是多对多的时候,双向的话可能因为可能出现重复主键报错
- 级联操作,级联操作配置cascade,注意谁级联出另一对象就配置谁那。
- 对象导航,在一对多中默认懒加载,多对一默认立即加载
代码托管码云:https://gitee.com/calmtho/springdatajpa
ok后期有空会更新整合springboot相关的博客,喜欢的就尽情期待吧