第一次接触
MyBatis
框架还是在半年前,当时第一次用的时候觉得真爽啊,不用自己封装工具类了,也不用自己管理数据库连接了,而且性能也提高了不少。但是后来在开发中逐渐接触到更多的框架之后,比如MyBatisPlus
以及JPA
等,发现这些框架更牛,省去了最基本的CRUD
,于是就去使用那些更能提高开发效率的框架了,到现在前前后后也有半年没碰这个框架了,很是惭愧,现在忘得一干二净了,今天花了一天的时间回顾了一下MyBatis
的基本操作,以及底层原理,被该框架所使用到的设计模式以及思想所折服。太强了,每看到一处都直呼妙啊
。
最近在回顾 java
关于数据库的操作,有以下文章供参考:
首先我们先把环境搭建好,具体的就是以下几个配置文件:
Mybatis
的主配置文件,用于注册其他的 Mapper
,以及建立数据库连接。jdbc
连接所必须的配置信息,这里我们单独的抽取出来作为一个文件。pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>top.wsuo</groupId>
<artifactId>wsuo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
</dependency>
</dependencies>
</project>
SqlMapConfig.xml:
这里 MyBatis
提供多种配置方式,这里我们都采取了最优的方式配置了,注释掉的为其他配置方式。
<?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="jdbcConfig.properties"/>
<typeAliases>
<!-- <typeAlias type="top.wsuo.pojo.User" alias="user"/>-->
<!-- 自动识别包下面的类名作为别名 -->
<package name="top.wsuo.pojo"/>
</typeAliases>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- <mapper resource="mapper/IUserDao.xml"/>-->
<package name="top.wsuo.dao"/>
</mappers>
</configuration>
jdbcConfig.properties:
注意后面加上编码规则,不然插入操作有可能会出现乱码问题。
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///eesy?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=root
log4j.properties:
这是 log4j
的配置文件,具体如何运行的可以不用管,他会在项目运行的时候在根目录生成一个axis.log
文件,用于记录你在控制台所有的输出信息。
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
然后我们先将数据库中的表建好:
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`
(
`id` int(11) NOT NULL auto_increment,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`birthday` datetime default NULL COMMENT '生日',
`sex` char(1) default NULL COMMENT '性别',
`address` varchar(256) default NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
insert into `user`(`id`, `username`, `birthday`, `sex`, `address`)
values (41, '老王', '2018-02-27 17:47:08', '男', '北京'),
(42, '小二王', '2018-03-02 15:09:37', '女', '北京金燕龙'),
(43, '小二王', '2018-03-04 11:34:34', '女', '北京金燕龙'),
(46, '老王', '2018-03-07 17:37:26', '男', '北京'),
(48, '小马宝莉', '2018-03-08 11:44:00', '女', '北京修正');
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`
(
`ID` int(11) NOT NULL COMMENT '编号',
`UID` int(11) default NULL COMMENT '用户编号',
`MONEY` double default NULL COMMENT '金额',
PRIMARY KEY (`ID`),
KEY `FK_Reference_8` (`UID`),
CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
insert into `account`(`ID`, `UID`, `MONEY`)
values (1, 41, 1000),
(2, 45, 1000),
(3, 41, 2000);
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`
(
`ID` int(11) NOT NULL COMMENT '编号',
`ROLE_NAME` varchar(30) default NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) default NULL COMMENT '角色描述',
PRIMARY KEY (`ID`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
insert into `role`(`ID`, `ROLE_NAME`, `ROLE_DESC`)
values (1, '院长', '管理整个学院'),
(2, '总裁', '管理整个公司'),
(3, '校长', '管理整个学校');
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`
(
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`, `RID`),
KEY `FK_Reference_10` (`RID`),
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`),
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
insert into `user_role`(`UID`, `RID`)
values (41, 1),
(45, 1),
(41, 2);
总共有四张表:
单表操作这里以 User 为例。
首先我们创建出实体类 User,省略了 get
set
方法:
package top.wsuo.pojo;
import java.io.Serializable;
import java.util.Date;
/**
* 用户
*
* @Author shuo wang
* @Date 2020/5/24 0024 6:16
* @Version 1.0
*/
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
}
然后是 IUserDao
这个接口:
package top.wsuo.dao;
import top.wsuo.pojo.User;
import java.util.List;
/**
* 账户接口
*
* @author shuo wang
* @version 1.0
* @date 2020/5/24 0024 6:16
*/
public interface IUserDao {
/**
* 查询所有
*
* @return 返回的是list集合
*/
List<User> findAll();
/**
* 保存用户
*
* @param user 需要一个用户对象
*/
void saveUser(User user);
/**
* 修改记录
*
* @param user 需要一个用户对象
*/
void updateUser(User user);
/**
* 删除用户
*
* @param id 需要用户的 id
*/
void deleteUser(Integer id);
/**
* 根据id查询
*
* @param id 待查用户的 id
* @return 但会用户对象
*/
User findById(Integer id);
/**
* 模糊查询
*
* @param username 用户名
* @return 返回所有满足条件的用户
*/
List<User> findByUserName(String username);
/**
* 动态SQL查询
*
* @param user 用户对象
* @return 返回所有满足条件的 list 集合
*/
List<User> findUserByCondition(User user);
}
但是我们不用去实现这个接口,交给 MyBatis
来实现,所以我们需要通过配置文件来告诉它怎么配置,新建IUserDao.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">
<!--namespace表示这个文件要实现的接口-->
<mapper namespace="top.wsuo.dao.IUserDao">
<!-- 查询所有 -->
<select id="findAll" resultType="user">
select *
from user;
</select>
<!-- 保存操作 -->
<insert id="saveUser" parameterType="user">
# 查询最后一个插入的id值
<selectKey resultType="int" keyProperty="id" keyColumn="id" order="AFTER">
select last_insert_id();
</selectKey>
insert into user(username, birthday, sex, address)
values (#{username}, #{birthday}, #{sex}, #{address})
</insert>
<!-- 更新操作 -->
<update id="updateUser" parameterType="user">
update user
set username=#{username},
birthday=#{birthday},
sex=#{sex},
address=#{address}
where id = #{id}
</update>
<!-- 删除操作 -->
<delete id="deleteUser" parameterType="int">
delete
from user
where id = #{id}
</delete>
<!-- 根据 ID 查询 -->
<select id="findById" parameterType="int" resultType="user">
select *
from user
where id = #{id};
</select>
<!-- 模糊查询 -->
<select id="findByUserName" parameterType="String" resultType="user">
select *
from user
where username like #{username};
</select>
<!-- 动态SQL -->
<select id="findUserByCondition" resultType="list" parameterType="user">
select *
from user
<where>
<if test="username != null">
and username = #{username}
</if>
;
</where>
</select>
</mapper>
注意这里使用的:
<where>
<if test="username != null">
and username = #{username}
</if>
;
</where>
是动态 SQL 语句。
最后使用 JUnit
创建测试类测试:
public class DaoTest {
private InputStream in = null;
private SqlSession sqlSession = null;
private IUserDao userDao;
@Before
public void init() throws IOException {
in = Resources.getResourceAsStream("SqlMapConfig.xml");
sqlSession = new SqlSessionFactoryBuilder()
.build(in)
.openSession(true);
userDao = sqlSession.getMapper(IUserDao.class);
}
@After
public void destroy() throws IOException {
sqlSession.close();
in.close();
}
@Test
public void IUserDaoTest() {
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
sqlSession.close();
}
@Test
public void TestSave() {
User user = new User();
user.setUsername("小李子");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("江苏徐州");
userDao.saveUser(user);
}
@Test
public void TestUpdate() {
User user = new User();
user.setId(49);
user.setUsername("小李子");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("江苏徐州");
userDao.updateUser(user);
}
@Test
public void TestDelete() {
userDao.deleteUser(50);
}
@Test
public void TestFindOne() {
System.out.println(userDao.findById(49));
}
/**
* 模糊查询,拼接 % % 作为条件
*/
@Test
public void TestLike() {
List<User> users = userDao.findByUserName("%说%");
for (User user : users) {
System.out.println(user);
}
}
@Test
public void TestFindCondition() {
User user = new User();
user.setUsername("小说子");
List<User> users = userDao.findUserByCondition(user);
for (User u : users) {
System.out.println(u);
}
}
}
一对多查询也包括一对一查询。
我们先来看一对一查询,这里体现在每个账单都对应一个用户,我们首先创建一个账单类 Account
。
User 和 Account 的关系如下:
这里省略了 get
set
方法:
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
}
我们想要的结果集应该是这样的:
我们可以直接在 Account 的 xml 配置文件中设置resultMap
,让他的输出为一个新的对象映射关系,这里要使用association
标签。
<resultMap id="accountUserMap" type="account">
<id property="id" column="aid"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
<association property="user" javaType="user" column="uid">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="address" column="address"/>
<result property="sex" column="sex"/>
<result property="birthday" column="birthday"/>
</association>
</resultMap>
<select id="findAll" resultMap="accountUserMap">
# select *
# from account;
select u.*, a.ID as aid, a.uid, a.MONEY
from account a,
user u
where a.UID = u.id;
</select>
注意一点就是这里的 user
要作为 Account
类的成员变量。
// 一对一, Account 中包含 User
private User user;
还有一种方法就是新建一个类,包括以上结果集的每一列,这样也可以实现。
然后是一对多,这里体现在每个用户可以有多个订单,所以我们可以使用 User 去左外连接 Account 。
select *
from user u
left join account a on u.id = a.UID;
我们可以在 IUserDao 中添加一个 resultMap
。
涉及到一对多的时候需要使用collection
标签来表示。
<mapper namespace="top.wsuo.dao.IUserDao">
<resultMap id="userAccountMap" type="user">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<collection property="accounts" ofType="account">
<id property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
</collection>
</resultMap>
<select id="findAll" resultMap="userAccountMap">
select *
from user u
left join account a on u.id = a.UID;
</select>
<select id="findById" resultType="user">
select *
from user
where id = #{id};
</select>
</mapper>
注意这个时候,User
的实体类中应该包含一个 List<Account>
,这样才能体现一对多的关系。
// 一对多,User 中包含 Account 的集合
private List<Account> accounts;
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
我们在写 resultMap
的时候要注意column
属性要和查出来的列名保持一致,property
属性要和实体类中的相关字段保持一致。
最后是多对多,这里我们引入一个中间表。
user
表:
role
表:
role_user
表:
我们要做的查询应该是这样的:
select u.*, r.ID as rid, r.ROLE_NAME, r.ROLE_DESC
from role r
left join user_role ur on r.ID = ur.RID
left join user u on ur.UID = u.id;
我们在 xml
中做如下配置:
<mapper namespace="top.wsuo.dao.IRoleDao">
<resultMap id="roleMap" type="role">
<id property="id" column="rid"/>
<result property="roleName" column="ROLE_NAME"/>
<result property="roleDesc" column="ROLE_DESC"/>
<collection property="users" ofType="user">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
</collection>
</resultMap>
<select id="findAll" resultMap="roleMap">
select u.*, r.ID as rid, r.ROLE_NAME, r.ROLE_DESC
from role r
left join user_role ur on r.ID = ur.RID
left join user u on ur.UID = u.id;
</select>
</mapper>
对应的实体类关系为:
public class Role implements Serializable {
private Integer id;
private String roleName;
private String roleDesc;
// 多对多的关系映射
private List<User> users;
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
//...
}
我们只需要保证自己封装的resultMap
可以对应上查询的结果即可。
总结一下就是对于单表查询,我们无需配置 resultMap
,对于多表查询,一般使用 resultMap
与其子标签 association
,collection
配合使用。
延时加载
常用在一对多
或多对多
中,因为后面是多,有时候我们不需要全部都查出来,都查出来反而浪费时间,所以延时加载。立即加载
,常用于一对一
和多对一
中。下面我们以用户 User
与账单 Account
的一对多关系演示一下延时加载:
<resultMap id="userAccountMap" type="user">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<collection property="accounts" ofType="account"
column="id" select="top.wsuo.dao.IAccountDao.findAccountByUid"/>
</resultMap>
// 注意这里的 findAll 就不能再使用外连接了
<select id="findAll" resultMap="userAccountMap">
select *
from user;
</select>
column
:用用户的 ID 去查所有的账单,所以是 id
;select
:表示用什么方法去查,这里调用的是 Account 中的根据用户 id 查询所有账单的方法;所以在 Account 的 dao
中应该提供一个方法来获取所有符合条件的 Account:
<select id="findAccountByUid" resultType="account" parameterType="int">
select *
from account
where UID = #{id};
</select>
二级缓存
dao
配置文件中添加<cache/>
标签;useCache="true"
由于注解实现比较简单,所以这里只是用上面的例子简单介绍一下。
public interface IUserDao {
/**
* 查询所有用户
*
* @return 返回用户集合
*/
@Select("select * from user")
List<User> findAll();
/**
* 保存用户
*/
@Insert("insert into user(username, address, sex, birthday) values(#{username}, #{address}, #{sex}, #{birthday})")
void saveUser(User user);
/**
* 更新用户
*
* @param user 传入用户对象
*/
@Update("update user set username=#{username}, address=#{address}, sex=#{sex}, birthday=#{birthday} where id=#{id}")
void updateUser(User user);
/**
* 删除用户
*
* @param id 传入用户id
*/
@Delete("delete from user where id=#{id}")
void deleteUser(Integer id);
/**
* 根据id查询
*
* @param id 传入 id
* @return 返回用户
*/
@Select("select * from user where id=#{id};")
User findById(Integer id);
/**
* 根据用户名模糊查询
*
* @return 返回所有符合条件的用户
*/
@Select("select * from user where username like #{username}")
List<User> findByUser();
/**
* 查询总共的个数
*
* @return 返回个数
*/
@Select("select count(id) from user;")
int findTotal();
}
可以看到很简单,不用重新配置方法名和返回值信息,直接在上面写即可,所以单表查起来是很方便的,所以开发中常用注解查单表,使用配置文件查多表。
我们之前遇到过数据库表中的字段和实体类字段不对应的时候使用的是 resultMap
标签来配置的,使用注解也可以这样配置,要使用Results
(定义) 和 ResultMap
(引用) 注解。
可以使用 @one
和 @many
注解实现功能,注解中的属性值和标签的属性值类似。只不过使用的是延迟加载的那种写法,需要从表提供一个根据 id
查询的方法,然后放到 select
属性值中,要使用全限定方法名。
下面我们以一个具体的例子来看一下,还是使用 User
表和 Account
表:
Account 中的代码:
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
//多对一(mybatis中称之为一对一)的映射:一个账户只能属于一个用户
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
······
}
我们之前使用的是 association
标签,现在我么使用注解实现:
/**
* 查询所有账户,并且获取每个账户所属的用户信息
*
* @return 返回 List 集合
*/
@Select("select * from account")
@Results(id = "accountMap", value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "uid", property = "uid"),
@Result(column = "money", property = "money"),
@Result(property = "user",
column = "uid",
one = @One(select = "top.wsuo.dao.IUserDao.findById", fetchType = FetchType.EAGER))
})
List<Account> findAll();
Results
:封装结果集,并指定唯一 id
;Result
:指定属性与数据库中属性的映射关系, id
指定是否是主键,默认是 false
;@One
:代表一对一的关系;fetchType = FetchType.EAGER
: 立即加载;select
:指定全限定类名+方法名。可以看到使用的是延迟加载的方法,所以我们在 IUserDao
中需要提供一个方法。
/**
* 根据id查询用户
*
* @param userId 用户 ID
* @return 返回用户对象
*/
@Select("select * from user where id=#{id} ")
@ResultMap("userMap")
User findById(Integer userId);
注意这里有一个 @ResultMap("userMap")
,这是干什么的?我们下面再说。
这样我们就先实现了一对一查询。
那么一对多呢? 肯定是使用 Many
注解了。
/**
* 查询所有用户
*
* @return 返回用户集合
*/
@Select("select * from user")
@Results(id = "userMap", value = {
@Result(id = true, column = "id", property = "userId"),
@Result(column = "username", property = "userName"),
@Result(column = "address", property = "userAddress"),
@Result(column = "sex", property = "userSex"),
@Result(column = "birthday", property = "userBirthday"),
@Result(property = "accounts", column = "id",
many = @Many(select = "top.wsuo.dao.IAccountDao.findAccountByUid",
fetchType = FetchType.LAZY))
})
List<User> findAll();
@Many
:表示一对多的关系;select
:指定全限定类名+方法名;fetchType = FetchType.LAZY
:延迟加载。然后我们再来看这个方法:
@Select("select * from user where id=#{id} ")
@ResultMap("userMap")
User findById(Integer userId);
他返回的是 userMap
集合,这是我们之前定义的结果集,延迟加载的好处就在于你不用的话他就不会查出来,可以看到这里就没用到 accounts
,所以这里不会查出来,只会查出来 User
,就是我们想要的。
注解开启二级缓存
在对应的 dao
接口上面添加 cacheNameSpace
注解开启缓存。
中文乱码问题:
jdbc.url=jdbc:mysql:///eesy?useUnicode=true&characterEncoding=UTF-8
巨人的肩膀: