zoukankan      html  css  js  c++  java
  • 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,为方法开启二级缓存

  • 相关阅读:
    java 14 -7 Date
    java 14 -6 BigInteger和BigDecimal
    java 14 -5 System类
    java14-4 Pattern和Matcher类的使用
    java 14-3 正则表达式的分割
    转:StringBuilder与StringBuffer的区别(转)
    kafka之config/server.properties配置参数说明
    Kafka内核理解:消息的收集/消费机制
    kafka删除topic及其相关数据
    kafka使用问题解决
  • 原文地址:https://www.cnblogs.com/lanuage/p/12051857.html
Copyright © 2011-2022 走看看