Mybatis笔记
一、概述
框架:封装通用功能,软件开发中的半成品,简化开发过程。
轻量级的,持久层框架,负责完成java和数据库的通信。
代码分布:DAO+Service
MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)
1. 诞生背景
Java的原生数据库通信API( jdbc ),使用过于繁琐,而且随着数据变得复杂越发变得繁冗。
如下一个极其简单的查询动作,足以说明问题:
// 1> 自己管理 所有底层对象:驱动类,Connection,Statement,ResultSet
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("mysql:jdbc://localhost:3306/db9?\
useUnicode=true&characterEncoding=utf8");
PreparedStatement preparedStatement = connection.prepareStatement("select id,name,age from t_user");
ResultSet resultSet = preparedStatement.executeQuery();
// 2> 自己处理数据转换过程:【 数据表格 ==> java对象 】
ArrayList<User> users = new ArrayList<User>();
while(resultSet.next()){
Integer id = resultSet.getInt("id");
String name = resultSet.getString("name");
Integer age = resultSet.getInt("age");
User user = new User(id,name,age);
users.add(user);
}
2. ORM
概念:Object Relational Mapping,对象关系映射。
目标:在【java对象】 和 【关系表】 建立映射
:简化两者之间的通信过程。可以直接通信。( ops: 如上的过程不再用自己处理 )
细节:ORM是持久层框架(MyBatis,Hibernate),的重要底层设计思路。为简化持久层开发提供驱动
: java持久层框架将jdbc纳入底层,然后上层架设orm,使开发者脱离jdbc的繁琐
MyBatis对ORM的践行方式: |
---|
二、编码过程
- Jdk环境:jdk1.8
- 数据库环境:MySQL 5.1
- Mybatis:3.4.5
1. 搭建流程
1.1 导入依赖
// pom.xml
<!-- mybatis 依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- mysql驱动 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
//pom.xml,使得src/main/java下的xml文件可以进入编译范围
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
</project>
1.2 定义O和R
create table t_user(
id int primary key auto_increment,
name varchar(20),
gender char(1),-- tinyint(1)
create_time datetime
)default charset=utf8 engine=innodb;
class User{
private Integer id;
private String name;
private Boolean gender;
private Date createTime;
//get/set....
}
1.3 M-定义映射文件
DAO接口定义不变,映射文件即Mapper文件替换之前的DAO实现
public interface UserDAO {
public List<User> queryAll();
public User queryOne(@Param("id") Integer id);
public List<User> queryManyByName(@Param("name") String name);
public List<User> queryManyByDate(@Param("createTime") Date craeteTime);
// 明确为参数定义别名,用于mapper文件中获取
public List<User> queryUserByIdAndName(@Param("id") Integer id, @Param("name") String name);
}
// UserDAO.xml 重点搞清楚 何为映射
<!-- 不用定义dao的实现类,此映射文件等价于dao的实现 -->
<!-- namespace="dao接口路径"-->
<?xml version="1.0" encoding="UTF-8"?>
<!-- dtd:document type definition 配置文件规范 -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhj.dao.UserDAO">
<!--
根据id查询用户,User queryOne(Integer id)
select:配置查询语句
id:可以通过id找到执行的statement,对应dao中的一个方法名
parameterType: 参数类型 【int long float double string boolean date 自建类型(com.zhj.domain.User)】
resultType:结果类型,查询的结果会被封装到对应类型的对象中
#{}:相当于占位符
#{id}:就是把 “queryOne” 方法的名为id的参数填充到占位上
-->
<select id="queryOne" parameterType="int" resultType="com.zhj.domain.User">
select id as id,name as name,gender as gender,create_time as createTime
from t_user
where id=#{id}
</select>
<!-- 注意:返回类型实际是List<User> , 但此处只需定义User即可 -->
<select id="queryAll" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from t_user
</select>
<select id="queryManyByName" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from t_user
where name like #{name}
</select>
<select id="queryManyByDate" parameterType="date" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from t_user
where create_time=#{createTime}
</select>
<!-- 注意:此时方法有多个参数,【#{xx}】取值时,需要@param支持 -->
<select id="queryUsers" resultType="com.qianfeng.pojo.User"
parameterType="com.qianfeng.pojo.User">
select id,name,gender,birth from t_user where id=#{id} or name like #{name}
</select>
</mapper>
1.4 搭建配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 使用 id为“development”的环境信息 -->
<environments default="development">
<environment id="development">
<!-- 配置JDBC事务控制,由mybatis进行管理 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据源,采用mybatis连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<!-- 【&】是特殊字符,【&】是【&】的转义 -->
<property name="url" value="jdbc:mysql://localhost:3306/db9?useUnicode=true&characterEncoding=utf8" />
<property name="username" value="root" />
<property name="password" value="111111" />
</dataSource>
</environment>
</environments>
<!-- 加载映射文件 -->
<mappers>
<!-- 使用资源的路径
<mapper resource="com/zhj/dao/UserDAO.xml"/> -->
<!-- 加载某个包下的映射文件 (推荐)
要求:Mapper接口的名称与映射文件名称一致 且 必须在同一个目录下 -->
<package name="com.zhj.dao"/>
</mappers>
</configuration>
2. 测试使用
2.1 核心API
SqlSessionFactoryBuilder
:该对象负责加载MyBatis配置文件 并 构建SqlSessionFactory实例SqlSessionFactory
:每一个MyBatis的应用程序都以一个SqlSessionFactory对象为核心。负责创建SqlSession对象实例。SqlSession
:等价于jdbc中的Connection,用于完成数据操作。
//1、读取配置文件
String resource = "configuration.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//2、根据配置文件创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//3、SqlSessionFactory创建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
2.2 数据操作
- 通过SqlSession获得DAO实现(Mapper)
- 通过DAO实现完成具体的数据操作
// 获得UserDAO的实现
// UserDAO userMapper = sqlSession.getMapper(UserDAO.class);
UserDAO userDAO = sqlSession.getMapper(UserDAO.class);
//根据id查询
userDAO.queryOne(1);
//查询所有
userDAO.queryAll();
//根据姓名模糊查询
userDAO.queryManyByName("%zhj%");
//根据日期查询
Date date = new GregorianCalendar(2019, 11, 12).getTime();
userDAO.queryManyByDate(date);
回收资源:操作最后,关闭sqlSession
sqlSession.close();//关闭sqlSession
2.3 分页操作
定义分页信息类:
class Page{
private Integer pageNum; //页号
private Integer pageSize; //每页几条数据
private Integer offset; //偏移量,即从哪条查起
//offset的get方法中动态计算偏移量
public Integer getOffset() {
return (pageNum-1)*pageSize;
}
//其他set/get
}
定义DAO接口:
interface User{
...
public List<User> queryUserByPage(Page page);
}
定义Mapper:
<!-- #{offset} 取值时实际是会调用Page类中的getOffset()方法。 -->
<select id="queryUserByPage" parameterType="com.zhj.domain.Page" resultType="User">
select id,name,gender,create_time from t_user
limit #{offset},#{pageSize}
</select>
测试:
UserDAO userDAO = sqlSession.getMapper(UserDAO.class);
Page page = new Page(3,3);//第3页,每页3条
List<User> users = userDAO.queryUserByPage(page);
3. 增删改操作
3.1 DAO接口
public interface UserDAO {
...
public Integer insertUser(User user);
public Integer deleteUser(Integer id);
public Integer updateUser(User user);
}
3.2 映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace="dao接口路径"-->
<mapper namespace="com.zhj.dao.UserDAO">
....
....
<!-- 添加用户
1>注意:#{方法参数对象的属性名}
2>parameterType 和 resultType中都可以使用 缺省包
3>增加时id补全两种方式
-->
<insert id="insertUser" parameterType="com.zhj.domain.User">
<selectKey resultType="int" keyProperty="id" order="AFTER">
SELECT last_insert_id()
</selectKey>
insert into tt2 (name,gender,create_time)
values(#{name},#{gender},#{createTime})
</insert>
<insert id="insertUser2" parameterType="com.zhj.domain.User">
insert into tt2 (name,gender,create_time)
values(#{name},#{gender},#{createTime})
</insert>
<!--
删除用户
-->
<delete id="deleteUser" parameterType="int">
delete from tt2
where id=#{id}
</delete>
<!--
修改用户
-->
<update id="updateUser" parameterType="com.zhj.domain.User">
update tt2 set name=#{name},gender=#{gender},create_time=#{createTime}
where id= #{id}
</update>
</mapper>
3.3 测试使用
注意:增删改操作需要控制事务,否则操作不会同步到数据库
//事务控制
SqlSession sqlSession = sqlSessionFactory.openSession();// 随着session获得,事务会自动开启
.... //数据操作
.... //数据操作
sqlSession.commit();//提交事务
sqlSession.rollback();//回滚事务
增删改操作
try{
....
userDAO.updateUser(user);//除了需要控制事务之外,和查询操作使用无异
/**User u = new User(...);
userDAO.insertUser(u);**/
/**userDAO.deleteUser(1)**/
sqlSession.commit();//提交事务
}catch(Exception e){
e.printStackTrace();
sqlSession.rollback();//回滚事务
}finally{
if(sqlSession!=null){
sqlSession.close();//回收资源
}
}
3.4 增加细节
ID缺失!
User user = new User("zhj",true,new Date());//此时user没有id
userDAO.insertUser(user);
sqlSession.commit();//此时数据已经插入到数据库中,数据库中有id,但user依然没有id
System.out.println(user.getId());//没有id
//则无法得知插入的数据是哪一条,如果后续程序需要此id,则出现bug!
两种解决方案:
1> selectKey标签
2> useGenerateKeys 和 keyProperty属性
<insert id="insertUser" parameterType="com.zhj.domain.User">
<!-- AFTER:此中语句在插入语句之后执行
resultType=“int”: 此中语句执行后的返回类型是 int
keyProperty="id": 此中语句返回值要 传递给当前方法参数中的id属性 (com.zhj.domain.User的id属性)
select last_insert_id():mysql特性语句,返回当前事务中,最近一次插入的数据的id-->
<selectKey resultType="int" keyProperty="id" order="AFTER">
select last_insert_id()
</selectKey>
insert into t_user (name,gender,create_time)
values(#{name},#{gender},#{createTime})
</insert>
<!-- useGeneratedKeys="true" 声明此处添加中id用的是自动增长
keyProperty="id" 将id值 传递给当前方法的参数中的id属性 (com.zhj.domain.User的id属性)-->
<insert id="insertUser" parameterType="com.zhj.domain.User" useGeneratedKeys="true"
keyProperty="id">
insert into t_user (name,gender,create_time)
values(#{name},#{gender},#{createTime})
</insert>
三、别名
在mapper文件中,
parameterType
和resultType
中使用类型时:
<select id="xxx" parameterType="com.zhj.domain.Page" resultType="com.zhj.domain.User">
除mybatis自动映射的类型外,其他类型都要定义完整路径,相对繁琐。可用如下两种方式简化:
// configuration.xml
<configuration>
...
<typeAliases>
<!-- 1. 单独为每个类定义别名,则 "Page"等价于"com.zhj.domain.Page"
<typeAlias type="com.zhj.domain.Page" alias="Page"/>
<typeAlias type="com.zhj.domain.User" alias="User"/>-->
<!-- 2. 定义缺省包,当mapper中的类型没有定义包时,使用此配置作为默认包;
则 “Page” 会自动认为是 “com.zhj.domain”中的“Page”,即“com.zhj.domain.Page”
则 “User” 会自动认为是 “com.zhj.domain”中的“User”,即“com.zhj.domain.User”-->
<package name="com.zhj.domain"/>
</typeAliases>
...
</configuration>
有如上别名配置后,mapper中:
<select id="xxx" parameterType="Page" resultType="User">
四、参数分离
在mybatis的配置文件中有一项重要且可能需要经常改动的配置,即,数据库连接参数。
可以单独进行管理,方便后续维护。
<!-- 如下四项参数 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/db9?useUnicode=true&characterEncoding=utf8"/>
<property name="username" value="root" />
<property name="password" value="111111" />
</dataSource>
1. 单独定义参数文件
# 在resources目录下,创建 jdbc.properties文件
# 参数名=参数值
jdbc.user=root
jdbc.password=111111
jdbc.url=jdbc:mysql://localhost:3306/db9?useUnicode=true&characterEncoding=utf8
jdbc.driver=com.mysql.jdbc.Driver
2. 加载参数文件
// 在 configuration.xml中
<configuration>
<!-- 加载参数文件 -->
<properties resource="jdbc.properties"></properties>
....
....
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<!-- 使用 ${参数名} 获取参数文件中的值。如此这四项参数需要修改时,需要改动参数文件即可 -->
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.user}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
五、关联关系
项目中的表都不是孤立的,会彼此关联。在操作数据时,需要联合多张表。
我们需要将表间的关系映射清楚,mybatis可以提供很好的
多表联合操作支持,
比如:
查询所有用户及其所有订单
查询所有学生及其所学课程
等操作,可以省去很多冗繁代码,直接拿到结果。
关联关系种类:
1对1
1对多
多对多
Person(公民) Passport(护照)
1对1
User(用户) Order(订单)
1对多
Student(学生) Course(课程)
多对多
1. 一对一
1.1 建表
drop table IF EXISTS t_passport;
drop table IF EXISTS t_person;
create table t_person(
id int primary key AUTO_INCREMENT,
name VARCHAR(50),
age SMALLINT
)DEFAULT CHARSET = utf8 ENGINE =innodb;
create table t_passport(
id int primary key AUTO_INCREMENT,
note VARCHAR(50),
create_time DATE,
person_id int UNIQUE,
FOREIGN KEY(person_id) REFERENCES t_person(id)
)DEFAULT CHARSET = utf8 ENGINE =innodb;
insert into t_person(name,age) values("zhangsan",18);
insert into t_person(name,age) values("lisi",19);
insert into t_passport(note,create_time,person_id) values("pass1",\'2019-05-07\',1);
insert into t_passport(note,create_time,person_id) values("pass2",\'2019-05-07\',2);
1.2 建类
public class Person {
private Integer id;
private String name;
private Integer age;
//关系属性
private Passport passport;
//set/get
}
public class Passport {
private Integer id;
private String note;
private Date createTime;
//关系属性
private Person person;
//set/get
}
1.3 查询
查询某个Person,及其Passport
//PersonDAO :查询某个Person的信息及其Passport的信息
public Person queryPersonAndPassport(Integer id);
//映射文件
<select id="queryPersonAndPassport" resultType="Person" parameterType="int">
select
t_person.id as pid,
name,
age,
t_passport.id as passid,
note,
create_time,
person_id
from t_person join t_passport
ON t_person.id = t_passport.person_id
where t_person.id=#{id};
</select>
如上映射文件,存在问题 ==> 关系属性:passport,需要多个列映射到这一个属性上,但并没有映射清楚 |
---|
若要完成上图中的映射,不能再使用默认的同名映射规则,需要定制映射规则。
resultMap
— 映射规则定制利器
<!-- 定义查询中的各个列 与 属性的映射规则 -->
<!-- id=标识 type=返回类型(将列映射到User的属性上) -->
<resultMap id="person_passport" type="Person">
<!-- 主键列pid,用 id标签配置; property=属性名 column=列名 -->
<id property="id" column="pid"/>
<!-- 常规列用 result标签配置; property=属性名 column=列名 -->
<result property="name" column="name"/>
<result property="age" column="age"/>
<!-- 重点:关系属性【Passport passport】是一个对象
用 association标签配置,如下配置就是在完成上图中 未完成之映射
property=属性名 javaType=该属性的类型 -->
<association property="passport" javaType="Passport">
<!-- 将passid列 映射到 passport的id属性中 -->
<id property="id" column="passid"/>
<!-- 将note列 映射到 passport的note属性中 -->
<result property="note" column="note"/>
<!-- 将create_time列 映射到 passport属性的createTime属性中 -->
<result property="createTime" column="create_time"/>
</association>
</resultMap>
<!-- 删除之前的resultType, 改为使用resultMap="person_passport"
即,采用resultMap中定义的映射规则去封装对象-->
<select id="queryPersonAndPassport" resultMap="person_passport" parameterType="int">
select
t_person.id as pid,
name,
age,
t_passport.id as passid,
note,
create_time,
person_id
from t_person join t_passport
ON t_person.id = t_passport.person_id
where t_person.id=#{id};
</select>
1.4 测试
// 查询id=1的person及其passport
Person person = personDAO.queryPersonAndPassport(1);
// 从person中获取passport
Passport passport = person.getPassport();
**思考题 ==> 请自主设计,完成以下: **
查询所有Person及其Passport:public List<Person> queryAll();
查询某个Passport及其Person数据:public Passport queryPassportAndPerson(Integer id)
细节:
增删改,没有任何特别需要改动的,正常定义<insert> <update> <delete> 即可
2. 一对多
2.1 建表
drop table IF EXISTS t_order;
drop table IF EXISTS t_user;
create table t_user( # 用户表
id int primary key AUTO_INCREMENT,
name VARCHAR(50),
gender char(1),
regist_time DATE
)DEFAULT CHARSET = utf8 ENGINE =innodb;
create table t_order( # 订单表
id int primary key AUTO_INCREMENT,
price VARCHAR(50),
note VARCHAR(50),
create_time DATE,
user_id int,
FOREIGN KEY(user_id) REFERENCES t_user(id)
)DEFAULT CHARSET = utf8 ENGINE =innodb;
insert into t_user(name,gender,regist_time) values(\'zhangsan\',\'1\',\'2019-12-12\');
insert into t_user(name,gender,regist_time) values(\'lisi\',\'0\',\'2019-12-11\');
insert into t_order(price,note,create_time,user_id) values(3000.55,\'哈哈\',\'2019-12-12\',1);
insert into t_order(price,note,create_time,user_id) values(600.55,\'哈哈2\',\'2019-12-12\',1);
insert into t_order(price,note,create_time,user_id) values(900.55,\'呵呵2\',\'2019-12-12\',2);
insert into t_order(price,note,create_time,user_id) values(4000.55,\'呵呵2\',\'2019-12-12\',2);
2.2 建类
public class User {
private Integer id;
private String name;
private Boolean gender;
private Date registTime;
//关系属性,存储用户的多个订单
private List<Order> orders;
//set/get
}
public class Order {
private Integer id;
private BigDecimal price;
private String note;
private Date createTime;
//关系属性
private User user;
//set/get
}
2.3 查询
//DAO
public queryOneUserAndOrders(Integer id);//根据用户id查询用户及其所有订单
<!-- 按照之前的思路,映射如此定义,但此时,resultType为User,所以只能接收User本身的信息!
那如何将查询中得到的Order信息也保存到User中呢?
User中保存Order信息的属性名为“orders”,则此属性需要有一个和它同名的列才可以接收值!
但是该属性的值又不可能来自某一个列,而是多个列的值!
我们该如何配置,才可以将多列的值对应到一个属性上呢?
-->
<select id="queryOneUserAndOrders" parameterType="int" resultType="User">
select t_user.id as uid,
name,
gender,
regist_time as registTime,
t_order.id as oid,
note,price,
create_time as createTime
from t_user join t_order
ON t_user.id = t_order.user_id
where t_user.id=#{id}
</select>
如上映射文件,存在问题 ==> 关系属性:orders,需要多个列映射到这一个属性上,但并没有映射清楚 |
---|
若要完成上图中的映射,不能再使用默认的同名映射规则,需要定制映射规则。
resultMap
— 映射规则定制利器
<!-- 定义查询中的各个列 与 属性的映射规则 -->
<!-- id=标识 type=最终返回类型(将列映射到User的属性上) -->
<resultMap id="user_orders" type="User">
<!-- 主键列单独用id标签标识
property=类中属性名 column=查询中的列名-->
<id property="id" column="uid"/>
<!-- 常规属性都用result标签 -->
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="registTime" column="registTime"/>
<!-- 重点:关系属性【List<Order> orders】,非主键也非常规属性,而是一个List
用collection标签配置,用于完成上图中未完成之映射。
property=属性名 ofType=集合中的泛型类型-->
<collection property="orders" ofType="Order">
<!-- Order的主键列 -->
<id property="id" column="oid"/>
<!-- 常规属性用result标签 -->
<result property="note" column="note"/>
<result property="price" column="price"/>
<result property="createTime" column="createTime"/>
</collection>
</resultMap>
<!-- 此处是resultType换为resultMap,进而引用了如上定义的“user_orders” -->
<select id="queryOneUserAndOrders" parameterType="int" resultMap="user_orders">
select t_user.id as uid,
name,
gender,
regist_time as registTime,
t_order.id as oid,
note,price,
create_time as createTime
from t_user join t_order
ON t_user.id = t_order.user_id
where t_user.id=#{id}
</select>
2.4 测试
//查询用户1,及其订单信息
User user = userDAO.queryOneUserAndOrders(1);
//获取用户1的订单信息
List<Order> orders = user.getOrders();
**思考题 ==> 请自主设计,完成以下: **
查询所有User及其Order:public List<User> queryAll();
查询某个Order及其User:public Order queryOrderAndUser(Integer orderid)
细节:
增删改,没有任何特别需要改动的,正常定义<insert> <update> <delete> 即可
3. 多对多
3.1 建表
drop table IF EXISTS t_student;
drop table IF EXISTS t_course;
drop table IF EXISTS t_student_course;
create table t_student(
id int primary key AUTO_INCREMENT,
name VARCHAR(50),
age SMALLINT
)DEFAULT CHARSET = utf8 ENGINE =innodb;
create table t_course(
id int primary key AUTO_INCREMENT,
title VARCHAR(50)
)DEFAULT CHARSET = utf8 ENGINE =innodb;
create table t_student_course(
student_id int,
course_id int,
FOREIGN KEY (student_id) REFERENCES t_student(id),
FOREIGN KEY (course_id) REFERENCES t_course(id),
PRIMARY KEY (student_id,course_id)
)DEFAULT CHARSET = utf8 ENGINE =innodb;
insert into t_student (name,age) values("zhangsan",18);
insert into t_student (name,age) values("lisi",19);
insert into t_course (title) values("java基础");
insert into t_course (title) values("mybatis");
insert into t_student_course values(1,1);
insert into t_student_course values(1,2);
insert into t_student_course values(2,1);
insert into t_student_course values(2,2);
3.2 建类
public class Student {
private Integer id;
private String name;
private Integer age;
//关系属性
private List<Course> courses;
//set/get
}
public class Course {
private Integer id;
private String title;
//关系属性
private List<Student> students;
//set/get
}
public class StudentCourse {
private Integer studentId;
private Integer courseId;
//set/get
}
3.3 查询
//StudentDAO
public List<Student> queryStudents();
//StudentDAO.xml 映射文件
<!-- 定义映射规则 和一对多中使用一样-->
<resultMap id="student_course" type="Student">
<id column="sid" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<!-- 此处使用collection和一对多中一样 -->
<collection property="courses" ofType="Course">
<id column="cid" property="id"/>
<result column="title" property="title"/>
</collection>
</resultMap>
<!-- 查询,并根据映射规则完成数据封装 -->
<select id="queryStudents" resultMap="student_course">
select
t_student.id as sid,
name,
age,
t_course.id as cid,
title
from t_student join t_student_course
ON t_student.id = t_student_course.student_id
JOIN t_course
ON t_student_course.course_id = t_course.id;
</select>
3.4 测试
StudentDAO studentDAO = sqlSession.getMapper(StudentDAO.class);
//查询所有学生及其所有课程
List<Student> students = studentDAO.queryStudents();
//获取每个学生的所有课程
for(Student stu:students){
System.out.println(stu);
for(Course cour:stu.getCourses()){
System.out.println(cour);
}
}
3.5 增加测试
//StudentDAO.xml
<mapper namespace="com.zhj.dao.StudentDAO">
<!-- 增加Student到 t_student表 ( ops:后续操作需要新增学生的id )-->
<insert id="insertStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="id">
insert into t_student(name,age) values(#{name},#{age})
</insert>
</mapper>
//StudentCourseDAO.xml
<mapper namespace="com.zhj.dao.StudentCourseDAO">
<!-- 为学生添加课程 或 为课程添加学生 -->
<insert id="insertStudentCourse" parameterType="StudentCourse">
insert into t_student_course(student_id,course_id) values(#{studentId},#{courseId})
</insert>
</mapper>
//添加学生 赵六,并为其关联课程 1
// 获得 学生DAO 和 学生课程DAO
StudentDAO studentDAO = sqlSession.getMapper(StudentDAO.class);
StudentCourseDAO scDAO = sqlSession.getMapper(StudentCourseDAO.class);
// 新建学生,并添加
Student student = new Student(null, "赵六", 22);
studentDAO.insertStudent(student);
// 新建学生赵六和课程1的关系,并添加 (为赵六添加课程 1)
StudentCourse sc = new StudentCourse(student.getId(),1);
scDAO.insertStudentCourse(sc);
// 提交
sqlSession.commit();
4. 关系的方向(了解)
单向关系:A B 双方,A中有B,或B中有A
双向关系:A B双方,A中有B,且B中有A
//互相持有彼此,即为双向关系
class User{
...
private List<Order> orders;
}
class Order{
...
private User user;
}
//Order不持有User,或User不持有Order,即为单向关系
class User{
...
private List<Order> orders;
}
class Order{
...
}
六、# 和 $
1. 各自特点
//如果用$,则必须用 @Param注解,否则${name}会认为是要从参数中取名为name的属性
public List<User> test3(@Param("name") String a);
<!-- 注意${} 就是在做字符拼接,所以此处用了【‘${name}’】而不是【${name}】
类比jdbc的sql语句的拼接:
String name="zhj";
String sql = "select ... from tt2 where name=\'"+name+"\'";//此处是要加单引号的
注意:sql拼接时,有sql注入的风险
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name = \'${name}\'
</select>
<!-- 注意#{} 就是在做占位符,所以此处用了【#{name}】而不是【’#{name}‘】
类比jdbc的sql语句的拼接:
String name="zhj";
String sql = "select ... from tt2 where name=?“;//此处是不加单引号的
...
prepareStatement.executeQuery(sql,name);//占位符赋值
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
where name = #{name}
</select>
2. 使用$场景
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from tt2
order by id ${name}
</select>
<select id="test4" parameterType="string" resultType="com.zhj.domain.User">
select id,name,gender,create_time as createTime
from ${tn}
where name = #{name}
</select>
<!-- 用在列名上亦可 -->
userDAO.test3("desc");
userDAO.test3("asc");
userDAO.test4("t_user");
userDAO.test4("t_admin");
八、动态sql
在映射文件中,定义了要执行的sql语句,mybatis支持在sql语句中填充一些逻辑,是的sql语句可以呈现不同的语义,
即动态sql
1. IF
在sql中 注入
if
,可以让sql更加灵活,让一个查询语句,可以应对更多查询情景。重点:== != > < >= <= and or
: 比较字符串需要加引号,比较数字、布尔、null不用加引号
常用:对参数是否为空的判断,动态决定sql语句的组成
情景:对用户可以有通过name 或 gender的搜索,如果没有
if
动态逻辑,则是要定义多个<select>
<select id="queryUsers" parameterType="User" resultType="User">
SELECT id,name,gender,regist_time
FROM t_user
WHERE
<if test="name != null and name!=\'\'">
name=#{name}
</if>
<if test="gender != null">
AND gender=#{gender}
</if>
</select>
<!-- 如上如果gender为null,name不为null,则sql为:
SELECT id,name,gender,regist_time
FROM t_user
WHERE name=#{name}
-->
<!-- 如上如果gender不为null,name不为null,则sql为:
SELECT id,name,gender,regist_time
FROM t_user
WHERE name=#{name} AND gender=#{gender}
-->
<!-- 补充:比较的其他用法 -->
<select id="queryUsers" parameterType="User" resultType="User">
SELECT id,name,gender,regist_time
FROM t_user
WHERE
<if test="name == null or name==\'zhj\'">
name=#{name}
</if>
<if test="id>=1">
AND id>#{id}
</if>
<if test="gender == false">
AND gender=#{gender}
</if>
</select>
2. Choose
如果在多个判断中,只会有一个是可以成立的,可以使用Choose标签
<!-- 如果id不为空就按id查询。如果id为空但name不为空就按name查询。如果都为空,就查询所有男性用户。 -->
<select id="queryUsers" parameterType="User" resultType="User">
SELECT id,name,gender,regist_time
FROM t_user
WHERE
<choose> <!-- 从上到下,有任何一个when标签,判断成功则停止判断 -->
<when test="id != null"> <!-- 判断 -->
id > #{id}
</when>
<when test="name !=null"> <!-- 判断 -->
name = #{name}
</when>
<otherwise> <!-- 以上判断都不成立时,执行 -->
gender = \'1\'
</otherwise>
</choose>
</select>
注意,在sql中的判断,本意不在于判断,而在于将sql动态的适应不同场景,简化开发。
3. Where
动态sql在使用中,存在一些问题:
<select id="queryUsers" parameterType="User" resultType="User">
SELECT id,name,gender,regist_time FROM t_user
WHERE
<if test="name != null">
name=#{name}
</if>
<if test="id>=1">
AND id>#{id}
</if>
</select>
<!-- 如果 name=null,id=3,则sql变为:
SELECT id,name,gender,regist_time FROM t_user
WHERE AND id>#{id}
-->
<!-- 如果 name=null,id=0,则sql变为:
SELECT id,name,gender,regist_time FROM t_user
WHERE
-->
<where>
标签中如果没有成立的条件,则不会拼接where语句
;
<where>
标签中如果以 and 或 or开头,会去将其去除。
<select id="queryUsers" parameterType="User" resultType="User">
SELECT id,name,gender,regist_time
FROM t_user
<where>
<if test="name == null">
name=#{name}
</if>
<if test="id>=1">
AND id>#{id}
</if>
</where>
</select>
<!-- 如果 name=null,id=3,则sql变为:
SELECT id,name,gender,regist_time FROM t_user
WHERE id>#{id}
-->
<!-- 如果 name=null,id=0,则sql变为:
SELECT id,name,gender,regist_time FROM t_user
-->
4. Set
<set>
标签主要用于update
中
<!-- 只更新非空的字段 -->
<update id="updateUser" parameterType="User">
UPDATE t_user2
SET
<if test="name != null">
name = #{name},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="registTime != null">
regist_time = #{registTime}
</if>
WHERE id = #{id}
</update>
<!-- 如果registTime=null,name或gender!=null,则sql为:
UPDATE t_user2
SET name = #{name},gender = #{gender},
WHERE id = #{id}
注意:多了逗号,sql语法错误
-->
使用
<set>
:
<update id="updateUser" parameterType="User">
UPDATE t_user2
<set>
<if test="name != null">
name = #{name},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="registTime != null">
regist_time = #{registTime}
</if>
</set>
WHERE id = #{id}
</update>
<!-- 如果registTime=null,name或gender!=null,则sql为:
UPDATE t_user2
SET name = #{name},gender = #{gender}
WHERE id = #{id}
注意:<set>会自动补充一个 ”set语句“,并将最后一个逗号去除
-->
5. Trim
<select id="queryUsers" parameterType="User" resultType="User">
SELECT id,name,gender,regist_time
FROM t_user2
<trim prefix="where" prefixOverrides="and|or">
<if test="name != null">
name=#{name}
</if>
<if test="id>=10">
OR id>#{id}
</if>
<if test="gender == false">
AND gender=#{gender}
</if>
</trim>
</select>
<!-- <trim prefix="where" prefixOverrides="and|or"> 等价于 <where>
prefix="where":会添加一个where关键字
prefixOverrides="and|or":会去除开头的and 或 or
-->
<update id="updateUser" parameterType="User">
UPDATE t_user2
<trim prefix="set" suffixOverrides=",">
<if test="name != null">
name = #{name},
</if>
<if test="gender != null">
gender = #{gender},
</if>
<if test="registTime != null">
regist_time = #{registTime}
</if>
</trim>
WHERE id = #{id}
</update>
<!-- <trim prefix="set" suffixOverrides=","> 等价于 <set>
prefix="set":会添加一个set关键字
suffixOverrides=",":会去除最后的逗号
-->
6. Foreach
批量查询:id为 1,3,5的数据
//DAO声明为: public List<User> queryUsers2(List<Integer> ids);
<select id="queryUsers2" resultType="User">
SELECT id,name,gender,regist_time
FROM t_user2
WHERE id IN
<foreach collection="list" open="(" separator="," close=")" item="id" index="ind">
#{id}
</foreach>
</select>
<!--
<foreach collection="list" open="(" separator="," close=")" item="id" index="ind">
collection="list" 代表参数是List,如果是数组,要用array
open="(" 以“(” 开头
close=")" 以“)” 结尾
separator="," 值之间用“,”分隔
item="id" 每次遍历的值的临时变量
#{id} 获取每次遍历的值
如上:标签的效果是 (值1,值2,值3)
示例:如果传入 List{1 3 5},则最终的sql:
【SELECT id,name,gender,regist_time
FROM t_user2
WHERE id IN (1,3,5)】
-->
批量添加:
insert into t_user (name,gender) values(\’xx\’,1),(\’xxx\’,0),(\’xxxx\’,1)
<!-- 批量添加 -->
<insert id="insertUsers" parameterType="java.util.List">
insert into t_user2 (name,gender,regist_time) values
<foreach collection="list" item="user" index="ind" close="" open="" separator=",">
(#{user.name},#{user.gender},#{user.registTime})
</foreach>
</insert>
List<User> users = ...;
System.out.println(userDAO.insertUsers(users));
7. Sql
复用sql语句
<!-- 定义一段sql -->
<sql id="order">
id,note,price,create_time as createTime
</sql>
<!-- 引用sql -->
<select id="queryOrder" parameterType="int" resultType="Order">
select
<include refid="order"/>
from t_order
where id = #{id}
</select>
九、缓存
缓存:将数据库的数据临时的存储起来,以更好的支持查询。
问题:如果有数据,查询频繁且更新极少,此种数据如果依然每次到数据库查询,效率偏低。
解决:将如上数据,临时的存储到内存中,提供对外界的查询服务,进而减少和数据库的通信,提高查询效率。
原理:当查询数据时,查询结果会被缓存在某个内存区域中,核心存储结构={sql:查询结果};
每次发起查询时,会先找到缓存,从中尝试获取数据,如果没有找到数据,再去查数据库,并将在数**
库中查到的结果存入缓存,以供后续查询使用。
MyBatis作为持久层框架,缓存管理自然是他的本职工作。
支持了两种缓存:
一级缓存,二级缓存
1. 一级缓存
存储位置:SqlSession;即一个SqlSession对象发起查询后,查询结果会缓存在自己内部
有效范围:同一个SqlSession的多次查询;即,同一个SqlSession的多次相同sql的查询可以使用一级缓存
开启:不用任何配置,默认启动。
清除:
sqlSession.clearCache();
2. 二级缓存
2.1 概述
**存储位置:SqlSessionFactory;同一个SqlSessionFactory创建的所有SqlSession发起的查询,查询结果都会缓存在 **
SqlSessionFactory内部。
有效范围:同一个SqlSessionFactory
开启:默认开启,但需要制定哪个DAO的Mapper需要使用二级缓存,定义一个
<cache>
即可注意:二级缓存必须在
sqlSession.commit()
或sqlSession.close()
之后才生效清除:
sqlSession.rollback();//则查询的结果不会进入二级缓存
2.2 应用
二级缓存使用:
<mapper namespace="com.zhj.dao.UserDAO">
<!-- 当前mapper中的所有查询,都进入二级缓存
缓存数据中涉及的pojo一定要实现 Serialiable。
-->
<cache></cache>
<select>...</select>
.....
</mapper>
UserDAO userDAO1 = sqlSession1.getMapper(UserDAO.class);
UserDAO userDAO2 = sqlSession2.getMapper(UserDAO.class);
userDAO1.queryOne(1);
userDAO2.queryOne(1);
// 在开启了二级缓存的情况下,如上代码依然会查询两次数据库。
// userDAO1.queryOne(1);之后缓存只在sqlSession1中,并未进入二级缓存。userDAO2.queryOne(1);无法使用
UserDAO userDAO1 = sqlSession1.getMapper(UserDAO.class);
UserDAO userDAO2 = sqlSession2.getMapper(UserDAO.class);
userDAO1.queryOne(1);
sqlSession1.commit();//close()也可以,因为close内部流程和commit内部流程有对缓存的相同处理
userDAO2.queryOne(1);
// 此时如上代码会只查询一次数据库。
// sqlSession1.commit();执行时,会将查到的数据序列化,存入二级缓存中。userDAO2.queryOne(1)可以使用
思考题:如下代码会如何查询数据库 ?
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserDAO userDAO= sqlSession.getMapper(UserDAO.class);
UserDAO userDAO2= sqlSession2.getMapper(UserDAO.class);
UserDAO userDAO3= sqlSession3.getMapper(UserDAO.class);
userDAO.queryOne(1);
userDAO.queryOne(2);
sqlSession.close();
userDAO2.queryOne(3);
sqlSession2.close();
userDAO3.queryOne(1);
userDAO3.queryOne(2);
userDAO3.queryOne(3);
//请分析如上查询,会触发哪些数据库查询
2.3 结构
二级缓存存储结构 |
---|
2.4 清除
二级缓存是以
namespace
为单位组织的,当某个namespace
中发生数据改动,则namespace
中缓存的所有数据会被mybatis清除。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis_config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserDAO userMapper = sqlSession.getMapper(UserDAO.class);
UserDAO userMapper2 = sqlSession2.getMapper(UserDAO.class);
UserDAO userMapper3 = sqlSession3.getMapper(UserDAO.class);
userMapper.queryUserById(1);
sqlSession.close();//二级缓存生效
// userMapper中的所有二级缓存被清除
userMapper2.updateUser(new User(1,"zs",true,new Date()));
sqlSession2.commit();
// 再次查询,二级缓存中已没有数据,会查询数据库
userMapper3.queryUserById(1);
2.5 cache-ref
和关系属性相关
注意如果
<collection>
中没有使用select
关联查询,则不存在此问题。
<mapper namespace="com.zhj.dao.UserDAO">
<cache/>
<resultMap id="user_orders" type="User">
<id property="id" column="uid"/>
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="registTime" column="registTime"/>
<collection property="orders" select="com.zhj.dao.OrderDAO.queryOrderOfUser" column="id" fetchType="eager/lazy"/>
</resultMap>
<select id="queryOne" parameterType="int" resultMap="user_orders">
select id,name,gender,regist_time as registTime
from t_user
where id=#{id}
</select>
</mapper>
<mapper namespace="com.zhj.dao.OrderDAO">
<!-- 使用cache-ref 则OrderDAO的缓存数据,会存放于com.zhj.dao.UserDAO分支下,
与UserDAO的缓存数据存储于同一个Perpetual对象中
-->
<cache-ref namespace="com.zhj.dao.UserDAO"/>
<select id="queryOrderOfUser" parameterType="int" resultType="Order">
select id as oid,note,price,create_time as createTime
from t_order
where user_id = #{userId}
</select>
</mapper>
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
//User 和 关系属性Order 都被缓存
User user = userDAO.queryOne(1);
user.getOrders().size();
sqlSession.commit();
OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //有二级缓存数据可用 (OrderDAO中必须有 <cache>或<cache-ref>)
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
userDAO.queryOne(1);
//数据改动,此时会清空User缓存,并清空Order缓存(因为order中是 <cache-ref>,如果是<cache>则此处只会清空User缓存)
userDAO.insertUser(new User(null,"new_user",true,new Date()));
sqlSession.commit();
OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //重新查询数据库
十、PageHelper
1. 使用过程
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>RELEASE</version>
</dependency>
<!--
plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下:
properties?, settings?,
typeAliases?, typeHandlers?,
objectFactory?,objectWrapperFactory?,
plugins?,
environments?, databaseIdProvider?, mappers?
-->
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 页号自动回归到合理数值 -->
<property name="reasonable" value="true"/>
</plugin>
</plugins>
<!-- spring等价配置
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor"></bean>
</array>
</property>
</bean>
-->
//使用:
PageHelper.startPage(2,3);// 第2页,每页3条数据,pageNum,pageSize
PageHelper.orderBy("id desc");//可以选择设置排序(可选)
List<User> users = mapper.queryAllUsers();//PageHelper后的第一个查询语句,会被PageHelp增强处理(可观测mysql日志)
for (User user : users) {// users中已经是分页数据
System.out.println(user);
}
//包装一个PageInfo,其中会持有所有分页会用到的信息:当前页号,每页多少条,共多少页,是否为第一页/最后一页,是否有下一页等。
PageInfo<User> pageInfo=new PageInfo<User>(users);
PageInfo对象 概览 |
---|
注意:如果是多表查询,则会有如下效果:
select t_user.id,name,gender,regist_time,
t_order.id orderId,price,note,create_time
from t_user JOIN t_order
ON t_user.id = t_order.user_id LIMIT 2, 2
#是对大表做了分页,此时数据可能不完整,比如用户有10个订单,却只能查到2个,或部分。
2. 重要提示
PageHelper.startPage
方法重要提示只有紧跟在
PageHelper.startPage
方法后的第一个Mybatis的查询(Select)方法会被分页。请不要配置多个分页插件
请不要在系统中配置多个分页插件(使用Spring时,
mybatis-config.xml
和Spring<bean>
配置方式,请选择其中一种,不要同时配置多个分页插件)!分页插件不支持带有
for update
语句的分页对于带有
for update
的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。