MyBatis核心

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中


版权声明:本文为cylcjsg原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。