第三章 MyBatis
二、MyBatis 基本使用
4. CRUD 强化练习
4.1 准备数据库数据
- 首先,我们需要准备一张名为 user 的表。该表包含字段 id(主键)、username、password。创建SQL如下:
CREATE TABLE `user` (`id` INT(11) NOT NULL AUTO_INCREMENT,`username` VARCHAR(50) NOT NULL,`password` VARCHAR(50) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
4.2 实体类准备
- 接下来,我们需要定义一个实体类 User,来对应 user 表的一行数据。
@Data //lombok
public class User {private Integer id;private String username;private String password;
}
4.3 Mapper接口定义
- 定义一个 Mapper 接口 UserMapper,并在其中添加 user 表的增、删、改、查方法。
public interface UserMapper {int insert(User user);int update(User user);int delete(Integer id);User selectById(Integer id);List<User> selectAll();
}
4.4 MapperXML编写
- 在 resources /mappers目录下创建一个名为
UserMapper.xml
的 XML 文件,包含与 Mapper 接口中相同的五个 SQL 语句,并在其中,将查询结果映射到User
实体中。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace等于mapper接口类的全限定名,这样实现对应 -->
<mapper namespace="com.alex.mapper.UserMapper"><!-- 定义一个插入语句,并获取主键值 --><insert id="insert" useGeneratedKeys="true" keyProperty="id">INSERT INTO user(username, password)VALUES(#{username}, #{password})</insert><update id="update">UPDATE user SET username=#{username}, password=#{password}WHERE id=#{id}</update><delete id="delete">DELETE FROM user WHERE id=#{id}</delete><!-- resultType使用user别名,稍后需要配置!--><select id="selectById" resultType="user">SELECT id, username, password FROM user WHERE id=#{id}</select><!-- resultType返回值类型为集合,所以只写范型即可! --><select id="selectAll" resultType="user">SELECT id, username, password FROM user</select></mapper>
4.5 MyBatis配置文件
- 位置:resources: mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><settings><!-- 开启驼峰式映射--><setting name="mapUnderscoreToCamelCase" value="true"/><!-- 开启logback日志输出--><setting name="logImpl" value="SLF4J"/></settings><typeAliases><!-- 给实体类起别名 --><package name="com.alex.pojo"/></typeAliases><!-- environments表示配置Mybatis的开发环境,可以配置多个环境,在众多具体环境中,使用default属性指定实际运行时使用的环境。default属性的取值是environment标签的id属性的值。 --><environments default="development"><!-- environment表示配置Mybatis的一个具体的环境 --><environment id="development"><!-- Mybatis的内置的事务管理器 --><transactionManager type="JDBC"/><!-- 配置数据源 --><dataSource type="POOLED"><!-- 建立数据库连接的具体信息 --><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/mybatis-example"/><property name="username" value="root"/><property name="password" value="root"/></dataSource></environment></environments><mappers><!-- Mapper注册:指定Mybatis映射文件的具体位置 --><!-- mapper标签:配置一个具体的Mapper映射文件 --><!-- resource属性:指定Mapper映射文件的实际存储位置,这里需要使用一个以类路径根目录为基准的相对路径 --><!-- 对Maven工程的目录结构来说,resources目录下的内容会直接放入类路径,所以这里我们可以以resources目录为基准 --><mapper resource="mappers/UserMapper.xml"/></mappers></configuration>
4.6 效果测试
package com.alex.test;import com.alex.mapper.UserMapper;
import com.alex.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;import java.io.IOException;
import java.util.List;/*** projectName: com.alex.test*/
public class MyBatisTest {private SqlSession session;// junit会在每一个@Test方法前执行@BeforeEach方法@BeforeEachpublic void init() throws IOException {session = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")).openSession();}@Testpublic void createTest() {User user = new User();user.setUsername("admin");user.setPassword("123456");UserMapper userMapper = session.getMapper(UserMapper.class);userMapper.insert(user);System.out.println(user);}@Testpublic void updateTest() {UserMapper userMapper = session.getMapper(UserMapper.class);User user = userMapper.selectById(1);user.setUsername("root");user.setPassword("111111");userMapper.update(user);user = userMapper.selectById(1);System.out.println(user);}@Testpublic void deleteTest() {UserMapper userMapper = session.getMapper(UserMapper.class);userMapper.delete(1);User user = userMapper.selectById(1);System.out.println("user = " + user);}@Testpublic void selectByIdTest() {UserMapper userMapper = session.getMapper(UserMapper.class);User user = userMapper.selectById(1);System.out.println("user = " + user);}@Testpublic void selectAllTest() {UserMapper userMapper = session.getMapper(UserMapper.class);List<User> userList = userMapper.selectAll();System.out.println("userList = " + userList);}// junit会在每一个@Test方法后执行@@AfterEach方法@AfterEachpublic void clear() {session.commit();session.close();}
}
5. mapperXML 标签总结
-
MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。
-
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
insert
– 映射插入语句。update
– 映射更新语句。delete
– 映射删除语句。select
– 映射查询语句。
5.1 select 标签
- MyBatis 在查询和结果映射做了相当多的改进。一个简单查询的 select 元素是非常简单:
<select id="selectPerson"
resultType="hashmap" resultMap="自定义结构"> SELECT * FROM PERSON WHERE ID = #{id} </select>
- 这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。
- 注意参数符号:#{id} ${key}
- MyBatis 创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:
// 近似的 JDBC 代码,非 MyBatis 代码...
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
- select 元素允许你配置很多属性来配置每条语句的行为细节:
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
resultType | 期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。 |
resultMap | 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
5.2 insert, update 和 delete 标签
- 数据变更语句 insert,update 和 delete 的实现非常接近:
<insertid="insertAuthor"statementType="PREPARED"keyProperty=""keyColumn=""useGeneratedKeys=""timeout="20"><updateid="updateAuthor"statementType="PREPARED"timeout="20"><deleteid="deleteAuthor"statementType="PREPARED"timeout="20">
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
useGeneratedKeys | (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。 |
keyProperty | (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset )。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn | (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
三、MyBatis 多表映射
1. 多表映射概念
1.1 多表查询结果映射思路
- 上面讲解了单表的mybatis操作!但是开发中更多的是多表查询需求,这种情况我们如何该如何进行处理?
- MyBatis 思想是:数据库不可能永远是你所想或所需的那个样子。 我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。 如果能有一种数据库映射模式,完美适配所有的应用程序查询需求,那就太好了,而 ResultMap 就是 MyBatis 就是完美答案。
- 官方例子:我们如何映射下面这个语句?
<!-- 非常复杂的语句 -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">selectB.id as blog_id,B.title as blog_title,B.author_id as blog_author_id,A.id as author_id,A.username as author_username,A.password as author_password,A.email as author_email,A.bio as author_bio,A.favourite_section as author_favourite_section,P.id as post_id,P.blog_id as post_blog_id,P.author_id as post_author_id,P.created_on as post_created_on,P.section as post_section,P.subject as post_subject,P.draft as draft,P.body as post_body,C.id as comment_id,C.post_id as comment_post_id,C.name as comment_name,C.comment as comment_text,T.id as tag_id,T.name as tag_namefrom Blog Bleft outer join Author A on B.author_id = A.idleft outer join Post P on B.id = P.blog_idleft outer join Comment C on P.id = C.post_idleft outer join Post_Tag PT on PT.post_id = P.idleft outer join Tag T on PT.tag_id = T.idwhere B.id = #{id}
</select>
- 你可能想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。 我们先来看看下面这个完整的例子,它是一个非常复杂的结果映射(假设作者,博客,博文,评论和标签都是类型别名)。 虽然它看起来令人望而生畏,但其实非常简单。
<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog"><constructor><idArg column="blog_id" javaType="int"/></constructor><result property="title" column="blog_title"/><association property="author" javaType="Author"><id property="id" column="author_id"/><result property="username" column="author_username"/><result property="password" column="author_password"/><result property="email" column="author_email"/><result property="bio" column="author_bio"/><result property="favouriteSection" column="author_favourite_section"/></association><collection property="posts" ofType="Post"><id property="id" column="post_id"/><result property="subject" column="post_subject"/><association property="author" javaType="Author"/><collection property="comments" ofType="Comment"><id property="id" column="comment_id"/></collection><collection property="tags" ofType="Tag" ><id property="id" column="tag_id"/></collection></collection>
</resultMap>
- 现在可能看不懂,接下来要学习将多表查询结果使用ResultMap标签映射到实体类对象上!
- 目标:
- 多表查询语句使用
- 多表结果承接实体类设计
- 使用ResultMap完成多表结果映射
1.2 实体类设计方案
1.2.1 多表关系回顾:(双向查看)
- 一对一
- 夫妻关系,人和身份证号
- 一对多| 多对一
- 用户和用户的订单,锁和钥匙
- 多对多
- 老师和学生,部门和员工
1.2.2 实体类设计关系(查询):(单向查看)
1.2.2.1 对一
-
对一 : 夫妻一方对应另一方,订单对应用户都是对一关系
-
实体类设计:对一关系下,类中只要包含单个对方对象类型属性即可!
public class Customer {private Integer customerId;private String customerName;}public class Order {private Integer orderId;private String orderName;private Customer customer;// 体现的是对一的关系}
1.2.2.2 对多
- 对多: 用户对应的订单,讲师对应的学生或者学生对应的讲师都是对多关系
- 实体类设计:对多关系下,类中只要包含对方类型集合属性即可!
public class Customer {private Integer customerId;private String customerName;private List<Order> orderList;// 体现的是对多的关系
}public class Order {private Integer orderId;private String orderName;private Customer customer;// 体现的是对一的关系}//查询客户和客户对应的订单集合 不要管!
1.2.3 多表结果实体类设计小技巧:
- 对一,属性中包含对方对象
- 对多,属性中包含对方对象集合
- 只有真实发生多表查询时,才需要设计和修改实体类,否则不提前设计和修改实体类!
- 无论多少张表联查,实体类设计都是两两考虑!
- 在查询映射的时候,只需要关注本次查询相关的属性!例如:查询订单和对应的客户,就不要关注客户中的订单集合!
1.3 多表映射案例准备
1.3.1 数据库
CREATE TABLE `t_customer` (`customer_id` INT NOT NULL AUTO_INCREMENT, `customer_name` CHAR(100), PRIMARY KEY (`customer_id`) );CREATE TABLE `t_order` ( `order_id` INT NOT NULL AUTO_INCREMENT, `order_name` CHAR(100), `customer_id` INT, PRIMARY KEY (`order_id`) ); INSERT INTO `t_customer` (`customer_name`) VALUES ('c01');INSERT INTO `t_order` (`order_name`, `customer_id`) VALUES ('o1', '1');
INSERT INTO `t_order` (`order_name`, `customer_id`) VALUES ('o2', '1');
INSERT INTO `t_order` (`order_name`, `customer_id`) VALUES ('o3', '1');
- 实际开发时,一般在开发过程中,不给数据库表设置外键约束。
- 原因是避免调试不方便。
- 一般是功能开发完成,再加外键约束检查是否有bug。
1.3.2 实体类设计
- 稍后会进行订单关联客户查询,也会进行客户关联订单查询,所以在这先练习设计
@Data
public class Customer {private Integer customerId;private String customerName;private List<Order> orderList;// 体现的是对多的关系} @Data
public class Order {private Integer orderId;private String orderName;private Customer customer;// 体现的是对一的关系}