Mybatis 复习

javamagicsun 2019-12-17

概述

mybatis 是一个用java编写的持久层框架, 它封装了jdbc操作的很多细节,使开发者只需要关注sql语句本身,而无需关注注册驱动,创建连接等繁杂过程,它使用了ORM思想实现了结果 集的封装

ORM Object Relational Mapping 对象关系映射,把数据库表和实体类及实体类的属性对应起来,让我们可以操作实体类就实现操作数据库表

入门案例

  1. 创建数据库,创建User表

  2. 创建maven工程并导入坐标

    <project ...>
        ...
        <packaging>jar</packaging>
    
        <dependencies>
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.2</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.15</version>
            </dependency>
        </dependencies>
    </project>
  3. 编写实体类

    public class User{
        private Integer id;
        private String username;
        private String password;
        getter()...
        setter()...
        toString()...
    }
  4. 创建UserDao接口

    public interface UserDao{
        List<User> findAll();   //查询所有用户
    }
  5. 创建主配置文件SqlMapConfig.xml

    <?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">
    <!-- mybatis的主配置文件 -->
    <configuration>
        <!-- 配置环境 -->
        <environments default="mysql">
            <!-- 配置mysql的环境 -->
            <environment id="mysql">
                <!-- 配置事务的类型 -->
                <transactionManager type="JDBC"></transactionManager>
                <!-- 配置数据源(连接池) -->
                <dataSource type="POOLED">
                    <!-- 配置连接数据库的四个基本信息 -->
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/db"/>
                    <property name="username" value="root"/>
                    <property name="password" value="root"/>
                </dataSource>
            </environment>
        </environments>
    
        <!-- 指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件 -->
        <mappers>
            <mapper resource="com/whteway/dao/UserDao.xml"/>
        </mappers>
    </configuration>
  6. 创建映射配置文件UserDao.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">
    <mapper namespace="com.whteway.dao.UserDao">
        <!-- 配置查询所有,id为对应方法名,resultType指定结果封装位置 -->
        <select id="findAll" resultType="com.whteway.domain.User">
            select * from user
        </select>
    </mapper>
  • 注意
    • Mybatis中把持久层的操作接口名称和映射文件也叫做Mapper,所以dao也可以叫mapper
    • 在idea中创建com.whteway.dao包时是三级结构,创建目录时是一级目录
    • 映射配置文件的位置结构必须和dao接口的包结构相同
    • 映射配置文件的mapper标签的namespace属性的取值必须是dao接口的全限定类名
    • 映射配置文件的操作标签的id属性必须是对应的方法名
    • 遵循以上规则,则不用写dao实现类
  1. 使用

    public void test(){
        //1.读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.创建SqlSessionFactory工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);
        //3.使用工厂生产SqlSession对象
        SqlSession session = factory.openSession();
        //4.使用SqlSession创建Dao接口的代理对象
        UserDao userDao = session.getMapper(UserDao.class);
        //5.使用代理对象执行方法
        List<User> users = userDao.findAll();
        for(User user: users)
            System.out.println(user);
        //6.释放资源
        session.close();
        in.close();
    }
  • 注解方式

    • 主配置文件SqlMapConfig.xml

      ...
          <mappers>
              <!-- <mapper resource="com/whteway/dao/UserDao.xml"/> -->
              <mapper class="com.whteway/dao/UserDao"/>
          </mappers>
      ...
    • UserDao.java

      public interface UserDao{
          @Select("select * from uesr")
          List<User> findAll();
      }
    • 删除UserDao.xml

  • 连接池

    mybatis连接池提供三种方式的配置

    • 配置位置:主配置文件中dataSource标签的type属性

    • type取值:

      • POOLED

        采用传统的javax.sql.DataSource规范中的连接池

      • UNPOOLED

        采用传统的获取连接的方式,实现了DataSource接口,没有用连接池思想

      • JNDI

        采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器能拿到的DataSource不一样,如果不是web或者maven的war工程,就不能使用

  • 事务管理

    • 通过SqlSession的 commit() 和 rollback() 方法
    • SqlSession openSession(boolean autoCommit); 传入false开启事务
  • 自定义dao实现类(不常用, 用来研究原理)

    public class UserDaoImpl implements UserDao{
        private SqlSessionFactory factory;
        public UserDaoImpl(SqlSessionFactory factory){
            this.factory = factory;
        }
        public List<User> findAll(){
            //使用工厂创建SqlSession对象
            SqlSession session = factory.openSession();
            //使用session对象执行sql语句
            List<User> users = session.selectList("映射配置文件的namespace.findAll");
            session.close();
            //返回查询结果
            return users;
        }
    }
    //-----------------使用----------------
    public void test(){
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);
    
        UserDao userdao = new UserDaoImpl(factory);
        List<User> users = userDao.findAll();
    
        in.close();
    }

