在之前的内容中,我写了Java的基础知识、Java Web的相关知识。有这些内容就可以编写各种各样丰富的程序。但是如果纯粹手写所有代码,工作量仍然很大。为了简化开发,隐藏一些不必要的细节,专心处理业务相关内容 ,Java提供了许多现成的框架可以使用
Mybatis介绍
在程序开发中讲究 MVC 的分层架构,其中M表示的是存储层,也就是与数据库交互的内容。一般来说使用jdbc时,需要经历:导入驱动、创建连接、创建statement对象,执行sql、获取结果集、封装对象、关闭连接这样几个过程。里面很多过程的代码都是固定的,唯一有变化的是执行sql并封装对象的操作。而封装对象时可以利用反射的机制,将返回字段的名称映射到Java实体类的各个属性上。这样我们很自然的就想到了,可以编写一个框架或者类库,实现仅配置sql语句和对应的映射关系,来实现查询到封装的一系列操作,从而简化后续的开发。Mybatis帮助我们实现了这个功能。
Mybatis实例
假设现在有一个用户表,存储用户的相关信息,我们现在需要使用mybatis来进行查询操作,可能要经历如下步骤:
- 定义对应的实体类
public class User {
private Integer id;
private String username;
private String birthday;
private char sex;
private String address;
//后面省略对应的getter和setter方法
//为了方便后面的实体类都会省略这些内容
}
- 编辑主配置文件,主要用来配置mybati的数据库连接信息以及指定对应dao的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTDConfig3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mybatis主配置文件-->
<configuration>
<!--配置环境-->
<environments default="mybatis_demo">
<environment id="mybatis_demo">
<!--配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置连接池-->
<dataSource type="POOLED">
<!--配置数据库连接的4个基本信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/>
<property name="username" value="root"/>
<property name="password" value="masimaro_root"/>
</dataSource>
</environment>
</environments>
<!--指定配置文件的位置,配置文件是每个dao独立的配置文件-->
<mappers>
<mapper resource="com/MybatisDemo/Dao/IUserDao.xml"></mapper>
</mappers>
</configuration>
- 编写dao接口
public interface IUserDao {
public List<User> findAll();
}
- 并提供dao的xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTDMapper3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--每个函数配置一条,标签名是要进行的数据库操作,resultType是需要返回的数据类型-->
<mapper namespace="com.MyBatisDemo.Dao.IUserDao">
<!--标签里面的文本是sql语句-->
<select id="findAll" resultType="com.MyBatisDemo.domain.User">
select * from user;
</select>
</mapper>
写完了对应的配置代码,接下来就是通过简单的几行代码来驱动mybatis,完成查询并封装的操作
InputStream is = null;
SqlSession = null;
try {
//加载配置文件
is = Resources.getResourceAsStream("dbconfig.xml");
//创建工厂对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
//创建sqlsession对象
sqlSession = factory.openSession();
//使用sqlsession对象创建dao接口的代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
//使用对象执行方法
List<User> users = this.userDao.findAll();
System.out.println(users);
} catch (IOException e) {
e.printStackTrace();
}finally{
// 清理资源
if (null != this.is){
try {
this.sqlSession.commit();
this.is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != this.sqlSession){
this.sqlSession.close();
}
}
mybatis大致的执行过程
- 根据我们传入的InputStream对象来获取配置xml中对应对象的值
- 接着根据配置信息创建连接并生成数据库的连接池对象
- 根据配置文件中的mapper项获取到对应的Dao接口的配置文件,在读取该文件时会准备一个Map结构,其中key是mapper中的namespace + id,value是对应的sql语句,例如上述例子中得到的map结构为{"com.MyBatisDemo.Dao.IUserDao.findAll", "select * from user"}
- 在创建sqlsession时从连接中获取到一个Statement对象
- 在我们调用dao接口时,首先根据dao接口得到详细的类名,然后获取到当前调用的接口名称,由这两项得到一个key,比如在上述例子中,dao接口的名称为
com.MyBatisDemo.Dao.IUserDao
, 而调用的方法是findAll
,将这两个字符串进行拼接,得到一个key,根据这个key去map中查找对应的sql语句。并执行 - 执行sql语句获取查询的结果集
- 根据resultType中指定的对象进行封装并返回对应的实体类
使用mybatis实现增删改查操作
在之前的代码上可以看出,使用mybatis来实现功能时,只需要提供dao接口中的方法,并且将方法与对应的sql语句绑定。在提供增删改查的dao方法时如果涉及到需要传入参数的情况下该怎么办呢?
下面以根据id查询内容为例:
我们先在dao中提供这样一个方法:
public User findById(int id);
然后在dao的配置文件中编写sql语句
<!--parameterType 表示传入的参数的类型-->
<select id="findById" resultType="com.MyBatisDemo.domain.User" parameterType="int">
select * from user where id = #{id}
</select>
从上面的配置可以看到,mybatis中, 使用#{}
来表示输入参数,使用属性parameterType属性来表示输入参数的类型。一般如果使用Java内置对象是不需要使用全限定类名,也不区分大小写。
当我们使用内置类型的时候,这里的id
仅仅起到占位符的作用,取任何名字都可以
看完了使用内置对象的实例,再来看看使用使用自定义类类型的情况,这里我们使用update的例子来说明,首先与之前的操作一样,先定义一个upate的方法:
void updateUser(User user);
然后使用如下配置
<update id="updateUser" parameterType="User">
update user set username=#{username}, birthday=#{birthday}, sex=#{sex}, address=#{address} where id = #{id}
</update>
与使用id查询的配置类似,当我们使用的是自定义类类型时,在对应的字段位置需要使用类的属性表示,在具体执行的时候,mybatis会根据传入的类对象来依据配置取出对应的属性作为sql语句的参数。上面在使用内置对象时我们说它可以取任何的名称,但是这里请注意 名称只能是自定义对象的属性名,而且区分大小写
这里使用的都是确定的值,如果要使用模糊查询时该如何操作呢,这里我们按照名称来模糊查询,首先在dao中提供一个对应的方法
User findByName(String name);
接着再来进行配置
<select resultType="com.MyBatisDemo.domain.User" parameterType="String">
select * from User where username like #{username}
</select>
从sql语句来看我们并没有实现模糊的方式,这时候在传入参数的时候就需要使用模糊的方式,调用时应该在参数中添加 %%
, 就像这样 userDao.findByName("%" + username + "%")
当然我们可以使用另外一种配置
<select resultType="com.MyBatisDemo.domain.User" parameterType="String">
select * from User where username like %${username}%
</select>
这样我们在调用时就不需要额外添加 %
了。
既然他们都可以作为参数,那么这两个符号有什么区别呢?区别在于他们进行查询的方式,$ 使用的是字符串拼接的方式来组成一个完成的sql语句进行查询,而#使用的是参数话查询的方式。一般来说拼接字符串容易造成sql注入的漏洞,为了安全一定要使用参数话查询的方式
mybatis的相关标签
resultMap标签
在之前的配置中,其实一直保持着数据库表的字段名与对应的类属性名同名,但是有些时候我们不能保证二者同名,为了解决这问题也为了以后进行一对多和多对多的配置,可以使用resultMap来定义数据库表字段名和类属性名的映射关系
下面是一个使用它的例子。
我们简单修改一下User类的属性定义
public class User {
private Integer uid;
private String name;
private String userBirthday;
private char userSex;
private String userAddress;
//后面省略对应的getter和setter方法
}
这样直接使用之前的配置执行会报错,报找不到对应属性的错误,这个时候就可以使用resultMap属性来解决这个问题
<resultMap id="UserMapper" type="User">
<id column="id" property="uid"></id>
<result column="username" property="username"></result>
<result column="sex" property="sex"></result>
<result column="birthday" property="birthday"></result>
<result column="address" property="address"></result>
</resultMap>
<select id="findAll" resultMap="UserMapper">
select * from user;
</select>
其中 id属性来唯一标示这个映射关系,在需要使用到这个映射关系的地方,使用resultMap这个属性来指定
type属性表示要将这些值封装到哪个自定义的类类型中
resultMap中有许多子标签用来表示这个映射关系
- id用来表明表结构中主键的映射关系
- result表示其他字段的映射关系
- 每个标签中的column属性表示的是对应的表字段名
- 标签中的property对应的是类属性的名称
properties 标签
properties标签可以用来定义数据库的连接属性,主要用于引入外部数据库连接属性的文件,这样我们可以通过直接修改连接属性文件而不用修改具体的xml配置文件。
假设现在在工程中还有一个database.properties
文件
jdbc.driver ="com.mysql.jdbc.Driver"
jdbc.url = "jdbc:mysql://localhost:3306/mybatis_demo"
jdbc.username ="root"
jdbc.password" ="masimaro_root"
然后修改对应的主配置文件
<!--引入properties文件-->
<properties resource="database.properties">
</properties>
<!--修改对应的dataSource标签-->
<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>
typeAliases 标签
之前我们说过,使用内置类型时不需要写全限定类名,而且它不区分大小写。而使用自定义类型时需要写很长一串,如何使自定义类型与内置类型一样呢?这里可以使用typeAliases标签。它用来定义类名的别名
<typeAliases>
<!--typeAlias中来定义具体类的别名,type表示真实类名,alias表示别名-->
<typeAlias type="com.MyBatisDemo.domain.User" alias="user"></typeAlias>
</typeAliases>
使用typeAlias标签时,每个类都需要提供一条对应的配置,当实体类多了,写起来就很麻烦了,这个时候可以使用package子标签来代替typeAlias
<typeAliases>
<package name="com.MyBatisDemo.domain"/>
</typeAliases>
它表示这里包中的所有类都使用别名,别名就是它的类名
package标签
在定义对应的mapper xml文件时,一个dao接口就需要一条配置。dao接口多了,一条条的写很麻烦,为了减轻编写的工作量可以使用package标签
<mappers>
<!--它表示这个包中的所有xml都是mapper配置文件-->
<package name="com/MyBatis/Dao"/>
</mappers>
连接池
在配置数据库连接 dataSource
标签中有一个type属性,它用来定义使用的连接池,该属性有三个取值:
- POOLE:使用连接池,采用
javax.sql.DataSource
规范中的连接池,mybatis中有针对它的数据库连接池的实现 - UNPOOLED:与POOLED相同,使用的都是
javax.sql.DataSource
规范,但是它使用的是常规的连接方式,没有采用池的思想 - JNDI:根据服务器提供的jndi基础来获取数据库的连接 ,具体获取到的连接对象又服务器提供
动态sql
当我们自己拼接sql的时候可以根据传入的参数的不同来动态生成不同的sql语句执行,而在之前的配置中,我们事先已经写好了使用的sql语句,但是如果碰上使用需要按照条件搜索,而且又不确定用户会输入哪些查询条件,在这样的情况下,没办法预先知道该怎么写sql语句。这种情况下可以使用mybatis中提供的动态sql
假设我们提供一个findByValue的方法,根据值来进行查询。
public List<User> findByValue(User user);
事先并不知道user的哪些属性会被赋值,我们需要做的就是判断user的哪些属性不为空,根据这些不为空的属性来进行and
的联合查询。这种情况下我们可以使用if标签
<select id="findByValue" resultType="User" parameterType="User">
select * from user where
<if test="id != null">
id = #{id} and
</if>
<if test="username != null">
username=#{username} and
</if>
.....
1=1
</select>
if标签中使用test来进行条件判断,而判断条件可以完全使用Java的语法来进行。
这里在最后用了一个1=1
的条件来结束判断,因为事先并不知道用户会传入哪些值,不知道哪条语句是最后一个条件,因此我们加一个恒成立的条件来确保sql语句的完整
当然mybatis中也有办法可以省略最后的1=1,我们可以使用 where标签来包裹这些if,表明if中的所有内容都是作为查询条件的,这样mybatis在最后会在生成查询条件后自动帮助我们进行格式的整理
使用if标签我们搞定了不确定用户会使用哪些查询条件的问题,如果有这样一个场景:用户只知道某个字段的名字有几种可能,我们在用户输入的几种可能值中进行查找,也就是说,用户可以针对同一个查询条件输入多个可能的值,根据这些个可能的值进行匹配,只要有一个值匹配上即可返回;
针对这种情况没办法使用if标签了,我们可以使用循环标签,将用户输入的多个值依次迭代,最终组成一个in
的查询条件
我们在这里提供一个根据多个id查找用户的方法
public List<User> findByIds(List<Integer> ids);
这里我们为了方便操作,额外提供一个类用来存储查询条件
public class QueryVo {
List<Integer> ids;
}
<select id="findUserByIds" resultType="User" parameterType="QueryVo">
select * from user
<where>
<if test="ids != null and ids.size() != 0">
<foreach collection="ids" open="and id in (" close=")" item= "id" separator=",">
${id}
</foreach>
</if>
</where>
</select>
在上面的例子中使用foreach来迭代容器其中使用collection表示容器,这里取的是parameterType中指定类的属性,open表示在迭代开始时需要加入查询条件的sql语句,close表示在迭代结束后需要添加到查询语句中的sql,item表示每个元素的变量名,separator表示每次迭代结束后要添加到查询语句中的字符串。当我们迭代完成后,整个sql语句就变成了这样: select * from user where 1=1 and id in (id1, id2, ...)
多表查询
一对多查询
在现实中存在着这么一些一对多的对应关系,像什么学生和班级的对应关系,用户和账户的对应关系等等。关系型数据库在处理这种一对多的情况下,使用的是在多对应的那张表中添加一个外键,这个外键就是对应的一那张表的主键,比如说在处理用户和账户关系时,假设一个用户可以创建多个账户,那么在账户表中会有一个外键,指向的是用户表的ID
在上面例子的基础之上,来实现一个一对多的关系。
首先添加一个账户的实体类,并且根据关系账户中应该有一个唯一的用户类对象,用来表示它所属的用户
public class Account {
private int id;
private int uid;
private double money;
private User user;
}
同时需要在User这个实体类上添加一个Account的列表对象,表示一个User下的多个Account
public class User {
private Integer id;
private String username;
private String birthday;
private char sex;
private String address;
private List<Account> accounts;
}
首先根据user来查询多个account,我们可以写出这样的sql语句来查询
select u.*, a.id as aid, a.money, a.uid from user as u left join account as a on a.uid = u.id;
那么它查询出来的结果字段名称应该是id, username, sex, birthday, address, aid, money, uid 这些,前面的部分可以封装为一个User对象,但是后面的部分怎么封装到Accounts中去呢,这里可以在resultMap中使用collection标签,该标签中对应的对象会被封装为一个容器。因此这里的配置可以写为:
<resultMap id="UserAccountMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
<collection property="accounts" ofType="account">
<id property="id" column="aid"></id>
<result property="money" column="money"></result>
<result property="uid" column="uid"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="UserAccountMap">
select u.*, a.ID as aid, a.MONEY, a.UID from user as u left join acc ount as a on u.id = a.uid
</select>
我们需要一个resultMap来告诉Mybatis,这些多余的字段该怎么进行封装,为了表示一个容器,我们使用了一个coolection标签,标签中的property属性表示这个容器被封装到resultType对应类的哪个属性中,ofType表示的是,容器中每一个对象都是何种类型,而它里面的子标签的含义与resultMap子标签的含义完全相同
从User到Account是一个多对多的关心,而从Account到User则是一个一对一的关系,当我们反过来进行查询时,需要使用的配置是 association
标签,它的配置与使用与collection
相同
<resultMap id="AccountUserMap" type="Account">
<id property="id" column="aid"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<association property="user" column="uid" javaType="user">
<id property="id" column="uid"></id>
<result property="username" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
</association>
</resultMap>
<select id="findUserAccounts" resultType="Account" parameterType="User">
select * from account where uid = ${id}
</select>
多对多查询
说完了一对多,再来说说多对多查询。多对多在关系型数据库中使用第三张表来体现,第三张表中记录另外两个表的主键作为它的外键。
这里使用用户和角色的关系来演示多对多查询
与之前一样,在两个实体类中新增对方的一个list对象,表示多对多的关系
public class Role implements Serializable {
private int id;
private String roleName;
private String roleDesc;
private List<User> users;
}
利用之前一对多的配置,我们只需要修改一下ResultMap和sql语句就可以完成多对多的查询
<mapper namespace="com.liuhao.Dao.IUserDao">
<resultMap id="UserRoleMapper" type="User">
<id property="id" column="id"></id>
<result column="username" property="username"></result>
<result column="sex" property="sex"></result>
<result column="address" property="address"></result>
<result column="birthday" property="birthday"></result>
<collection property="roles" ofType="role">
<id property="id" column="rid"></id>
<result column="role_desc" property="roleDesc"></result>
<result column="role_name" property="roleName"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="UserRoleMapper">
select user.*, role.ID as rid, role.ROLE_DESC, role.ROLE_NAME from u ser left outer join user_role on user_role.uid = user.id left OUTER join role on user_role.RID = role.ID
</select>
</mapper>
另一个多对多的关系与这个类似,这里就不再单独说明了
延迟加载
之前说了该如何做基本的单表和多表查询。这里有一个问题,在多表查询中,我们是否有必要一次查询出它所关联的所有数据,就像之前的一对多的关系中,在查询用户时是否需要查询对应的账户,以及查询账户时是否需要查询它所对应的用户。如果不需要的话,我么采用上面的写法会造成多执行一次查询,而且当它关联的数据过多,而这些数据我们用不到,这个时候就会造成内存资源的浪费。这个时候我们需要考虑使用延迟加载,只有需要才进行查询。
之前的sql语句一次会同时查询两张表,当然不满足延迟加载的要求,延迟加载应该将两张表的查询分开,先只查询需要的一张表数据,另一张表数据只在需要的时候查询。
根据这点我们进行拆分,假设我们要针对User做延迟加载,我们先不管accounts的数据,只查询user表,可以使用sql语句select * from user
, 在需要的时候执行select * from account where uid = id
在xml配置中可以在collection标签中使用select属性,该属性指向一个方法,该方法的功能是根据id获取所有对象的列表。也就说我们需要在AccountDao接口中提供这么一个方法,并且编写它的xml配置
public List<Account> findByUid(int uid);
接着我们对之前的xml进行改写
<resultMap id="UserMapper" type="User">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="sex" property="sex"></result>
<result column="birthday" property="birthday"></result>
<result column="address" property="address"></result>
<collection property="accounts" ofType="Account" select="com.liuhao.Dao.IAccountDao.findByUid" column="id">
</collection>
</resultMap>
<select id="findAll" resultMap="UserMapper">
select * from user;
</select>
完成了接口的编写与配置,还需要对主配置文件做一些配置,我们在主配置文件中添加settings节点,开启延迟加载
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
缓存
缓存用来存储一些不经常变化的内容,使用缓存可以减少查询数据库的次数,提高效率。mybatis有两种缓存,一种是在每个sqlsession中的缓存,一种是在每个SqlSessionFactory中的缓存
在SqlSession中的缓存又被叫做是Mybatis的一级缓存。每当完成一次查询操作时,会在SqlSession中形成一个map结构,用来保存调用了哪个方法,以及方法返回的结果,下一次调用同样的方法时会优先从缓存中取
当我们执行insert、update、delete等sql操作,或者执行SqlSession的close或者clearCache等方法时缓存会被清理
在SqlSessionFactory中的缓存被称做二级缓存,所有由同一个SqlSessionFactory创建出来的SqlSessin共享同一个二级缓存。二级缓存是一个结果的二进制值,每当我们使用它时,它会取出这个二进制值,并将这个值封装为一个新的对象。在我们多次使用同一片二级缓存中的数据,得到的对象也不是同一个
使用二级缓存需要进行一些额外的配置:
- 在主配置文件中添加配置 在settings的子标签setting 中添加属性 enableCache=True开启二级缓存
- 在对应的dao xml配置中添加 cache标签(标签中不需要任何属性或者文本内容),使接口支持缓存
- 在对应的select、update等标签上添加属性 useCache=true,为方法开启二级缓存