MyBatis框架使用回顾
数据库新建表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
新建 Maven 项目和设置编译版本及添加依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope><!-- 测试阶段才用 -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope><!-- 编译阶段和测试阶段才使用 -->
</dependency>
</dependencies>
配置文件
在 resources 目录下添加下面这些配置文件。
db.properties
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatisdemo
username=root
password=admin
mybatis-config.xml
<?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>
<properties resource="db.properties"/>
<!-- 类型别名配置 -->
<typeAliases>
<!-- 包名范围不要太大,一般到 domain,在没有注解的情况下,会使用实体类的首字母小写的非限定类名来作为它的别名 -->
<package name="cn.xxx.domain"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<!-- 关联 Mapper 文件 -->
<mappers>
<mapper resource="cn/xxx/mapper/UserMapper.xml" />
</mappers>
</configuration>
log4j.properties
log4j.rootLogger=ERROR, stdout
log4j.logger.cn.xxx=TRACE
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
编写实体类
@Setter
@Getter
@ToString
public class User {
private Long id;
private String username;
private String password;
}
编写 UserMapper.xml
在 resources 目录下新建 cn/xxx/mapper/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="cn.xxx.mapper.UserMapper">
<insert id="save" keyColumn="id" keyProperty="id" useGeneratedKeys="true">
INSERT INTO user(username, password) VALUES(#{username}, #{password})
</insert>
<update id="update">
UPDATE user SET
username = #{username},
password = #{password}
WHERE id = #{id}
</update>
<select id="get" resultType="User">
SELECT id, username, password FROM user WHERE id = #{id}
</select>
<delete id="delete">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
编写 MyBatisUtil
public abstract class MyBatisUtil {
private static SqlSessionFactory sqlSessionFactory;
static {
InputStream inputStream;
try {
inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSession() {
return sqlSessionFactory.openSession();
}
}
编写单元测试类
public class UserMapperTest {
@Test
public void testSave() throws Exception {
User user = new User();
user.setUsername("哈哈");
user.setPassword("123456");
SqlSession session = MyBatisUtil.getSqlSession();
session.insert("cn.xxx.mapper.UserMapper.save", user);
session.commit();
session.close();
System.out.println(user);
}
@Test
public void testGet() throws Exception {
SqlSession session = MyBatisUtil.getSqlSession();
User user = (User)session.selectOne("cn.xxx.mapper.UserMapper.get", 1L);
session.close();
System.out.println(user);
}
}
之前代码问题
@Test
public void testGet() throws Exception {
SqlSession session = MyBatisUtil.getSqlSession();
User user = (User)session.selectOne("cn.xxx.mapper.UserMapper.get", 1L);
session.close();
System.out.println(user);
}
- 映射文件xml的命名空间id 使用的是 String 类型(第一个参数),一旦编写错误,只有等到运行代码才能报错;
- 传入的实际参数类型不被检查,因为第二个参数类型是 Object。
使用 Mapper 接口
类似之前的 DAO,在接口中定义 CRUD 等操作方法。Mapper 组件现在就变成了 ==> Mapper 接口 + Mapper XML 文件。
- 接口的命名为实体名Mapper,一般和其对应 XML 文件放一起(还是放资源目录下同文件夹,只要编译之后字节码文件和 XML 文件在一起);
- XML 命名空间用其对应接口的全限定名;
- Mapper 接口的方法名要和 Mapper XML 文件元素(select | update | delete | insert) id 值一样;
- 方法的返回类型对应 SQL 语句中定义的 resultType / resultMap 类型;
- 方法的参数类型对应 SQL 语句中定义的 paramterType 类型(一般不写)。
定义 Mapper 接口
public interface UserMapper {
User get(Long id);
}
使用 Mapper 接口对象
public void testGet() throws Exception{
SqlSession session = MyBatisUtil.getSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.get(1L);
System.out.println(user);
session.close();
}
Mapper 接口实现的原理
通过打印接接口,发现打印的是:class com.sun.proxy.$Proxy5,底层使用的是动态代理(后面 Spring 再讲),生成 Mapper 接口的实现类。
接口是规范,实质做的实现还是要由实现类对象来做,而这个实现类不需要我们写,实现类对象也不由我们创建,这些都 MyBatis 使用动态代理帮我们做了;
我们只需提供 Mapper 接口与对应 Mapper XML 文件,获取实现类对象的时候传入Mapper 接口对象就可以了(不然 MyBatis 也不知道你要获取哪个 Mapper 接口的实现类对象);
至于实现类中操作方式底层还是和之前的一样,因为 Mapper XML 命名空间是使用 Mapper 接口的全限定名,方法名又与对应 XML 元素 id 一致,所以可以通过获取调用方法所在 Mapper 接口的全限定包名和方法名,拼接出 namespace + id,再配合调用方法的实参就可以像之前一样操作了。
MyBatis 的参数处理
实现一个登录需求,本质就是调用 Mapper 接口中的方法根据用户名和密码查询用户。
mapper接口
User login(String username, String password);
xml映射文件
<select id="login" resultType="User">
SELECT id, username, password FROM user
WHERE username = #{username} AND password = #{password}
</select>
发现调用时会抛出异常,原因本质接口的底层还是原来之之前的方法,就只支持一个参数的。
封装成一个参数解决方式
修改 UserMapper 接口及 UserMapper.xml。
接口
User login1(Map<String, Object> param);//map方式
User login2(User param); //javaBean方式
xml
<select id="login1" resultType="User">
SELECT id, username, password FROM user
WHERE username = #{username} AND password = #{password}
</select>
<select id="login2" resultType="User">
SELECT id, username, password FROM user
WHERE username = #{username} AND password = #{password}
</select>
使用 @Param 注解解决方式
修改 UserMapper 接口中的 login 方法,在形参贴注解即可。
// 本质相当于构建一个 Map 对象,key 为注解 @Param 的值,value 为参数的值。
User login(@Param("username")String username, @Param("password")String password);
集合/数组参数
当传递一个 List 对象或数组对象参数给 MyBatis 时,MyBatis 会自动把它包装到一个 Map 中,此时:List 对象会以 list 作为 key,数组对象会以 array 作为 key,也可以使用注解 @Param 设置 key 名。
MyBatis 的 # 和 $取值方式
相同点
都可以获取对象(Map 对象或者 JavaBean 对象)的信息。
不同点
- 使用 # 传递的参数会先转换为字符串,无论传递是什么类型数据都会带一个单引号;使用 $ 传递的参数,直接把值作为 SQL 语句的一部分。
- 使用 # 支持把简单类型(八大基本数据类型及其包装类、String、BigDecimal 等等)参数作为值,根据不同类型,取值方式不一样;使用 $ 不支持把简单类型参数作为值,对任何类型的对象都当做map对象来取其中的值
- 使用 # 好比使用 PrepareStatement,没有 SQL 注入的问题,相对比较安全;使用 $ 好比使用 Statement,可能会有 SQL 注入的问题,相对不安全。
代码证明
// #
User queryByUsername1(String username);
// $
User queryByUsername2(@Param("username")String name);
// #
List<User> orderByDesc1(String columnName);
// $
List<User> orderByDesc2(@Param("columnName")String columnName);
<select id="queryByUsername1" resultType="User">
SELECT id, username, password FROM user WHERE username = #{username}
</select>
<select id="queryByUsername2" resultType="User">
SELECT id, username, password FROM user WHERE username = ${username}
</select>
<select id="orderByDesc1" resultType="User">
SELECT id, username, password FROM user ORDER BY #{columnName} DESC
</select>
<select id="orderByDesc2" resultType="User">
SELECT id, username, password FROM user ORDER BY ${columnName} DESC
</select>
$取值方式应用
若需要作为 ORDER BY 或 GROUP BY 子句获取参数值使用 $取值,这样不会加引号导致排序失败
动态 SQL
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
动态 SQL 之 if 和 where
查看之前的文章
动态 SQL 之 set
set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号,若里面条件都不成立,就会去除 SET 关键字。其用来解决更新时丢失数据的问题。
**需求:**给某个员工加工资。
错误代码实现
<update id="update">
UPDATE employee SET
name = #{name},
sn = #{sn},
salary = #{salary}
WHERE id = #{id}
</update>
存在问题:传入的对象某个属性值为 null,则会造成更新丢失数据。
使用if动态标签
<update id="update">
UPDATE employee SET
<if test="name != null">
name = #{name},
</if>
<if test="sn != null">
sn = #{sn},
</if>
<if test="salary != null">
salary = #{salary}
</if>
WHERE id = #{id}
</update>
存在问题:虽然可以解决更新丢失数据的问题,但会造成多逗号或者少逗号的问题(比如就仅第一个条件成立)。
使用 set 标签实现
<update id="update">
UPDATE employee
<set>
<if test="name != null">
name = #{name},
</if>
<if test="sn != null">
sn = #{sn},
</if>
<if test="salary != null">
salary = #{salary},
</if>
</set>
WHERE id = #{id}
</update>
动态 SQL 之 foreach
动态 SQL 的另外一个常用的操作需求是对一个集合或数组进行遍历,通常是在构建 IN 条件语句的时候,这里就会使用到 foreach 元素。
需求: 批量地根据员工 id 删除员工。
代码实现
修改 EmployeeMapper 接口及 EmployeeMapper.xml
void batchDelete(@Param("ids")Long[] ids);
<delete id="batchDelete">
DELETE FROM employee WHERE id IN
<!--
collection 遍历数组或集合的 key 或者属性名
open 遍历开始拼接的字符串
index 遍历索引
item 遍历元素
separator 每遍历元素拼接字符串
close 遍历结束拼接字符串
-->
<foreach collection="ids" open="(" item="id" separator="," close=")">
#{id}
</foreach>
</delete>
关系概述
生活中数据很多是存在关系的,就是把生活中有关系的数据通过 MyBatis 持久化到数据库,且存储的数据也能表示出来这种关系,再由数据库中把有关系的数据查询出来在页面展示。
- 保存:页面的数据 —> 使用 Java 对象封装 —> 通过 MyBatis —> 数据库表的数据
- 查询:数据库表的数据 —> 通过 MyBatis —> 封装成 Java 对象 —> 页面展示数据
那么这里需要解决问题:
- 怎么使用数据库表设计来表示数据之间关系;
- 怎么使用 Java 类设计来表示对象之间关系;
- 怎么通过 MyBatis 配置来映射上面两者(翻译)。
对象关系分类
- 泛化关系
- 实现关系
- 依赖关系
- 关联关系
- 聚合关系
- 组合关系
关联关系
A 对象依赖 B 对象,并且把 B 作为 A 的一个成员变量,则 A 和 B 存在关联关系。
关联关系分类
按照导航性分
若通过 A 对象中的某一个属性可以访问到 B 对象,则说 A 可以导航到 B。
- 单向:只能从 A 通过属性导航到 B,B 不能导航到 A。
- 双向:A 可以通过属性导航到 B,B 也可以通过属性导航到 A。
按照多重性分
- 一对一
- 一对多
- 多对一
- 多对多。
判断对象的关系
- 判断都是从对象的实例上面来看的;
- 判断关系需要根据对象的属性;
- 判断关系必须确定具体需求。
单向多对一的设计
表设计
外键一般设计在多方,遵循数据库范式,一列只有一个数据
类设计
@Setter
@Getter
@ToString
public class Department {
private Long id;
private String name;
}
@Setter
@Getter
@ToString
public class Employee {
private Long id;
private String name;
// 关联属性
private Department dept;
}
resultMap属性的使用
在表的列名,或者给这个列查询时起了别名时,会在封装成对象时找不到对应的属性,这时使用resultMap属性指定哪个列封装到对象中的哪个属性上
<select id="get" resultMap="baseResultMap">
SELECT id, name, dept_id FROM employee WHERE id = #{id}
</select>
<resultMap type="Employee" id="baseResultMap">
<!-- 什么列名对应值封装到对象的什么属性上 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="dept_id" property="dept.id"/>
</resultMap>
使用 association标签发送额外 SQL
<resultMap type="Employee" id="baseResultMap">
<!-- 什么列名对应值封装到对象的什么属性上 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- 使用额外 SQL
association 针对的关联属性配置,非集合类型
若关联属性是集合类型,使用 collection 标签来配置
select 发送什么额外 SQL,命名空间+id
column 发送额外 SQL 所需要的参数,从使用这个resultMap的sql中的select查询的列进行选择
property 封装到这个resultMap的封装类型type对象的什么属性上
-->
<association select="cn.xxx.mapper.DepartmentMapper.get"
column="dept_id" property="dept" javaType="Department"/>
<!--额外 SQL 方式可以不配置 javaType 属性(因为这条额外get,sql语句中已配置了封装类型)-->
</resultMap>
单向多对一,额外 SQL 查询 N+1 问题
需求:查询所有员工及其对应部门。假设在 employee 表中有 N 条数据,每一个员工都关联着一个不同的部门 id。当在查询所有员工时,就会发送 N + 1条语句
使用多表查询,此时一条 SQL 语句搞定,实现查询所有员工及其对应部门。
<select id="listAll" resultMap="multiTableResultMap">
SELECT e.id, e.name, d.id AS d_id, d.name AS d_name
FROM employee e JOIN department d ON e.dept_id = d.id
</select>
发 N + 1条额外 sql 问题没有,但如何解决结果集映射问题呢(查询出来的列名与属性名不一致)
结果集映射方式 1
<resultMap type="Employee" id="multiTableResultMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="d_id" property="dept.id"/>
<result column="d_name" property="dept.name"/>
</resultMap>
结果集映射方式 2
<resultMap type="Employee" id="multiTableResultMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="dept" javaType="Department">
<result column="d_id" property="id"/>
<result column="d_name" property="name"/>
</association>
</resultMap>
结果集映射方式 3
问题:当关联表查询的列太多了,那么前缀 d_ 就要写很多。
<resultMap type="Employee" id="multiTableResultMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association columnPrefix="d_" property="dept" javaType="Department">
<result column="id" property="id"/>
<result column="name" property="name"/>
</association>
</resultMap>
单向一对多的设计
表设计
和多对一表设计是一样的。
类设计
@Setter
@Getter
@ToString
public class Employee {
private Long id;
private String name;
}
@Setter
@Getter
@ToString
public class Department {
private Long id;
private String name;
// 关联属性,建议集合对象直接 new 出来,避免后面写测试类的时候造成空指针。
private List<Employee> employees = new ArrayList<>();
}
单向一对多存在的问题
问题是保存到员工表中的员工数据没有部门 id
解决的办法:
- 发送额外 SQL 修改员工的部门(性能较低不推荐);
- 改成双向的关联关系;
- 在 many 放添加一个 Long 类型的 deptId,在保存部门之后把部门的 id 值设置到员工对象这个 deptId 属性再保存员工。
修改员工实体类
@Setter
@Getter
@ToString
public class Employee {
private Long id;
private String name;
private Long deptId; // 这个属性用来封装这个员工的部门 id 值,不是关联属性
}
修改员工表的保存sql,在保存的时候把部门id存上
<insert id="save" useGeneratedKeys="true" keyProperty="id">
INSERT INTO employee(name, dept_id) VALUES(#{name}, #{deptId})
</insert>
使用 collection 标签发送额外 SQL
<select id="get" resultMap="baseResultMap">
SELECT id, name FROM department WHERE id = #{id}
</select>
<resultMap type="Department" id="baseResultMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!--
若关联属性是集合类型,使用 collection 标签来配置
若关联属性是非集合类型 association 标签来配置
select 发送什么额外 SQL,命名空间+id
column 发送额外 SQL 所需要的参数,从使用这个resultMap的sql中的select查询的列进行选择
property 查询结果封装到这个resultMap的封装类型type对象的什么属性上
-->
<collection select="cn.xxx.mapper.EmployeeMapper.queryByDeptId"
column="id" property="employees"/>
</resultMap>
单向多对多的设计
表设计

类设计
@Setter
@Getter
@ToString
public class Teacher {
private Long id;
private String name;
}
@Setter
@Getter
@ToString
public class Student {
private Long id;
private String name;
// 关联属性
private List<Teacher> teachers = new ArrayList<>();
}
其他
中间表的增删改查的sql一般写在有关联属性方的映射xml中