流程分析

使用过程

  1. 读配置文件

    读取配置文件时用绝对路径和相对路径(web工程部署后没有src路径)都有一定问题,实际开发中一般有两种方法

    • 使用类加载器,它只能读取类路径的配置文件
    • 使用SerbletContext对象的getRealPath()
  2. 创建SqlSessionFactory工厂

    建造者模式(Builder Pattern)

  3. 使用工厂生产SqlSession对象

    工厂模式(Factory Pattern)

  4. 使用SqlSession创建Dao接口的代理对象

    代理模式(Proxy Pattern)

  5. 使用代理对象执行方法

  6. 释放资源

自定义dao中selectList()方法的执行流程,也是代理对象增强的逻辑

  1. 注册驱动,获取Connection对象(需要数据库信息)
    • 通过SqlMapConfig.xml的数据库信息,解析xml文件用到的是dom4j技术
  2. 获取预处理对象PreparedStatement(需要sql语句)
    • 通过SqlMapConfig.xml中的mapper标签定位UserDao.xml,映射配置文件中有sql语句
  3. 执行查询,得到结果集ResultSet
  4. 遍历结果集用于封装
    • 根据UserDao.xml中的resultType反射获得User对象,User对象的属性名和表中列名一致,可以一一封装进user对象中,再把user对象封装进list中
  • 所以,要想让selectList()执行,需要提供两个信息,连接信息和映射信息,映射信息包括sql语句和封装结果的实体类的全限定类名,所以映射信息可以用map存储

创建代理对象流程

  • 根据dao接口的字节码创建dao的代理对象

    public <T> T getMapper(Class<T> daoInterfaceClass){
        /*
          类加载器,使用和被代理对象相同的类加载器
          接口数组,和被代理对象实现相同的接口
          增强逻辑,自己提供,此处是一个InvocationHandler接口,需要写一个该接口的实现类,在其中调用selectList()方法
        */
        Proxy.newProxyInstance(类加载器, 接口数组,增强逻辑);
    }

