1. MyBatis简介
- MyBatis提供了数据库查询的自动对象绑定功能;SQL写在XML中,便于统一管理和优化,解除了SQL与代码的耦合;执行效率比Hibernate快;对于复杂SQL,使用MyBatis会更加灵活。但关联的表或字段较多时,SQL工作量很大;而且SQL依赖于数据库,导致数据库移植性较差。
- Hibernate是全自动,Hibernate完全可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成SQL;对于单表的CRUD,使用Hibernate开发更加高效。但学习和精通的门槛较高,而且由于是自动生成SQL,因此无法直接维护SQL。
- MyBatis官网:https://mybatis.org/mybatis-3/
- MyBatis作用:
- 减少JDBC的重复代码
- 使数据库环境信息的硬编码和执行的SQL语句的硬编码解耦
- MyBatis分页插件PageHelper网址:https://pagehelper.github.io/
2. MyBatis基本使用
以查询user表中所有user为例
2.1 创建Maven工程,添加坐标
junit
junit
4.12
test
org.projectlombok
lombok
1.18.10
provided
mysql
mysql-connector-java
5.1.47
org.mybatis
mybatis
3.5.3
2.2 创建POJO
-- 创建user表
CREATE TABLE user(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT "id",
username VARCHAR(255) NOT NULL DEFAULT "" COMMENT "用户名",
password VARCHAR(16) NOT NULL DEFAULT "123456" COMMENT "密码",
balance DOUBLE(16, 3) NOT NULL DEFAULT 0.0 COMMENT "余额"
)ENGINE=INNODB, CHARSET=utf8, COMMENT="用户表";
-- 添加用户
INSERT INTO user VALUES
(NULL, 'user1', 'password1', 1000),
(NULL, 'user2', 'password2', 2000),
(NULL, 'user3', 'password3', 3000);
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Integer id;
private String username;
private String password;
private Double balance;
}
2.3 创建PojoDao接口
原则:MyBatis的所有PojoDao接口,方法参数只设置一个;
由于是全类名+方法名的保存和寻找策略,因此PojoDao接口里的方法不能重载
PojoDao接口的工作原理是JDK动态代理
public interface UserDao {
void addUser(User user);
void deleteById(Integer id);
void updateUser(User user);
List findAll();
}
2.4 创建外部jdbc配置文件
在resources下创建jdbc.properties文件:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/db01?useSSL=false
jdbc.username=root
jdbc.password=123456
2.5 创建MyBatis核心配置文件*
- 存储在resources根路径下
- 文件名随意(如:SqlMapConfig.xml)
- 需要引入约束
核心配置文件的顺序:
properties:引入外部properties文件
settings:全局配置参数
-
typeAliases:给类型设置别名
核心配置文件:typeAliases定义单个别名
typeHandlers:类型处理器
objectFactory:对象工厂
objectWrapperFactory
reflectorFactory
plugins:插件
environments:环境配置
databaseIdProvider:数据库厂商标识
mappers:映射器
2.6 创建SQL映射文件*
- 存储在resources里面
- 全路径和文件名要和相应的PojoDao接口的相同(在resources里创建多级文件夹时用/分隔,而不是.)
- 需要引入约束
根标签mapper的namespace属性的值为对应的PojoDao接口的全限定名
根标签mapper下的每一个子标签对应PojoDao接口中的一个方法:
- 查询方法对应select标签
- 添加方法对应insert标签
- 删除方法对应delete标签
- 修改方法对应update标签
子标签的id对应方法的名字
子标签的parameterType对应方法的形参类型
子标签的resultType(只有select标签有)对应方法的返回值类型:
- 如果返回值类型是简单类型,则直接写对应的Java类型
- 如果返回值类型是POJO、Map,则直接写POJO的全限定名
- 如果返回值类型是Collection,则resultType是集合中元素的类型
- 结果集的字段名和POJO的属性名能够对应时,resultType才可以自动映射
使用参数中的数据:
- 如果参数为pojo、map,则通过#{属性名或Map的key}来引入对应的值
- 如果参数为基本类型数据,则通过#{任意字符串}来引入对应的值
- 如果参数为pojo包含pojo,如ResultBean中包含User,则使用#{user.username}
resultMap标签:解决查询出来的结果的列名和JavaBean属性不一致的情况
- id属性:映射规则的唯一id
- type属性:将SQL查询出来的结果手动映射到哪个类
select LAST_INSERT_ID()
insert into user values (null, #{username}, #{password}, #{balance})
update user set username = #{username}, password = #{password}, balance = #{balance}
delete from user where id = #{id}
2.7 单元测试
public class UserDaoTest {
InputStream is;
SqlSession sqlSession;
UserDao userDao;
@Before
public void init() throws IOException {
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 读取核心配置文件,转换成字节输入流
is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 3. 创建SqlSessionFactory对象
SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(is);
// 4. 创建SqlSession对象
sqlSession = sessionFactory.openSession();
// 5. 创建UserDao的代理对象
userDao = sqlSession.getMapper(UserDao.class);
}
@Test
public void testFindAll() {
// 6. 调用需要执行的方法
List userList = userDao.findAll();
for (User user : userList) {
System.out.println(user);
}
}
@Test
public void testAddUser() {
User user = new User(0, "user4", "654321", 4000.0);
userDao.addUser(user);
System.out.println(user.getId());
}
@After
public void destroy() throws IOException {
// 在CUD操作中,需要通过sqlSession.commit()方法来执行提交操作
sqlSession.commit();
// 7. 释放资源
sqlSession.close();
is.close();
}
}
2.8 #{}与${}的区别*
- 传参的区别:
-
#{}
先用?占位,然后再设置参数,可以防止SQL注入 - ${}直接拼接SQL语句
-
-
#{}
不能写在引号里,而${}必须要写在引号里 - pojo、map类型的参数,#{}和${}括号里都是写属性名或Map的key
- 基本类型、字符串类型的参数,#{}和${}括号里可以写任意字符串
- 模糊查询:
- "%"#{username}"%"
- concat("%", #{username}, "%")
- "%${value}%"
2.9 自定义SqlSessionFactory工具类
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class SqlSessionFactoryUtils {
private static InputStream is;
private static SqlSessionFactory sessionFactory;
static {
try {
// 1. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 2. 读取核心配置文件,转换成字节输入流
is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 3. 创建SqlSessionFactory对象
sessionFactory = sqlSessionFactoryBuilder.build(is);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取一个SqlSession
*/
public static SqlSession openSession() {
SqlSession sqlSession = null;
// 4. 创建SqlSession对象
sqlSession = sessionFactory.openSession();
return sqlSession;
}
/**
* 提交事务并关闭资源
*/
public static void commitAndClose(SqlSession sqlSession) {
sqlSession.commit();
sqlSession.close();
}
/**
* 回滚事务并关闭资源
*/
public static void rollbackAndClose(SqlSession sqlSession) {
sqlSession.rollback();
sqlSession.close();
}
}
3. MyBatis日志
-
导入坐标:
log4j log4j 1.2.12 org.slf4j slf4j-api 1.6.6 org.slf4j slf4j-log4j12 1.6.6 -
在resources下创建log4j.properties文件:
#日志级别:ERROR > WARN > INFO > DEBUG log4j.rootLogger=DEBUG,stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=[%-5p] %t %l %d %rms:%m%n log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.File=E:\\Logs\\IdeaLogs\\IdeaLogs.log log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS\} %-5p [%t] {%c}-%m%n
4. Mybatis动态SQL*
动态SQL:SQL语句的主体结构,在编译时无法确定,只有等到程序运行后,在执行过程中才能确定
4.1 if标签
满足test属性的逻辑表达式,则if标签里面的语句有效
多条件查询时,每次查询的条件不确定有哪些,就可以使用if标签
当参数值为int的0时,MyBatis会将参数值其转化为空字符串
select * from user
where balance = #{balance}
4.2 where标签
select * from user
and balance = #{balance}
and password = #{password}
4.3 foreach标签
delete from user
#{id}
4.4 sql标签和include标签
sql标签可以定义公共的sql片段,需要时可以用include标签引入sql片段
select * from user
5. Mybatis多表关联查询*
建立数据库环境:
DROP TABLE IF EXISTS user;
-- 创建用户表
CREATE TABLE user(
uid INT PRIMARY KEY AUTO_INCREMENT COMMENT "id",
username VARCHAR(255) NOT NULL DEFAULT "" COMMENT "用户名",
password VARCHAR(16) NOT NULL DEFAULT "123456" COMMENT "密码",
balance DOUBLE(16, 3) NOT NULL DEFAULT 0.0 COMMENT "余额"
)ENGINE=INNODB, CHARSET=utf8, COMMENT="用户表";
-- 添加用户
INSERT INTO user VALUES
(NULL, 'user1', 'password1', 1000),
(NULL, 'user2', 'password2', 2000),
(NULL, 'user3', 'password3', 3000);
-- 创建账户表
CREATE TABLE account (
aid INT AUTO_INCREMENT PRIMARY KEY,
money DOUBLE,
uid INT,
CONSTRAINT fk_account_uid FOREIGN KEY (uid) REFERENCES user(uid)
)ENGINE=INNODB, CHARSET=utf8;
-- 添加账户
INSERT INTO account VALUES
(NULL, 1000, 1),
(NULL, 2000, 1),
(NULL, 1000, 2),
(NULL, 2000, 2),
(NULL, 3000, 3);
-- 创建课程表
CREATE TABLE course(
cid INT PRIMARY KEY AUTO_INCREMENT,
coursename VARCHAR(255),
book VARCHAR(255)
)ENGINE=INNODB, CHARSET=utf8;
-- 添加课程
INSERT INTO course VALUES
(NULL, '面向对象程序设计', 'Java编程思想'),
(NULL, '数据结构与算法', '算法导论'),
(NULL, '设计模式', '大话设计模式');
-- 创建选课表
CREATE TABLE course_selection(
uid INT,
cid INT,
CONSTRAINT fk_course_selection_uid FOREIGN KEY (uid) REFERENCES user(uid),
CONSTRAINT fk_course_selection_cid FOREIGN KEY (cid) REFERENCES course(cid),
PRIMARY KEY (uid, cid)
)ENGINE=INNODB, CHARSET=utf8;
-- 添加选课信息
INSERT INTO course_selection VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 1),
(2, 2),
(3, 1);
5.1 一对一
根据账户id查询账户信息,并关联查询对应的用户表信息;由于一个账户只能对应一个用户,因此为一对一查询
在外键所在的POJO中新增一个外键参考的POJO类型的属性
-
创建Account表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class Account implements Serializable { private Integer aid; private Double money; private Integer uid; /** * 表示一个Account对应一个User */ private User user; }
-
创建PojoDao接口:
public interface AccountDao { Account findAccountUserByAid(int aid); }
-
创建SQL映射文件:
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); AccountDao accountDao = sqlSession.getMapper(AccountDao.class); Account account = accountDao.findAccountUserByAid(1); System.out.println(account); /** * 结果: * Account(aid=1, money=1000.0, uid=1, user=User(id=null, uid=1, username=user1, password=password1, balance=1000.0)) */ }
5.2 一对多
根据用户id查询用户信息,并关联查询对应的所有账户表信息;由于一个用户可能对应多个账户,因此为一对多查询
在外键参考的POJO中新增一个List<外键所在POJO>类型的属性
-
创建User表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Integer uid; private String username; private String password; private Double balance; /** * 表示一个User对应多个Account */ private List
accountList; } -
创建PojoDao接口:
public interface UserDao { User findUserAccountByUid(int uid); }
-
创建SQL映射文件:
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); UserDao userDao = sqlSession.getMapper(UserDao.class); User user = userDao.findUserAccountByUid(1); System.out.println(user); /** * 结果: * User(uid=1, username=user1, password=password1, balance=1000.0, * accountList=[Account(aid=1, money=1000.0, uid=1, user=null), Account(aid=2, money=2000.0, uid=1, user=null)]) */ }
5.3 多对多
根据用户id查询用户所选的所有课程信息,相当于一个用户对应多个课程
多对多关系可以看成是双向的一对多关系
-
创建Course表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class Course implements Serializable { private Integer cid; private String coursename; private String book; }
-
创建User表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Integer uid; private String username; private String password; private Double balance; /** * 表示一个User对应多个Course */ private List
courseList; } -
创建PojoDao接口:
public interface UserDao { User findUserCourseByUid(int uid); }
-
创建SQL映射文件:
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); UserDao userDao = sqlSession.getMapper(UserDao.class); User user = userDao.findUserCourseByUid(2); System.out.println(user); /** * 结果: * User(uid=2, username=user2, password=password2, balance=2000.0, * courseList=[Course(cid=1, coursename=面向对象程序设计, book=Java编程思想), Course(cid=2, coursename=数据结构与算法, book=算法导论)]) */ }
6. Mybatis分步查询延迟加载*
- 立即加载:一调用方法就立即发起查询
- 延迟加载(懒加载):需要使用数据时,才发起查询;如果暂时不需要使用数据,则暂时不查询
- 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降
- 优点:先从单表查询,需要时再从关联表去关联查询;由于查询单表要比关联查询多张表速度要快,因此可以提高数据库性能
- 延迟加载的SQL语句类似于嵌套子查询,分步骤查询
-
全局延迟加载:在MyBatis核心配置文件中配置
6.1 association标签实现分步查询延迟加载
根据账户id查询账户信息,在需要用户信息时再关联查询对应的用户表信息
-
创建User表和Account表的POJO:
@NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Integer uid; private String username; private String password; private Double balance; } @Data @NoArgsConstructor @AllArgsConstructor public class Account implements Serializable { private Integer aid; private Double money; private Integer uid; /** * 表示一个Account对应一个User */ private User user; }
-
创建PojoDao接口:
public interface AccountDao { Account findAccountByAid(int aid); } public interface UserDao { User findUserByUid(int uid); }
-
创建SQL映射文件AccountDao.xml:
-
创建SQL映射文件UserDao.xml:
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); AccountDao accountDao = sqlSession.getMapper(AccountDao.class); Account account = accountDao.findAccountByAid(1); System.out.println(account.getMoney()); System.out.println(account.getUser()); }
6.2 collection标签实现分步查询延迟加载
根据用户id查询用户信息,在需要账户信息时再关联查询对应的所有账户表信息
-
创建User表和Account表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Integer uid; private String username; private String password; private Double balance; /** * 表示一个User对应多个Account */ private List
accountList; } @Data @NoArgsConstructor @AllArgsConstructor public class Account implements Serializable { private Integer aid; private Double money; private Integer uid; } -
创建PojoDao接口:
public interface UserDao { User findUserByUid(int uid); } public interface AccountDao { List
findAccountListByUid(int uid); } -
创建SQL映射文件UserDao.xml:
-
创建SQL映射文件AccountDao.xml:
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); UserDao userDao = sqlSession.getMapper(UserDao.class); User user = userDao.findUserByUid(1); System.out.println(user.getUsername()); System.out.println(user.getAccountList()); }
7. MyBatis缓存-
MyBatis缓存类别:
- 一级缓存:是sqlSession对象的缓存,自带的(不需要配置)不可卸载的(不想使用还不行);一级缓存的生命周期与sqlSession一致
- 二级缓存:是SqlSessionFactory的缓存,只要是同一个SqlSessionFactory创建的SqlSession就共享二级缓存的内容,并且可以操作二级缓存。二级缓存如果要使用,需要手动开启(需要配置)
7.1 一级缓存
- 查询时,先去缓存中找;如果缓存中没有,则从数据库查询用户信息,然后将查询到的信息存储到缓存中;如果缓存中有,则直接从缓存中获取信息。
- 清除一级缓存:
- sqlSession.commit(); // commit时清除一级缓存是为了避免数据发生增删改后产生脏读
- sqlSession.close();
- sqlSession.clearCache();
- 执行增删改操作
7.2 二级缓存
-
步骤1:在MyBatis核心配置文件中开启二级缓存(由于默认开启,因此这一步可省略)
-
步骤2:在要使用二级缓存的SQL映射文件的mapper标签里增加cache标签
步骤3:要使用二级缓存的POJO类必须实现Serializable接口
- 只有当前的sqlSession.close()时,该sqlSession的数据才会存入二级缓存中
- 执行增删改操作时会清除二级缓存
- select标签属性:
- flushCache默认为false
- useCache默认为true
- insert、delete、update标签属性:
- flushCache默认为true
- 没有useCache属性
8. MyBatis注解-
使用注解代替了SQL映射文件
@SelectKey():代替了selectKey标签
@Insert("SQL"):代替了insert标签
@Delete("SQL"):代替了delete标签
@Update("SQL"):代替了update标签
@Select("SQL"):代替了select标签
@Results(id = "", value = @Result())
或@Results(@Result())
或@Results({@Result(), @Result()})
@Result(column = "列名", property = "属性名", one = @One(select = "指定用来多表查询的sqlmapper"), many = @Many(select = ""))
@Results代替了resultMap标签
@Result代替了result标签和id标签
@One()代替了association标签
@Many()代替了collection标签
8.1 MyBatis注解实现基本CRUD
-
创建PojoDao接口:
public interface UserDao { @SelectKey(keyProperty = "uid", keyColumn = "uid", before = false, statement = "select last_insert_id()", resultType = int.class) @Insert("INSERT INTO user VALUES (NULL, #{username}, #{password}, #{balance})") void addUser(User user); @Delete("delete from user where uid = #{id}") void deleteById(Integer id); @Update("update user set username = #{username}, password = #{password}, balance = #{balance} where uid = #{uid}") void updateUser(User user); @Select("select * from user") List
findAll(); } 创建MyBatis核心配置文件
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); UserDao userDao = sqlSession.getMapper(UserDao.class); User user = new User(2, "user4", "password4", 4000.0); userDao.addUser(user); System.out.println(user.getUid()); userDao.deleteById(3); userDao.updateUser(user); userDao.findAll().stream().forEach(System.out::println); SqlSessionFactoryUtils.commitAndClose(sqlSession); }
8.2 MyBatis注解实现一对一映射及延迟加载
-
创建Account表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class Account implements Serializable { private Integer aid; private Double money; private Integer uid; /** * 表示一个Account对应一个User */ private User user; }
-
创建PojoDao接口:
public interface UserDao { @Select("select * from user where uid = #{id}") User findUserByUid(int uid); } public interface AccountDao { @Results(id = "accountUserMap", value = { @Result(column = "uid", property = "uid"), @Result(property = "user", one = @One(select = "com.liu2m.dao.UserDao.findUserByUid", fetchType = FetchType.LAZY), column = "uid") }) @Select("select * from account where aid = #{aid}") Account findAccountByAid(int aid); }
-
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); AccountDao accountDao = sqlSession.getMapper(AccountDao.class); Account account = accountDao.findAccountByAid(1); System.out.println(account.getMoney()); }
8.3 MyBatis注解实现一对多映射及延迟加载
-
创建User表的POJO:
@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Integer uid; private String username; private String password; private Double balance; /** * 表示一个User对应多个Account */ private List
accountList; } -
创建PojoDao接口:
public interface UserDao { @Results(id = "userAccountMap", value = { @Result(column = "uid", property = "uid", id = true), @Result(property = "accountList", many = @Many(select = "com.liu2m.dao.AccountDao.findAccountListByUid", fetchType = FetchType.LAZY), column = "uid") }) @Select("SELECT * FROM user WHERE uid = #{uid}") User findUserByUid(int uid); } public interface AccountDao { @Select("SELECT * FROM account WHERE uid = #{uid}") List
findAccountListByUid(int uid); } -
单元测试:
@Test public void test01() { SqlSession sqlSession = SqlSessionFactoryUtils.openSession(); UserDao userDao = sqlSession.getMapper(UserDao.class); User user = userDao.findUserByUid(1); System.out.println(user); }