Mybatis框架

在之前的内容中,我写了Java的基础知识、Java Web的相关知识。有这些内容就可以编写各种各样丰富的程序。但是如果纯粹手写所有代码,工作量仍然很大。为了简化开发,隐藏一些不必要的细节,专心处理业务相关内容 ,Java提供了许多现成的框架可以使用

Mybatis介绍

在程序开发中讲究 MVC 的分层架构,其中M表示的是存储层,也就是与数据库交互的内容。一般来说使用jdbc时,需要经历:导入驱动、创建连接、创建statement对象,执行sql、获取结果集、封装对象、关闭连接这样几个过程。里面很多过程的代码都是固定的,唯一有变化的是执行sql并封装对象的操作。而封装对象时可以利用反射的机制,将返回字段的名称映射到Java实体类的各个属性上。这样我们很自然的就想到了,可以编写一个框架或者类库,实现仅配置sql语句和对应的映射关系,来实现查询到封装的一系列操作,从而简化后续的开发。Mybatis帮助我们实现了这个功能。

Mybatis实例

假设现在有一个用户表,存储用户的相关信息,我们现在需要使用mybatis来进行查询操作,可能要经历如下步骤:

  1. 定义对应的实体类
public class User {
    private Integer id;
    private String username;
    private String birthday;
    private char sex;
    private String address;

	//后面省略对应的getter和setter方法
	//为了方便后面的实体类都会省略这些内容
}

  1. 编辑主配置文件,主要用来配置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>
  1. 编写dao接口
public interface IUserDao {
    public List<User> findAll();
}
  1. 并提供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大致的执行过程

  1. 根据我们传入的InputStream对象来获取配置xml中对应对象的值
  2. 接着根据配置信息创建连接并生成数据库的连接池对象
  3. 根据配置文件中的mapper项获取到对应的Dao接口的配置文件,在读取该文件时会准备一个Map结构,其中key是mapper中的namespace + id,value是对应的sql语句,例如上述例子中得到的map结构为{“com.MyBatisDemo.Dao.IUserDao.findAll”, “select * from user”}
  4. 在创建sqlsession时从连接中获取到一个Statement对象
  5. 在我们调用dao接口时,首先根据dao接口得到详细的类名,然后获取到当前调用的接口名称,由这两项得到一个key,比如在上述例子中,dao接口的名称为com.MyBatisDemo.Dao.IUserDao, 而调用的方法是 findAll,将这两个字符串进行拼接,得到一个key,根据这个key去map中查找对应的sql语句。并执行
  6. 执行sql语句获取查询的结果集
  7. 根据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中有许多子标签用来表示这个映射关系

  1. id用来表明表结构中主键的映射关系
  2. result表示其他字段的映射关系
  3. 每个标签中的column属性表示的是对应的表字段名
  4. 标签中的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属性,它用来定义使用的连接池,该属性有三个取值:

  1. POOLE:使用连接池,采用javax.sql.DataSource 规范中的连接池,mybatis中有针对它的数据库连接池的实现
  2. UNPOOLED:与POOLED相同,使用的都是javax.sql.DataSource 规范,但是它使用的是常规的连接方式,没有采用池的思想
  3. 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共享同一个二级缓存。二级缓存是一个结果的二进制值,每当我们使用它时,它会取出这个二进制值,并将这个值封装为一个新的对象。在我们多次使用同一片二级缓存中的数据,得到的对象也不是同一个

使用二级缓存需要进行一些额外的配置:

  1. 在主配置文件中添加配置 在settings的子标签setting 中添加属性 enableCache=True开启二级缓存
  2. 在对应的dao xml配置中添加 cache标签(标签中不需要任何属性或者文本内容),使接口支持缓存
  3. 在对应的select、update等标签上添加属性 useCache=true,为方法开启二级缓存
posted @ 2023-07-19 15:46  masimaro  阅读(1)  评论(0编辑  收藏  举报  来源