原理分析:模拟Mybatis各组件实现入门程序

  • 分析用到的类

    public void test(){
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);
        SqlSession session = factory.openSession();
        UserDao userDao = session.getMapper(UserDao.class);
        List<User> users = userDao.findAll();
        for(User user: users) System.out.println(user);
        session.close();
        in.close();
    }
  • 解决读取配置信息的问题

    读取xml文件具体实现可以使用dom4j和xpath技术,坐标名dom4j和jaxen

    //模拟Mybatis的配置类
    public class Configuration{
        private String driver;
        private String url;
        private String username;
        private String password;
        /*
          mappers中的数据结构形如
          -------------------------------------------------------------------------
           key                               | value    
          -----------------------------------|-------------------------------------
          "com.whteway.dao.UserDao.findAll()"| queryString:"select * from user"
                                             | resultType:"com.whteway.domain.User"
          -----------------------------------|-------------------------------------
          "com.whteway.dao.StuDao.findAll()" | queryString:"select * from stu"
                                             | resultType:"com.whteway.domain.Stu"
          -----------------------------------|-------------------------------------
          "package.daoInterface.method()"    | queryString: SQL语句
                                             | resultType: 实体类的全限定类名
        */
        private Map<String Mapper> mappers;
        //to generate getters and setters exclude setMappers;
        public void setMappers(Map<String, Mapper> mappers){
            this.mappers.putAll(mappers); //追加而不是覆盖
        }
    }
    //用于封装映射配置文件的信息,SQL语句和封装实体类的全限定类名
    //SqlMapConfig.xml的mappers标签中的每一个mapper标签对应一个Mapper对象,
    //也就是说第一个映射配置文件对应一个Mapper对象
    public class Mapper{
        private String queryString;
        private String resultType;
        //to generate getters and setters;
    }
    //读取xml文件的工具类
    public class XMLConfigBuilder{
        //读取SqlMapConfig.xml文件并调用方法读取映射配置文件或加注解的类,将配置信息存入Configuration中并返回
        public static Configuration loadConfiguration(InputStream is){
            Configuration config = new Configuration();
            //1.读取SqlMapConfig文件,将数据库连接信息存入config
            /*2.根据SqlMapConfig文件中的mappers,属性循环读取每一个映射配置文件
                    如果用的是resource属性
                        mappers = loadMapperResource(resource.value);
                    如果用的是class属性
                        mappers = loadMapperAnnotation(class.value);
                    使用追加的方式将mappers存入config.mappers中
                        config.setMappers(mappers);
            */
            return config;
        }
        //读取映射配置文件
        public static Map<String, Mapper> laodMapperResource(String daoClassPath) throws Exception{
            Map<String, Mapper> mappers = new HashMap<String, Mapper>();
            /*
                1 循环读取每一个映射配置文件,如UserDao.xml
                2   读取mappers标签的namespace
                3   循环mappers标签中的每一个子标签(对应一个方法)
                4       读取id,组成namespace.id形式的字符串,作为key
                5       读取sql语句存入Mapper中的queryString
                6       读取resultType存入Mapper中的resultType
                7       将Mapper作为value,与key组成键值对,存入mappers中
            */
            return mappers;
        }
        //读取加注解的类
        public static Map<String, Mapper> laodMapperAnnotation(String daoClassPath) throws Exception{
            Map<String, Mapper> mappers = new HashMap<String, Mapper>();
            /*
                1.得到dao接口的字节码对象
                2.循环每一个方法(对应一个Mapper)
                3.  判断是否有select注解,如果有
                4.      获取各种数据封装到一个Mapper中,包名,类名,@Select的value值,返回类型的泛型的实际类型,方法名
                5.      把Mapper存入mappers中
            */
            return mappers;
        }
    }
  • Resources

    //将文件名转换为输入流
    public class Resources{
        //根据传入的参数,返回一个输入流
        //1.Resources.class 获取当前类的字节码
        //2.getClassLoader() 获取这个字节码的类加载器
        //3.getResourceAsStream(filePath) 根据类加载器读取文件配置
        public static InputStream getResourceAsStream(String filePath){
            return Resources.class.getClassLoader().getResourceAsStream(filePath);
        }
    }
  • SqlSessionFactoryBuilder 读取配置文件将配置信息存入配置类中的代码在此类中调用

    public class SqlSessionFactoryBuilder{
        //根据参数的字节输入流来构建一个SqlSessionFactory工厂
        public SqlSessionFactory build(InputStream config){
            Configuration cfg = XmlConfigBuilder.loadConfiguration(config);
            return new DefaultSqlSessionFactory(config);
        }
    }
  • SqlSessionFactory

    //接口
    public interface SqlSessionFactory{
        public SqlSession openSession();
    }
    //实现类
    public class DefaultSqlSessionFactory implements SqlSessionFactory{
        private Configuration cfg;
        //实例化此类必须传入配置信息
        public DefaultSqlSessionFactory(Configuration cfg){
            this.cfg = cfg;
        }
        //创建一个新的操作数据库的对象
        @Override
        public SqlSession openSession(){
            return new DeaultSqlSession(cfg);
        }
    }
  • SqlSession jdbc代码调用全在此类和此类的代理对象中

    //接口,和数据库交互的核心类
    public interface SqlSession{
        //创建代理对象,参数是dao接口的字节码
        public <T> T getMapper(Class<T> daoInterfaceClass);
        //释放资源
        public void close();
    }
    //实现类
    public class DefaultSqlSession implements SqlSession{
        private Configuration cfg;
        private Connection conn;
        //实例化此类必须传入配置信息
        public DefaultSqlSession(Configuration cfg){
            this.cfg = cfg;
            conn = DataSourceUtil.getConnection(cfg);
            //使用传统JDBC方式获取连接,创建数据源,建议封装到一个工具类中,源码中叫executor
            try{
                Class.forName(cfg.getDriver());
                conn = DriverManager.getConnection(cfg.getUrl(), cfg.getUserName(), cfg.getPassword())
            }catch(Exception e){
                throw new RuntimeException(e);
            }
        }
    
        @Override
        public <T> T getMapper(Class<T> daoInterfaceClass){
            return Proxy.newProxyInstance(daoInterfaceClass.getClassLoader()), new Class[]{daoInterfaceClass}, new MapperProxy(cfg.getMappers()));
        }
    
        //关闭Connection
        public void close(){
            if(conn != null){
                try{
                    conn.close();
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        } 
    }
    //作为创建代理对象时的第三个参数,用于对方法进行增强,此处的增强只需要调用selectList()方法
    public class MapperProxy implements InvocationHandler{
        //接收参数
        private Map<String, Mappers> mappers;
        private Connection conn;
        public MapperProxy(Map<String, Mapper> mappers, Connection conn){
            this.mappers = mappers;
            this.conn = conn;
        }
        //定义增强逻辑
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
            //获取方法名
            String methodName = method().getName();
            //获取方法所在的类的名称
            String className = method.getDeclaringClass().getName();
            //组合key
            String key = className + "." + methodName();
            //获取mappers中的mapper对象
            Mapper mapper = mappers.get(key);
            //判断参数是否合法
            if(mapper == null)
                throw new IllegalArgumentException("传入参数有误");
            //使用传统JDBC方式执行SQL语句并封装到实体类列表中返回,建议作为selectList()方法封装到工具类中
            selectList();
            return null;
        }
    }

