目录
Mybatis简介
Mybatis介绍
MyBatis 本是 apache 的一个开源项目 iBatis, 2010 年这个项目由 apache software foundation 迁移到了 google code,并且改名为 MyBatis,实质上 Mybatis 对 ibatis 进行一些改进。MyBatis 是一个优秀的持久层框架,它对 jdbc 的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建 connection、创建 statement、手动设置参数、结果集检索等 jdbc 繁杂的过程代码。Mybatis 通过 xml 或注解的方式将要执行的各种 statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过 java 对象和 statement 中的 sql 进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射成 java 对象并返回。
与其他的对象关系映射框架不同,MyBatis 并没有将 Java 对象与数据库表关联起来,而是将 Java 方法与 SQL 语句关联。MyBatis 允许用户充分利用数据库的各种功能,例如存储过程、视图、各种复杂的查询以及某数据库的专有特性。如果要对遗留数据库、不规范的数据库进行操作,或者要完全控制 SQL 的执行,MyBatis 是一个不错的选择。
MyBatis 官网:https://mybatis.org/mybatis-3/zh/index.html
HelloWorld案例
通过一个简单的 HelloWorld 先来看下 MyBatis 的基本用法。
准备数据库
首先来准备一个数据库:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test01` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `test01`;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`address` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
创建普通的Maven工程
接下来创建一个普通的 Maven 工程,不用创建 Web 工程,JavaSE 工程即可。项目创建完成后,添加 MyBatis 依赖:
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
</dependencies>
创建一个软件包名字为model,用于存放实体类。在resources资源目录下创建一个mapper目录,用于存放xml文件。如下图:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/8c212b55558d5c002f632fb46406dd21.png#clientId=u2b58a9a4-dde8-4&from=paste&height=204&id=u942a27f4&margin=[object Object]&name=image.png&originHeight=407&originWidth=484&originalType=binary&ratio=1&size=20250&status=done&style=none&taskId=u883ba248-0a3e-4f43-881d-9ca74dc9fb9&width=242)
接下来,准备一个 Mapper 文件,Mapper 是用来在 MyBatis 中定义 SQL 的 XML 配置文件,由于在实际开发中,我们经常需要使用到 Mapper,经常需要自己创建 Mapper 文件,因此,我们可以将 Mapper 文件做成一个模板。具体操作如下:
1、在 IDEA 中,选择 resources 目录,右键单击,New–>Edit File Templates。
![image.png](https://www.icode9.com/i/ll/?i=img_convert/59022c825c1b480e49b09ecfbf5cc2d1.png#clientId=u285c1c92-dad1-4&from=paste&height=436&id=u14c2c9e4&margin=[object Object]&name=image.png&originHeight=871&originWidth=1006&originalType=binary&ratio=1&size=123400&status=done&style=none&taskId=uc7b723ba-a281-4764-b1e0-bc10c1f4e0f&width=503)
然后点击 + ,添加一个新的模板进来,给模板取名,同时设置扩展名,并将如下内容拷贝到模板中:
<?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">
<mapper namespace="#[[$namespace$]]#">
</mapper>
如下图:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/b0ad042a6ec52257729c67c07b618436.png#clientId=u285c1c92-dad1-4&from=paste&height=333&id=u62eb791b&margin=[object Object]&name=image.png&originHeight=666&originWidth=955&originalType=binary&ratio=1&size=95392&status=done&style=none&taskId=ue838b3c8-ebd8-4eaa-8d0f-f3652f7ea6a&width=477.5)
配置完成后,再次创建 Mapper 文件时,就可以选择 New–>mapper 了,这里,我们创建一个 UserMapper:
<?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">
<mapper namespace="mymapper">
<select id="getUserById" resultType="mybatis01.model.User">
select * from user where id = #{id};
</select>
</mapper>
定义User实体类
在model软件包下创建User实体类,内容如下:
@Data
public class User {
private Integer id;
private String username;
private String address;
}
@Data注解是生成get,set方法。使用之前需要提前引入Lombok依赖。
创建MyBatis配置文件
创建 MyBatis 配置文件,如果是第一次使用,可以参考官网,拷贝一下配置文件的头信息,如果需要多次使用这个配置文件,可以在 IDEA 中创建该配置文件的模板:
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="/mapper/UserMapper.xml"/>
</mappers>
</configuration>
在这个配置文件中,我们只需要配置 environments 和 mapper 即可,environment 就是 MyBatis 所连接的数据库的环境信息,它放在一个 environments 节点中,意味着 environments 中可以有多个 environment,为社么需要多个呢?开发、测试、生产,不同环境各一个 environment,每一个 environment 都有一个 id,也就是它的名字,然后,在 environments 中,通过 default 属性,指定你需要的 environment。每一个 environment 中,定义一个数据的基本连接信息。
在 mappers 节点中,定义 Mapper,也就是指定我们上一步所写的 Mapper 的路径。
最后,我们来加载这个主配置文件:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sessionFactory.openSession();
User user =(User) sqlSession.selectOne("getUserById", 1);
System.out.println(user);
sqlSession.close();
}
}
首先,我们加载主配置文件,生成一个 SqlSessionFactory,再由 SqlSessionFactory 生成一个 SqlSession,一个 SqlSession 就相当于是我们的一个会话,类似于 JDBC 中的一个连接,在 SQL 操作完成后,这个会话是可以关闭的。
在这里,SqlSessionFactoryBuilder 用于创建 SqlSessionFacoty,SqlSessionFacoty 一旦创建完成就不需要 SqlSessionFactoryBuilder 了,因为 SqlSession 是通过 SqlSessionFactory 生产,所以可以将 SqlSessionFactoryBuilder 当成一个工具类使用,最佳使用范围是方法范围即方法体内局部变量。
SqlSessionFactory 是一个接口,接口中定义了 openSession 的不同重载方法,SqlSessionFactory 的最佳使用范围是整个应用运行期间,一旦创建后可以重复使用,通常以单例模式管理 SqlSessionFactory。
SqlSession 中封装了对数据库的操作,如:查询、插入、更新、删除等。通过 SqlSessionFactory 创建 SqlSession,而 SqlSessionFactory 是通过 SqlSessionFactoryBuilder 进行创建。SqlSession 是一个面向用户的接口, sqlSession 中定义了数据库操作,默认使用 DefaultSqlSession 实现类。每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不能共享使用,它也是线程不安全的。因此最佳的范围是请求或方法范围。绝对不能将 SqlSession 实例的引用放在一个类的静态字段或实例字段中。打开一个 SqlSession;使用完毕就要关闭它。通常把这个关闭操作放到 finally 块中以确保每次都能执行关闭。
对 SqlSessionFactory 进行封装
基于上面几点,我们可以对 SqlSessionFactory 进行封装,在项目根目录新建一个utils软件包,并创建一个名为SqlSessionFactoryUtils类,内容如下:
public class SqlSessionFactoryUtils {
private static SqlSessionFactory SQL_SESSION_FACTORY = null;
public static SqlSessionFactory getInstance() {
if (SQL_SESSION_FACTORY == null) {
try {
SQL_SESSION_FACTORY = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
return SQL_SESSION_FACTORY;
}
}
这样,在需要使用的时候,通过这个工厂方法来获取 SqlSessionFactory 的实例。
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
MyBatis增删改查与间架构介绍
前面的 HelloWorld 案例,我们做了一个查询的 Demo,这里我们来看另外四种常见的操作。
添加单元测试
引入junit依赖
在Pom文件引入junit依赖;
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
新建测试单元类
在test目录新建测试类,内容如下:
public class MyBatisTest {
private SqlSession sqlSession;
private UserMapper userMapper;
@Before()
public void before() {
sqlSession = SqlSessionFactoryUtils.getInstance().openSession();
userMapper= sqlSession.getMapper(UserMapper.class);
}
@Test()
public void addUserTest(){
User user = new User();
user.setUsername("马士兵");
user.setAddress("javaboy");
Integer integer = userMapper.addUser(user);
sqlSession.commit();
System.out.println(integer);
}
@After()
public void after() {
sqlSession.close();
}
}
增
添加记录,id 有两种不同的处理方式,一种就是自增长,另一种则是 Java 代码传一个 ID 进来,传一个 ID 进来,这个 ID 可以是一个 UUID,也可以是其他的自定义的 ID。在 MyBatis 中,对这两种方式都提供了相应的支持。
主键自增长
首先我们在Mapper中,添加SQL插入语句:
<insert id="addUser" parameterType="mybatis01.model.User">
insert into user(username,address) values (#{username},#{address});
</insert>
这里有一个 parameterType 表示传入的参数类型。参数都是通过 # 来引用
然后,在 Java 代码中,调用这个方法:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = sessionFactory.openSession();
User user = new User();
user.setUsername("马云");
user.setAddress("杭州马");
int insert = sqlSession.insert("addUser", user);
sqlSession.commit();
System.out.println("返回影响行数:"+insert);
sqlSession.close();
}
}
注意,SQL 插入完成后,一定要提交,即 sqlSession.commit()
使用 UUID 做主键
也可以使用 UUID 做主键,使用 UUID 做主键,又有两种不同的思路,第一种思路,就是在 Java 代码中生成 UUID,直接作为参数传入到 SQL 中,这种方式就和传递普通参数一样,另一种方式,就是使用 MySQL 自带的 UUID 函数来生成 UUID。
这里我们使用第二种方式,因为第一种方式没有技术含量(自己练习)。使用 MySQL 自带的 UUID 函数,整体思路是这样:首先调用 MySQL 中的 UUID 函数,获取到一个 UUID,然后,将这个 UUID 赋值给 User 对象的 ID 属性,然后再去执行 SQL 插入操作,再插入时使用这个 UUID。
注意,这个实验需要先将数据的 ID 类型改为 varchar,User实体id数据类型也需要修改String
<insert id="adduser2" parameterType="mybatis01.model.User">
<selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
select uuid();
</selectKey>
insert into user(id,username,address) values (#{id},#{username},#{address})
</insert>
- selectKey 表示查询 key
- keyProperty 属性表示将查询的结果赋值给传递进来的 User 对象的 id 属性
- resultType 表示查询结果的返回类型
- order 表示这个查询操作的执行时机,BEFORE 表示这个查询操作在 insert 之前执行
- 在 selectKey 节点的外面定义 insert 操作
最后,在 Java 代码中,调用这个方法:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = sessionFactory.openSession();
User user = new User();
user.setUsername("马化腾");
user.setAddress("深圳马");
int insert = sqlSession.insert("addUser2", user);
sqlSession.commit();
System.out.println("返回影响行数:"+insert);
sqlSession.close();
}
}
删
删除操作比较容易,首先在 UserMapper 中定义删除 SQL:
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id = #{id}
</delete>
然后,在 Java 代码中调用该方法:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = sessionFactory.openSession();
int insert = sqlSession.delete("deleteUserById",1);
sqlSession.commit();
System.out.println("返回影响行数:"+insert);
sqlSession.close();
}
}
这里的返回值为该 SQL 执行后,数据库受影响的行数。
改
修改操作,也是先定义 SQL:
<update id="updateUser" parameterType="mybatis01.model.User">
update user set username = #{username} where id=#{id}
</update>
最后在 Java 代码中调用:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = sessionFactory.openSession();
User user = new User();
user.setUsername("didiplus");
user.setId(2);
int insert = sqlSession.update("updateUser",user);
sqlSession.commit();
System.out.println("返回影响行数:"+insert);
sqlSession.close();
}
}
查询
HelloWorld 中展示了根据 id 查询一条记录,这里来看一个查询所有:
<select id="getAlluser" resultType="mybatis01.model.User">
select * from user;
</select>
然后在 Java 代码中调用:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = sessionFactory.openSession();
User user = new User();
user.setUsername("didiplus");
user.setId(2);
List<User> users = sqlSession.selectList("getAlluser");
users.forEach(System.out::println);
sqlSession.close();
}
}
Mybatis Mapper详解
引入Mapper
前面我们所写的增删改查是存在问题的。主要问题就是冗余代码过多,模板化代码过多。例如,我想开发一个 UserDao,可能是下面这样:
public class UserDao {
private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getInstance();
public User getUserById(Integer id) {
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = (User) sqlSession.selectOne("getUserById", id);
sqlSession.close();
return user;
}
public Integer addUser(User user) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int insert = sqlSession.insert("addUser", user);
sqlSession.commit();
sqlSession.close();
return insert;
}
public Integer addUser2(User user) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int insert = sqlSession.insert("addUser2", user);
sqlSession.commit();
sqlSession.close();
return insert;
}
public Integer deleteUserById(Integer id) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int delete = sqlSession.delete("deleteUserById", id);
sqlSession.commit();
sqlSession.close();
return delete;
}
public Integer updateUser(User user) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int delete = sqlSession.delete("updateUser", user);
sqlSession.commit();
sqlSession.close();
return delete;
}
public List<User> getAllUser() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> users = sqlSession.selectList("getAllUser");
sqlSession.close();
return users;
}
}
然后,和这个 UserDao 对应的,还有一个 UserMapper.xml:
<?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">
<mapper namespace="mybatis01.mapper.UserMapper">
<select id="getUserById" resultType="mybatis01.model.User">
select * from user where id = #{id};
</select>
<insert id="addUser" parameterType="mybatis01.model.User">
insert into user(username,address) values (#{username},#{address});
</insert>
<insert id="addUser2" parameterType="mybatis01.model.User">
<selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
select uuid();
</selectKey>
insert into user(id,username,address) values (#{id},#{username},#{address})
</insert>
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id = #{id}
</delete>
<update id="updateUser" parameterType="mybatis01.model.User">
update user set username = #{username} where id=#{id}
</update>
<select id="getAllUser" resultType="mybatis01.model.User">
select * from user;
</select>
</mapper>
此时,我们分析这个 UserDao,发现它有很多可以优化的地方。每个方法中都要获取 SqlSession,涉及到增删改的方法,还需要 commit,SqlSession 用完之后,还需要关闭,sqlSession 执行时需要的参数就是方法的参数,sqlSession 要执行的 SQL ,和 XML 中的定义是一一对应的。这是一个模板化程度很高的代码。
既然模板化程度很高,我们就要去解决它,原理很简单,就是前面 Spring 中所说的动态代理。我们可以将当前方法简化成 一个接口:
public interface UserMapper {
User getUserById(Integer id);
Integer addUser(User user);
Integer addUser2(User user);
Integer deleteUserById(Integer id);
Integer updateUser(User user);
List<User> getAllUser();
}
这个接口对应的 Mapper 文件如下(注意,UserMapper.xml 和 UserMapper 需要放在同一个包下面):
<?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">
<mapper namespace="mybatis01.mapper.UserMapper">
<select id="getUserById" resultType="mybatis01.model.User">
select * from user where id = #{id};
</select>
<insert id="addUser" parameterType="mybatis01.model.User">
insert into user(username,address) values (#{username},#{address});
</insert>
<insert id="addUser2" parameterType="mybatis01.model.User">
<selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
select uuid();
</selectKey>
insert into user(id,username,address) values (#{id},#{username},#{address})
</insert>
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id = #{id}
</delete>
<update id="updateUser" parameterType="mybatis01.model.User">
update user set username = #{username} where id=#{id}
</update>
<select id="getAllUser" resultType="mybatis01.model.User">
select * from user;
</select>
</mapper>
使用这个接口,完全可以代替上面的 UserDao,为什么呢?因为这个接口提供了 UserDao 所需要的最核心的东西,根据这个接口,就可以自动生成 UserDao:
- 首先,UserDao 中定义了 SqlSessionFactory,这是一套固定的代码
- UserMapper 所在的包+UserMapper 类名+UserMapper 中定义好的方法名,就可以定位到要调用的 SQL
- 要调用 SqlSession 中的哪个方法,根据定位到的 SQL 节点就能确定。
因此,我们在 MyBatis 开发中,实际上不需要自己提供 UserDao 的实现,我们只需要提供一个 UserMapper 即可。
然后,我们在 MyBatis 的全局配置中,配置一下 UserMapper:
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="mybatis01/mapper"/>
</mappers>
</configuration>
然后,加载配置文件,获取 UserMapper,并调用它里边的方法:
public class Main {
public static void main(String[] args) throws Exception {
SqlSessionFactory sessionFactory = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = sessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> allUser = userMapper.getAllUser();
allUser.forEach(System.out::println);
//User user = new User();
//user.setUsername("马云");
// user.setAddress("杭州马");
// Integer integer = userMapper.addUser(user);
// sqlSession.commit();
}
}
mapper动态代理方式,在添加,修改、删除数据时,需要commit();
注意,在 Maven 中,默认情况下,Maven 要求我们将 XML 配置、properties 配置等,都放在 resources 目录下,如果我们强行放在 java 目录下,默认情况下,打包的时候这个配置文件会被自动忽略掉。对于这两个问题,我们有两种解决办法:
- 不要忽略 XML 配置:
我们可以在 pom.xml 中,添加如下配置,让 Maven 不要忽略我在 java 目录下的 XML 配置:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
- 按照 Maven 的要求来
按照 Maven 的要求来,将 xml 文件放到 resources 目录下,但是,MyBatis 中默认情况下要求,UserMapper.xml 和 UserMapper 接口,必须放在一起,所以,我们需要手动在 resources 目录下,创建一个和 UserMapper 接口相同的目录:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/0abca86cf2a32abf772db6509303a159.png#clientId=u5e8ef556-e901-4&from=paste&height=269&id=ud70c169b&margin=[object Object]&name=image.png&originHeight=538&originWidth=521&originalType=binary&ratio=1&size=36200&status=done&style=none&taskId=u5dfaff95-d8c8-44a2-a0e4-6d66f3fd9d0&width=260.5)
Mapper映射文件
mapper 映射文件,是 MyBatis 中最重要的部分,涉及到的细节也是非常非常多。
paramaterType
这个表示输入的参数类型。
$和#
这是一个非常非常高频的面试题,虽然很简单。在面试中,如果涉及到 MyBatis,一般情况下,都是这个问题。
在 MyBatis 中,我们在 mapper 引用变量时,默认使用的是 #,像下面这样:
<select id="getUserById" resultType="mybatis01.model.User">
select * from user where id = #{id};
</select>
除了使用 # 之外,我们也可以使用 $ 来引用一个变量:
<select id="getUserById" resultType="mybatis01.model.User">
select * from user where id = ¥{id};
</select>
在旧的 MyBatis 版本中,如果使用 $,变量需要通过 @Param 取别名,在最新的 MyBatis 中,无论是 # 还是 $,如果只有一个参数,可以不用取别名,如下:
public interface UserMapper {
User getUserById(Integer id);
}
既然 # 和 $ 符号都可以使用,那么他们有什么区别呢?
我们在 resources 目录下,添加 log4j.properties ,将 MyBatis 执行时的 SQL 打印出来:
log4j.rootLogger=DEBUG,stdout
log4j.logger.org.mybatis=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n
然后添加日志依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.5</version>
</dependency>
然后,我们可以分别观察 $ 和 # 执行时的日志:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/81e75b14e74a50835c3f13fb5f6844e1.png#clientId=u5e8ef556-e901-4&from=paste&height=212&id=u980e969c&margin=[object Object]&name=image.png&originHeight=423&originWidth=1867&originalType=binary&ratio=1&size=152874&status=done&style=none&taskId=u4db27592-91a1-4894-ac5d-c56b35b36d7&width=933.5)
上面这个日志,是 # 执行的日志,可以看到,这个日志中,使用了预编译的方式。
下面这个,是 $ 符号执行的日志,可以看到,SQL 直接就拼接好了,没有参数。
![image.png](https://www.icode9.com/i/ll/?i=img_convert/0de78bdedfa466222ec1b67d1a8cef29.png#clientId=u5e8ef556-e901-4&from=paste&height=219&id=u82e1f651&margin=[object Object]&name=image.png&originHeight=437&originWidth=1840&originalType=binary&ratio=1&size=149224&status=done&style=none&taskId=u6a44d6ad-1662-4ad1-b08c-ea33b6c99b4&width=920)
在 JDBC 调用中,SQL 的执行,我们可以通过字符串拼接的方式来解决参数的传递问题,也可以通过占位符的方式来解决参数的传递问题。当然,这种方式也传递到 MyBatis 中,在 MyBatis 中,$ 相当于是参数拼接的方式,而 # 则相当于是占位符的方式。
一般来说,由于参数拼接的方式存在 SQL 注入的风险,因此我们使用较少,但是在一些特殊的场景下,又不得不使用这种方式。
有的 SQL 拼接实际上可以通过数据库函数来解决,例如模糊查询:
<select id="getUserByName" resultType="mybatis01.model.User">
select * from user where username like concat("%",#{name},"%");
</select>
但是有的 SQL 无法使用 # 来拼接,例如传入一个动态字段进来,假设我想查询所有数据,要排序查询,但是排序的字段不确定,需要通过参数传入,这种场景就只能使用 $,例如如下方法:
<select id="getAllUser" resultType="mybatis01.model.User">
select * from user order by ${orderby} desc ;
</select>
简单类型
简单数据类型传递比较容易,像前面的根据 id 查询一条记录就算是这一类的。这里再举一个例子,比如根据 id 修改用户名:
Integer updateUsernameById(String username, Integer id);
再定义该方法对应的 mapper:
<update id="updateUsernameById">
update user set username=#{username} where id=#{id}
</update>
此时,如果直接调用该方法,会抛出异常:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/f0f7c3ca521c0cb4ac5a1af886ff8079.png#clientId=u5e8ef556-e901-4&from=paste&height=97&id=ucb8309f9&margin=[object Object]&name=image.png&originHeight=193&originWidth=1546&originalType=binary&ratio=1&size=42484&status=done&style=none&taskId=u56df984b-884e-408a-ac39-994b0ffdca1&width=773)
这里是说,找不到我们定义的 username 和 id 这两个参数。同时,这个错误提示中指明,可用的参数名是 [arg1, arg0, param1, param2],相当于我们自己给变量取的名字失效了,要使用系统提供的默认名字,默认名字实际上是两套体系:
第一套就是 arg0、arg1、、、、
第二套就是 param1、param2、、、
注意,这两个的下标是不一样的。
因此,按照错误提示,我们将参数改为下面这样:
<update id="updateUsernameById">
update user set username=#{arg0} where id=#{arg1}
</update>
或者下面这样:
<update id="updateUsernameById">
update user set username=#{param1} where id=#{param2}
</update>
这两种方式,都可以使该方法顺利执行。
但是,默认的名字不好记,容易出错,我们如果想要使用自己写的变量的名字,可以通过给参数添加 @Param 来指定参数名(一般在又多个参数的时候,需要加),一旦用 @Param 指定了参数类型之后,可以省略掉参数类型,就是在 xml 文件中,不用定义 parameterType 了:
Integer updateUsernameById(@Param("username") String username, @Param("id") Integer id);
这样定义之后,我们在 mapper.xml 文件中,就可以直接使用 username 和 id 来引用变量了。
对象参数
例如添加一个用户:
Integer addUser(User user);
对应的 mapper 文件如下:
<insert id="addUser" parameterType="mybatis01.model.User">
insert into user(username,address) values (#{username},#{address});
</insert>
我们在引用的时候,直接使用属性名就能够定位到对象了。如果对象存在多个,我们也需要给对象添加 @Param 注解,如果给对象添加了 @Param 注解,那么对象属性的引用,会有一些变化。如下:
Integer addUser(@Param("user") User user);
如果对象参数添加了 @Param 注解,Mapper 中的写法就会发生变化:
<insert id="addUser" parameterType="mybatis01.model.User">
insert into user(username,address) values (#{user.username},#{user.address});
</insert>
注意多了一个前缀,这个前缀不是变量名,而是 @Param 注解中定义名称。
resultType
resultType 是返回类型,在实际开发中,如果返回的数据类型比较复杂,一般我们使用 resultMap,但是,对于一些简单的返回,使用 resultType 就够用了。
resultType 返回的类型可以是简单类型,可以是对象,可以是集合,也可以是一个** hashmap**,如果是 hashmap,map 中的 key 就是字段名,value 就是字段的值。
输出 pojo 对象和输出 pojo 列表在 sql 中定义的 resultType 是一样的。
返回单个 pojo 对象要保证 sql 查询出来的结果集为单条,内部使用 sqlSession.selectOne 方法调用,mapper 接口使用 pojo 对象作为方法返回值。返回 pojo 列表表示查询出来的结果集可能为多条,内部使用 sqlSession.selectList 方法,mapper 接口使用 List 对象作为方法返回值。
resultMap
在实际开发中,resultMap 是使用较多的返回数据类型配置。因为实际项目中,一般的返回数据类型比较丰富,要么字段和属性对不上,要么是一对一、一对多的查询,等等,这些需求,单纯的使用 resultType 是无法满足的,因此我们还需要使用 resultMap,也就是自己定义映射的结果集。
先来看一个基本用法:
首先在 mapper.xml 中定义一个 resultMap:
<resultMap id="UserMap" type="mybatis01.model.User">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="address" property="address"/>
</resultMap>
在这个 resultMap 中,id 用来描述主键,column 是数据库查询出来的列名,property 则是对象中的属性名。
然后在查询结果中,定义返回值时使用这个 ResultMap:
<select id="getUserById" resultMap="UserMap">
select * from user where id = ${id};
</select>
注意,在旧版的 MyBatis 中,要求实体类一定要有一个无参构造方法,新版的 MyBatis 没有这个要求。
当然,我们也可以在 resultMap 中,自己指定要调用的构造方法,指定方式如下:
<resultMap id="UserMap2" type="mybatis01.model.User">
<constructor>
<idArg column="id" name="id"/>
<arg column="username" name="username"/>
</constructor>
</resultMap>
这个就表示使用两个参数的构造方法取构造一个 User 实例。注意,name 属性表示构造方法中的变量名,默认情况下,变量名是 arg0、arg1、、、、或者 param1、param2、、、,如果需要自定义,我们可以在构造方法中,手动加上 @Param 注解。
...省略...
public User(@Param("id") Integer id,@Param("username") String username) {
this.id = id;
this.username = username;
System.out.println("--------------------");
}
...省略...
动态SQL
动态 SQL 是 MyBatis 中非常强大的一个功能。例如一些常见的查询场景:
- 查询条件不确定
- 批量插入
- ….
这些类似需求,我们都可以通过 MyBatis 提供的动态 SQL 来解决。MyBatis 中提供的动态 SQL 节点非常多。
if
if 是一个判断节点,如果满足某个条件,节点中的 SQL 就会生效。例如分页查询,要传递两个参数,页码和查询的记录数,如果这两个参数都为 null,那我就查询所有。
我们首先来定义接口方法:
List<User> getUserByPage(@Param("page") Integer page,@Param("size") Integer size);
接口定义成功后,接下来在 XML 中定义 SQL:
<select id="getUserByPage" resultType="mybatis01.model.User">
select * from user
<if test="page !=null and size != null">
limit #{page},#{size}
</if>
</select>
if 节点中,test 表示判断条件,如果判断结果为 true,则 if 节点的中的 SQL 会生效,否则不会生效。也就是说,在方法调用时,如果分页的两个参数都为 null,则表示查询所有数据:
@Test()
public void getUserByPageTest(){
List<User> userByPage = userMapper.getUserByPage(1, 1);
userByPage.forEach(System.out::println);
List<User> userAll = userMapper.getUserByPage(null,null);
userAll.forEach(System.out::println);
}
where
where 用来处理查询参数。例如我存在下面一个查询函数:
List<User> getUsernameAndId(@Param("id") Integer id, @Param("username") String username);
这个查询的复杂之处在于:每个参数都是可选的,如果 id 为 null,则表示根据 name 查询,name 为 null,则表示根据 id 查询,两个都为 null,表示查询所有。
<select id="getUsernameAndId" resultType="mybatis01.model.User">
select * from user
<where>
<if test="id !=null">
and id > #{id}
</if>
<if test="username !=null">
and username like concat("%",#{username},"%")
</if>
</where>
</select>
用 where 节点将所有的查询条件包起来,如果有满足的条件,where 节点会自动加上,如果没有,where 节点也将不存在,在有满足条件的情况下,where 还会自动处理 and 关键字。
@Test()
public void getUserByUsernameAndIdTest() {
List<User> users1 = userMapper.getUsernameAndId(null, "腾");
List<User> users2 = userMapper.getUsernameAndId(8, null);
List<User> users3 = userMapper.getUsernameAndId(8, "东");
System.out.println(users1);
System.out.println(users2);
System.out.println(users3);
}
foreach
foreach 用来处理数组/集合参数。
例如,我们有一个批量查询的需求:
List<User> getUserByIds(@Param("ids") Integer[] ids);
对应的 XML 如下:
<select id="getUserByIds" resultType="mybatis01.model.User">
select * from user where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</select>
在 mapper 中,通过 foreach 节点来遍历数组,collection 表示数组变量,open 表示循环结束后,左边的符号,close 表示循环结束后,右边的符号,item 表示循环时候的单个变量,separator 表示循环的元素之间的分隔符。
注意,默认情况下,无论你的数组/集合参数名字是什么,在 XML 中访问的时候,都是 array,开发者可以通过 @Param 注解给参数重新指定名字。
例如我还有一个批量插入的需求:
Integer batchInsertUser(@Param("users") List<User> users);
然后,定义该方法对应的 mapper:
<insert id="batchInsertUser">
insert into user(username,address) values
<foreach collection="users" separator="," item="user">
(#{user.username},#{user.address})
</foreach>
</insert>
然后,在单元测试中进行测试:
public class MyBatisTest {
private SqlSession sqlSession;
private UserMapper userMapper;
@Before()
public void before() {
sqlSession = SqlSessionFactoryUtils.getInstance().openSession();
userMapper= sqlSession.getMapper(UserMapper.class);
}
@Test()
public void BachInsertUserTest(){
List<User> users = new ArrayList<>();
User user1= new User();
user1.setUsername("刘三哥");
user1.setAddress("北京");
users.add(user1);
User user2 = new User();
user2.setUsername("刘五");
user2.setAddress("广州");
users.add(user2);
Integer insertUser = userMapper.batchInsertUser(users);
sqlSession.commit();
System.out.println("影响行数:"+insertUser);
}
@After()
public void after() {
sqlSession.close();
}
}
foreach另外一个应用场景是修改,例如我有一个修改的需求:
我们先在mapper中定义一个SQL片段
Integer updateUser2(@Param("map")HashMap map, @Param("id") Integer id);
然后,在xml中编写,相对应的SQL语句
<update id="updateUser2">
update user
<set>
<foreach collection="map" index="key" item="val" separator=",">
${key} = #{val}
</foreach>
</set>
where id = #{id}
</update>
SQL片段
大家知道,在 SQL 查询中,一般不建议写 *,因为 select * 会降低查询效率。但是,每次查询都要把字段名列出来,太麻烦。这种使用,我们可以利用 SQL 片段来解决这个问题。
例如,我们先在 mapper 中定义一个 SQL 片段:
<sql id="Base_Colum">
id,username,address
</sql>
然后,在其他 SQL 中,就可以引用这个变量:
<select id="getUserById" resultMap="UserMap2">
select <include refid="Base_Colum" /> from user where id = ${id};
</select>
SET
set 关键字一般用在更新中。因为大部分情况下,更新的字段可能不确定,如果对象中存在该字段的值,就更新该字段,不存在,就不更新。例如如下方法:
Integer updateUser(User user);
现在,这个方法的需求是,根据用户 id 来跟新用户的其他属性,所以,user 对象中一定存在 id,其他属性则不确定,其他属性要是有值,就更新,没值(也就是为 null 的时候),则不处理该字段。
我们结合 set 节点,写出来的 sql 如下:
<update id="updateUser" parameterType="mybatis01.model.User">
update user
<set>
<if test="username !=null">
username=#{username},
</if>
<if test="address !=null">
address =#{address},
</if>
</set>
where id=#{id}
</update>
trlm
mybatis的trim标签一般用于去除sql语句中多余的and关键字,逗号,或者给sql语句前拼接 “where“、“set“以及“values(“ 等前缀,或者添加“)“等后缀,可用于选择性插入、更新、删除或者条件查询等操作。
以下是trim标签中涉及到的属性:
属性 | 描述 |
---|---|
prefix | 给sql语句拼接的前缀 |
suffix | 给sql语句拼接的后缀 |
prefixOverrides | 去除sql语句前面的关键字或者字符,该关键字或者字符由prefixOverrides属性指定,假设该属性指定为"AND",当sql语句的开头为"AND",trim标签将会去除该"AND" |
suffixOverrides | 去除sql语句后面的关键字或者字符,该关键字或者字符由suffixOverrides属性指定 |
例如,我们先在 mapper 中定义一个 SQL 片段:
List<User> getUserByUsernameAndAddress(@Param("username") String username, @Param("address") String address);
然后,定义该方法对应的 SQL:
<select id="getUserByUsernameAndAddress" resultType="mybatis01.model.User">
select <include refid="Base_Colum"></include> from user
<trim prefix="where" prefixOverrides="or ">
<if test="username !=null">
or username = #{username}
</if>
<if test="address !=null">
or address = #{address}
</if>
</trim>
</select>
bind
bind 标签可以使用 OGNL 表达式创建一个变量井将其绑定到上下文中 。我们现在有个需要根据用户名进行模糊查询。
我们先在 mapper 中定义一个 SQL 片段:
List<User> getUserLikeUsername(@Param("username") String username);
然后,定义该方法对应的 SQL:
<select id="getUserLikeUsername" resultType="mybatis01.model.User">
<bind name="usernameLike" value="username+'%'"/>
select <include refid="Base_Colum"></include>
from user
where username like #{usernameLike};
</select>
注意:其中username是mapper中定义的参数名称,usernameLike是bind中的名称。
查询结果高级映射
一对一查询
在实际开发中,经常会遇到一对一查询,一对多查询等。这里我们先来看一对一查询。
例如:每本书都有一个作者,作者都有自己的属性,根据这个,我来定义两个实体类:
package mybatis01.model;
import lombok.Data;
@Data
public class Book {
private Integer id;
private String name;
private Author author;
}
/*===========*/
package mybatis01.model;
import lombok.Data;
@Data
public class Author {
private Integer id;
private String name;
private Integer age;
}
并在数据库中新增book和author这两个表,同时插入一下测试数据。
![image.png](https://www.icode9.com/i/ll/?i=img_convert/83e4da3e5d1fda257439fe6d3d1e6eed.png#clientId=uee566232-bd76-4&from=paste&height=206&id=u0e67c536&margin=[object Object]&name=image.png&originHeight=318&originWidth=678&originalType=binary&ratio=1&size=15779&status=done&style=none&taskId=ufa44521d-1242-435a-bee4-8358a8d74e4&width=438.99737548828125)
添加成功后,我们新建一个 BookMapper接口:
package mybatis01.mapper;
import mybatis01.model.Book;
public interface BookMapper {
Book getBookById(Integer id);
}
BookMapper 中定义了一个查询 Book 的方法,但是我希望查出来 Book 的同时,也能查出来它的 Author。再定义一个 BookMapper.xml 。
在这个查询 SQL 中,首先应该做好一对一查询,然后,返回值一定要定义成 resultMap,注意,这里千万不能写错。然后,在 resultMap 中,来定义查询结果的映射关系。
其中,association 节点用来描述一对一的关系。这个节点中的内容,和 resultMap 一样,也是 id,result 等,在这个节点中,我们还可以继续描述一对一。
由于在实际项目中,每次返回的数据类型可能都会有差异,这就需要定义多个 resultMap,而这多个 resultMap 中,又有一部份属性是相同的,所以,我们可以将相同的部分抽出来,做成一个公共的模板,然后被其他 resultMap 继承,优化之后的 mapper 如下:
<?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">
<mapper namespace="mybatis01.mapper.BookMapper">
<resultMap id="BookeBaseMap" type="mybatis01.model.Book">
<id column="id" property="id"/>
<result column="name" property="name"/>
</resultMap>
<resultMap id="BookWithAuthor" type="mybatis01.model.Book" extends="BookeBaseMap">
<association property="author" javaType="mybatis01.model.Author"
resultMap="mybatis01.mapper.AuthorMapper.AuthorBaseMap"
columnPrefix="author_" >
</association>
</resultMap>
<select id="getBookById" resultMap="BookWithAuthor">
select
book.*,
author.id as author_id,
author.`name` as author_name,
author.age as author_age
from book, author
WHERE
book.aid=author.id
and book.id=#{id};
</select>
</mapper>
columnPrefix是定义前缀,这个是与查询语句的别名一一对应的。
association 节点用来描述一对一的关系,为了复用,我们在AuthorMapper定义了一个公共的AuthorBaseMap。内容如下:
<resultMap id="AuthorBaseMap" type="mybatis01.model.Author">
<id column="id" property="id"/>
<result column="name" property="name"></result>
<result column="age" property="age"></result>
</resultMap>
懒加载
上面这种加载方式,是一次性的读取到所有数据。然后在 resultMap 中做映射。如果一对一的属性使用不是很频繁,可能偶尔用一下,这种情况下,我们也可以启用懒加载。
懒加载,就是先查询 book,查询 book 的过程中,不去查询 author,当用户第一次调用了 book 中的 author 属性后,再去查询 author。
例如,我们再来定义一个 Book 的查询方法:
Book getBookById2(Integer id);
Author getAutorById(Integer id);
接下来,在 mapper 中定义相应的 SQL:
<resultMap id="BookeBaseMap2" type="mybatis01.model.Book" extends="BookBaseMap">
<association property="author" javaType="mybatis01.model.Author"
select="mybatis01.mapper.BookMapper.getAutorById" column="aid" fetchType="lazy"/>
</resultMap>
<select id="getBookById2" resultMap="BookeBaseMap2">
select * from book where id=#{id};
</select>
<select id="getAutorById" resultType="mybatis01.model.Author">
select * from author where id=#{aid};
</select>
这里,定义 association 的时候,不直接指定映射的字段,而是指定要执行的方法,通过 select 字段来指定,column 表示执行方法时传递的参数字段,最后的 fetchType 表示开启懒加载。
当然,要使用懒加载,还需在全局配置中开启:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
在单元测试验证懒加载的效果
![image.png](https://www.icode9.com/i/ll/?i=img_convert/40feffe95f1fc2ac75d7c1ec2c5fb2f5.png#clientId=uee566232-bd76-4&from=paste&height=449&id=u4b610930&margin=[object Object]&name=image.png&originHeight=897&originWidth=1539&originalType=binary&ratio=1&size=265675&status=done&style=none&taskId=u5fd81f26-f5fb-4342-baa1-d675908a048&width=769.5)
通过上图看到只有需要查询到作者的相关信息才会触发第二条查询作者的信息的SQL语句。
一对多查询
一对多查询,也是一个非常典型的使用场景。比如用户和角色的关系,一个用户可以具备多个角色。
首先我们准备三个表:
/*
Navicat MySQL Data Transfer
Source Server : localhost
Source Server Version : 50717
Source Host : localhost:3306
Source Database : security
Target Server Type : MYSQL
Target Server Version : 50717
File Encoding : 65001
Date: 2018-07-28 15:26:51
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`desc` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '2a10RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '2a10RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '2a10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
SET FOREIGN_KEY_CHECKS=1;
这三个表中,有用户表,角色表以及用户角色关联表,其中用户角色关联表用来描述用户和角色之间的关系,他们是一对多的关系。
这三个表中,有用户表,角色表以及用户角色关联表,其中用户角色关联表用来描述用户和角色之间的关系,他们是一对多的关系。
@Data
public class User {
private Integer id;
private String username;
private String password;
private String address;
private List<Role> roles;
}
@Data
public class Role {
private Integer id;
private String name;
private String desc;
}
接下来,定义一个根据 id 查询用户的方法:
User getUserById2(Integer id);
然后,定义该方法的实现:
<resultMap id="UserBaseMap" type="mybatis01.model.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="address" property="address"/>
<collection property="roles" ofType="mybatis01.model.Role"
resultMap="mybatis01.mapper.RoleMapper.RoleBaseMap" columnPrefix="role_"/>
</resultMap>
<select id="getUserById2" resultMap="UserBaseMap">
select u.id,u.username,u.address, r.id as role_id,r.name as role_name,r.desc as role_desc
from user u,role r,user_role ur
where u.id=ur.user_id and r.id=ur.role_id and u.id=#{id};
</select>
在 resultMap 中,通过 collection 节点来描述集合的映射关系。在映射时,会自动将一的一方数据集合并,然后将多的一方放到集合中,能实现这一点,靠的就是 id 属性。
当然,这个一对多,也可以做成懒加载的形式,那我们首先提供一个角色查询的方法:
User getUserById3(Integer id);
List<Role> getRoleByUid(Integer id);
然后,在 XML 文件中,处理懒加载:
<resultMap id="UserWithRole2" type="mybatis01.model.User" extends="UserBaseMap">
<collection property="roles" select="mybatis01.mapper.UserMapper.getRoleByUid" column="id" fetchType="lazy"/>
</resultMap>
<select id="getUserById3" resultMap="UserWithRole2">
select * from user where id=#{id}
</select>
<select id="getRoleByUid" resultMap="mybatis01.mapper.RoleMapper.RoleBaseMap">
select r.* from role r,user_role ur where r.id=ur.role_id and ur.user_id=#{id}
</select>
定义完成之后,我们在单元测试中实现了懒加载功能。
![image.png](https://www.icode9.com/i/ll/?i=img_convert/96d13e77154fb72b45388b1af30e9959.png#clientId=uee566232-bd76-4&from=paste&height=415&id=ub0647cd1&margin=[object Object]&name=image.png&originHeight=829&originWidth=1598&originalType=binary&ratio=1&size=254398&status=done&style=none&taskId=u36154720-4019-4b42-a395-70a5f3448c5&width=799)
鉴别映射器
有时候一个单独的数据库查询也会返回很多不同数据类型的结果集。例如一个表存储了单选题和多选题,查询是需要返回担心题对象和对象题对象。鉴别器就是用来处理这种情况的。鉴别器元素很像java语言中的switch分支语句结构。
有这样一个需求,当用户表的enabled字段为1时显示拥有的角色,当为0时不显示。
在UserMapper新建一个接口
List<User> getAllUsersWithRole();
然后,在 XML 文件中编写SQL
<resultMap id="UserBaseMap" type="mybatis01.model.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="address" property="address"/>
<result column="enabled" property="enabled"/>
</resultMap>
<resultMap id="UserWithRole" type="mybatis01.model.User" extends="UserBaseMap">
<collection property="roles" ofType="mybatis01.model.Role"
resultMap="mybatis01.mapper.RoleMapper.RoleBaseMap" columnPrefix="role_"/>
</resultMap>
<resultMap id="UserWithRole3" type="mybatis01.model.User">
<discriminator javaType="int" column="enabled">
<case value="1" resultMap="UserWithRole"></case>
<case value="0" resultMap="UserBaseMap"></case>
</discriminator>
</resultMap>
<select id="getAllUsersWithRole" resultMap="UserWithRole3">
select u.id,u.username,u.address,u.enabled, r.id as role_id,r.name as role_name,r.desc as role_desc
from user u,role r,user_role ur
where u.id=ur.user_id and r.id=ur.role_id
</select>
在单元测试中验证:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/4b120acfebd82748f3d2f3c4bb50c9d1.png#clientId=u0c2a23b7-9adf-4&from=paste&height=425&id=u99f43636&margin=[object Object]&name=image.png&originHeight=850&originWidth=1571&originalType=binary&ratio=1&size=241329&status=done&style=none&taskId=u33c00de7-b990-4c60-90af-6cfff6fdcd3&width=785.5)
自定义类型转换器
为什么要做自定义类型转换器。就是为了解决 java 类中的属性和数据库表字段属性类型不一致的情况。例如
![image.png](https://www.icode9.com/i/ll/?i=img_convert/4bfecfdaf0b93cc5ac05eb9b3cb2528f.png#clientId=u0c2a23b7-9adf-4&from=paste&id=u96d7e1fa&margin=[object Object]&name=image.png&originHeight=338&originWidth=557&originalType=url&ratio=1&size=36711&status=done&style=none&taskId=ud1b0c2af-bf2c-4017-a056-0632e3694eb)
我们可以看到 id、name、age的属性表与实体类都是一一对应的,但是 sex确不对应,此时如果我们不进行类型转换,那么向表 person 插入数据的时候会报错,查询的时候性别 sex 会显示空值(NULL),为了解决类似这种情形,我们需要自定义类型转换器 。
现在我们在用户表中添加一个sex字段,类型为int。User实体类型添加一个sex属性。如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String address;
private Boolean enabled;
private String sex;
private List<Role> roles;
}
建立转换器类,叫其继承 BaseTypeHandler(当然也可以叫其实现 TypeHandler 接口,BaseTypeHandler 为 TypeHandler 接口的实现类),具体实现代码如下:
package mybatis01.utils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author didiplus
* @version 1.0.0
* @ClassName StringToInt.java
* @Description 建立一个转换器,将String 转换为 int
* @createTime 2021年11月12日 11:03:00
*/
public class StringToInt extends BaseTypeHandler {
/* 该方法实现:通过 java类向数据库中设置(插入数据)
参数含义:第一个为 JDBC 预处理对象,第二个为操作的位置,第三个为 javaType,第四个为 jdbcTpye*/
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) throws SQLException {
if (o.equals("男")) {
preparedStatement.setInt(i,1);
}else {
preparedStatement.setInt(i,0);
}
}
/*该方法实现通过列名获取数据库表中的数据*/
@Override
public Object getNullableResult(ResultSet resultSet, String s) throws SQLException {
int sex = resultSet.getInt(s);
return sex == 1 ? "男":"女";
}
/*该方法实现通过列标获取数据库表中的数据*/
@Override
public Object getNullableResult(ResultSet resultSet, int i) throws SQLException {
int sex = resultSet.getInt(i);
return sex == 1 ? "男":"女";
}
/*该方法实现通过存储过程获取数据库表中的数据*/
@Override
public Object getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
int sex = callableStatement.getInt(i);
return sex == 1 ? "男":"女";
}
}
在 mybatis-config.xml 中配置自定义的转换器
<!--配置类型转换器-->
<typeHandlers>
<typeHandler handler="com.convert.StringToInt" javaType="String" jdbcType="INTEGER"></typeHandler>
</typeHandlers>
配置实体类 User的映射文件 UserMapper.xml,具体配置内容如下:
<!--带转换器形式的插入一个人员信息-->
<insert id="AddUser" parameterType="mybatis01.model.User">
insert into user (username,password,sex,address)
values (#{username},#{password},#{sex,javaType=String,jdbcType=INTEGER},#{address})
</insert>
<resultMap id="UserMap" type="mybatis01.model.User">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="address" property="address"/>
<result column="sex" property="sex" javaType="String" jdbcType="INTEGER"/>
</resultMap>
<select id="getUserById" resultMap="UserMap">
select * from user where id = ${id};
</select>
主键回填的两种实现方式
主键回填其实是一个非常常见的需求,特别是在数据添加的过程中,我们经常需要添加完数据之后,需要获取刚刚添加的数据 id,无论是 Jdbc 还是各种各样的数据库框架都对此提供了相关的支持,本文我就来和和大家分享下数据库主键回填在 MyBatis 中的两种实现思路。
原生写法
框架来源于我们学过的基础知识,主键回填实际上是一个在 JDBC 中就被支持的写法,有的小伙伴可能不知道这一点,因此这里我先来说说在 JDBC 中如何实现主键回填。
JDBC 中实现主键回填其实非常容易,主要是在构造 PreparedStatement 时指定需要主键回填,然后在插入成功后,查询刚刚插入数据的 id ,示例代码如下:
public int insert(Person person) {
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
con = DBUtils.getConnection();
ps = con.prepareStatement("INSERT INTO person(username,password,money) VALUES(?,?,?)", PreparedStatement.RETURN_GENERATED_KEYS);
ps.setObject(1, person.getUsername());
ps.setObject(2, person.getPassword());
ps.setObject(3, person.getMoney());
int i = ps.executeUpdate();
rs = ps.getGeneratedKeys();
int id = -1;
if (rs.next()) {
id = rs.getInt(1);
}
return id;
}
和普通的插入 SQL 不同之处主要体现在两个地方:
- 第一个是构造 PreparedStatement 时,多了一个参数,指定了需要主键回填。
- 在更新操作执行完成之后,调用 getGeneratedKeys ,然后又会获取到一个 ResultSet 对象,从这个游标集中就可以获取到刚刚插入数据的id。
这个是原生的写法,在 MyBatis 中,对此需求提供了两种不同的实现方案,下面分别来看。
框架写法
一般情况下,主键有两种生成方式:
- 主键自增长
- 自定义主键(一般可以使用UUID,或者类UUID)
如果是第二种,主键一般是在Java代码中生成,然后传入数据库执行插入操作,如果是第一个主键自增长,此时,Java 可能需要知道数据添加成功后的主键。
MyBatis 的基本用法就无需多说了,这也不是本文的重点,我们还是来看看 MyBatis 中主键回填的两种不同实现方式吧!
方式一
第一种方式比较简单,推荐的一种实现方式:
<insert id="insertBook" useGeneratedKeys="true" keyProperty="id">
insert into t_book (b_name,author) values (#{name},#{author});
</insert>
这种方式比较简单,就是在插入节点上添加 useGeneratedKeys 属性,同时设置接收回传主键的属性。配置完成后,我们执行一个插入操作,插入时传入一个对象,插入完成后,这个对象的 id 就会被自动赋值,值就是刚刚插入成功的id。使用这种方式,原因很简单,这种方式实现简便省事。
方式二
第二种方式则是利用MySQL自带的 last_insert_id() 函数查询刚刚插入的id,示例代码如下:
<insert id="insertBook">
<selectKey keyProperty="id" resultType="java.lang.Integer">
SELECT LAST_INSERT_ID()
</selectKey>
insert into t_book (b_name,author) values (#{name},#{author});
</insert>
这种方式是在 insert 节点中添加 selectKey 来实现主键回填,实际上这种方式的功能更加丰富,因为 selectKey 节点中的 SQL 我们既可以在插入之前执行,也可以在插入之后执行(通过设置节点的 Order 属性为 AFTER 或者 BEFORE 可以实现),具体什么时候执行,还是要看具体的需求,如果是做主键回填,我们当然需要在插入 SQL 执行之后执行 selectKey 节点中的 SQL。
注意第二种方式一样也要通过设置 keyProperty 来指定将查询到的数据绑定到哪个属性上。
MyBatis缓存
一级缓存
Mybatis 一级缓存的作用域是同一个 SqlSession,在同一个 sqlSession 中两次执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个 sqlSession 结束后该 sqlSession 中的一级缓存也就不存在了。Mybatis 默认开启一级缓存。
多次查询,只执行一次 SQL。但是注意,如果开启了一个新的 SqlSession,则新的 SqlSession 无法就是之前的缓存,必须是同一个 SqlSession 中,缓存才有效。
二级缓存
Mybatis 二级缓存是多个 SqlSession 共享的,其作用域是 mapper 的同一个 namespace,不同的 sqlSession 两次执行相同 namespace 下的 sql 语句且向 sql 中传递参数也相同即最终执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis 默认没有开启二级缓存需要在 setting 全局参数中配置开启二级缓存。
二级缓存开启方法
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
在xml文件添加cace标签
<?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">
<mapper namespace="mybatis01.mapper.AuthorMapper">
<cache/>
<resultMap id="AuthorBaseMap" type="mybatis01.model.Author">
<id column="id" property="id"/>
<result column="name" property="name"></result>
<result column="age" property="age"></result>
</resultMap>
</mapper>
转载 江南一点雨