单表CRUD

DML操作后要调用SqlSession.commit()方法进行事务提交

需要指定参数类型(实体类的全限定类名,基本类型可以用int,Integer,java.lang.Integer)

SQL语句传参时需要用#{实体类的属性值},此处的属性值是setter方法的set后的字符串并首字母小写

  • 保存 UserDao.saveUser(User user);

    • xml

      <insert id="saveUser" parameterType="com.whteway.domain.User">
          insert into user(username, address, sex, birthday) values(#{id},#{username},#{password})
      </insert>
  • 更新 UserDao.updateUser(User user);

    • xml

      <update id="updateUser" parameterType="com.whteway.domain.User">
          update user set username=#{username}, password#{password}) where id=#{id}
      </insert>
    • 注解

  • 删除 UserDao.deleteUser(Integer userId);

    • xml

      <delete id="deleteUser" parameterType="Integer">
          update from user where id=#{uid}
      </delete>
  • 查询单个 UserDao.findById(Integer id);

    • xml

      <select id="findById" parameterType="INT" resultType="com.whteway.domain.User">
          select * from user where id=#{id}
      </select>
  • 查询多个 UserDao.findByName(String name);

    • xml,传参的时候需要加%%(推荐,底层用的是PerparedStatement执行语句),不加%%可以使用固定写法 ‘%${value}%‘(底层用的是Statement执行语句)

      <select id="findByName" parameterType="string" resultType="com.whteway.domain.User">
          select * from user where username like #{name}
          /* select * from user where username like '%${value}' */
      </select>
  • 聚合函数 UserDao.findTotal();

    • xml

      <select id="findTotal" resultType="int">
          select count(id) from user;
      </select>

扩展

  • OGNL表达式

    • Object Graphic Navigation Language 对象 图 导航 语言

    • 通过对象的取值方法来获取数据,省略get

      如:user.getUsername(); --> user.username

    • mybatis中可以直接写username而不写user.,因为在parameterType中已经指定了类

  • 根据自定义的条件类查询 UserDao.findByName(QueryVo vo); 设QueryVo中有属性User

    • xml

      <select id="findByName" parameterType="com.whteway.domain.QueryVo" resultType="com.whteway.domain.User">
          select * from user where username like #{user.username}
      </select>
  • 获取保存数据的id

    • sql中有一条语句"select last_insert_id();"可以返回上一条保存的记录的id,使用下面的配置后mybatis会将查出来的id值存入作为参数的user对象中

      <insert ...>
          <selectKey keyProperty="id" keyColum="id" resultType="int" order="AFTER">
              select last_insertid()
          </selectKey>
          insert ...
      </insert>
  • 返回值与列名的对应

    • 返回值可以是基本类型,也可以是pojo(Plain Ordinary Java Object,普通java对象,即javaBeans)

    • 当实体类属性和查询结果列名不一致时会导致数据无法封装进resultType中

      • 可以在sql语句中通过起别名的方式解决

      • 在映射配置文件中配置查询结果的列名和实体类属性名的对应关系,设User中有uid,name,pwd属性

        <!-- 定义 -->
        <resultMap id="userMap" type="com.whteway.domain.User">
            <id property="uid" column="id"></id>
            <result property="name" column="username"></result>
            <result property="pwd" column="password"></result>
        </resultMap>
        
        <!-- 使用,替换resultType -->
        <select id="findAll" resultMap="userMap">
            select * from user
        </select>
  • 自定义dao实现类完成CRUD

    • 用session的不同方法执行sql语句,传入映射配置文件的namespace.SQL语句的id和sql需要的参数

      selectOne,selectList,insert,update

    public class UserDaoImpl implements UserDao{
        //只有第二步需要变化
        @Override
        public void saveUser(User user){
            //1.使用工厂创建SqlSession对象
            SqlSession session = factory.openSession();
            //2.使用session对象执行sql语句
            session.insert("com.whteway.dao.UserDao.saveUser", user);
            //3.释放资源
            session.close();
            //4.返回查询结果
            return;
        }
    }
  • session.selectList()源码分析

    //UserDaoImpl.java
    public List<User> findAll(){
        session.selectList("com.whteway.dao.UserDao.findAll");
    }
    //DefaultSqlSession.java
    public <E> List<E> selectList(String statement){
        this.selectList(statement, null);
    }
    public <E> List<E> selectList(String statement, Object parameter){
        this.selectList(statement, parameter, RowBounds.DEFAULT);
    }
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds){
        try{
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, wrapColeection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e){
    
        } finally {
            ErrorContext.instance().reset();
        }
    }
    //CachingExecutor.java
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException{
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException{
        ...
        return delegate.<E> query(ms, parameterObect, rowBounds, resultHandler, key, boundSql);
    }
    //BaseExecutor.java...
    //经过一系列的调用,会走到PreparedStatementHandler类中的一个query方法
    //PreparedStatementHandler.java
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException{
        //出现JDBC代码
        PreparedStatement ps = (PreparedStatement)statement;
        ps.execute();
        return resultSetHandler.<E> handleResultSets(ps);
    }
    //DefaultResultSetHandler.java
    public List<Object> handleResultSets(Statement stmt) throws SQLException{
        //封装结果集的代码
    }
  • 创建dao代理对象源码分析

  • properties标签

    配置properties,可以用标签配,也可以引用外部配置文件信息

    resource属性,用于指定配置文件的位置,按照类路径的写法来写,并且必须存在于类路径下(放在和SqlMapConfig.xml相同路径下)

    url属性,按照url的写法来写地址:协议://主机:端口 URI

    <configuration>
        <properties resource="jdbcConfig.properties">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/db"/>
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </properties>
        <!-- 在实际配置中用"${driver}"取值 -->
    </configuration>
  • typeAliases标签

    配置别名,只能配置实体类的别名,当指定了别名就不再区分大小写

    mybatis为基本类型和String配置了别名

    <typeAliases>
        <typeAlias type="com.whteway.domain.User" alias="user"></typeAlias>
    </typeAliases>
  • package标签

    在typeAliases中使用:为整个包中的类配置别名,当指定之后,该包下的实体类都会以类名注册别名

    在mappers中使用:指定dao接口所在的包,指定后,不需要再写resource或class了

    <typeAliases>
        <package name="com.whteway.domain"></package>
    </typeAliases>
    <mappers>
        <package name="com.whteway.dao"></package>
    </mappers>

动态SQL语句

  • if标签:满足条件则将标签内的语句拼接到sql语句后

    <!-- 根据条件查询,传入的条件可能是id,也可能是username -->
    <select id="findUserByCondition" resultMap="userMap" parameterType="user">
        select * from where 1=1
        <if test="username != null">and username=#{username}</if>
        <if test="id != null">and id=#{id}</if>
    </select>
  • where标签:sql后加 where 再将标签内的语句拼接到sql后

    <!-- if标签代码改进 -->
    <select id="findUserByCondition" resultMap="userMap" parameterType="user">
        select * from
        <where>
            <if test="username != null">and username=#{username}</if>
            <if test="id != null">and id=#{id}</if>
        </where>
    </select>
  • foreach标签

    循环将标签内部的语句和属性中的片段拼接到sql后

    open属性:开始片段;close属性:结束片段;separator属性:每个元素的分割符

    <!-- 根据queryvo中提供的id集合,查询用户信息 -->
    <select id="findByIds" resultMap="userMap" 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>
  • sql标签

    抽取重复的sql语句

    <!-- 定义,末尾不要加分号 -->
    <sql id="defaultUser">select * from user</sql>
    <!-- 使用 -->
    <select id="findUserByCondition" resultMap="userMap" parameterType="user">
        <include refid="defaultUser"></include>
        where id=#{id}
    </select>

相关推荐