MyBatis(一)Mybatis应用分析与最佳实践

Mybatis应用分析与最佳实践

1. 为什么使用Mybatis

JDBC连接数据库需要五步:

首先在pom.xml中引入MySQL驱动的依赖
第一步:Class.forName注册驱动
第二步:获取一个Connection对象
第三步:创建一个Statement对象
第四步:execute()方法执行SQL。返回一个ResultSet结果集
第五步:通过ResultSet获取数据,给POJO的属性赋值
最后关闭数据库相关的资源,包括ResultSet,Statement,Connection。

Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("");
        Statement st = conn.createStatement();
        ResultSet rs = st.executeQuery("SQL");

        while(rs.next()) {
            int id = rs.getInt("b_id");
        }

缺点:

  1. 代码重复
  2. 资源管理
  3. 结果集处理
  4. SQL耦合

DbUtils和Spring JDBC

Apache 2003年发布Commons DbUtils工具类,优化对数据库的操作
Spring对原生的JDBC进行封装

  1. 代码重复:spring提供模板方法JDBCTemp,里面封装了各种各样的execute、query、update方法.
    JDBCTemp:它是JDBC的核心包的中间类。简化了JDBC的使用,可以避免常见异常。封装了JDBC的核心流程,是线程安全的。
  2. 对于结果集的处理,SpringJDBC提供了RowMapper接口,可以把结果集转换成Java对象,它作为JDBCTemplate的参数使用。例如要把查询的对象换成Attachement对象,可以针对一个Attachement创建一个RowMapper对象:
class AttachmentRowRoMapper implements RowMapper{

    @Override
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        Attachment attachment = new Attachment();
        attachment.setAttachmentId(rs.getLong("is"));
        return attachment;
    }
}

如果项目的表比较多的时候,创建一个支持泛型的BaseRowMapper实现RowMapper接口,通过反射的方法自动获取所有的属性,把表字段赋值到属性。

class BaseRowMapper<T> implements RowMapper<T> {

    private Class<?> targetClazz;
    private HashMap<String, Field> fieldMap;

    public BaseRowMapper(Class<?> targetClazz) {
        this.targetClazz = targetClazz;
        fieldMap = new HashMap<>();
        Field[] fields = this.targetClazz.getDeclaredFields();
        for (Field field : fields) {
            fieldMap.put(field.getName(), field);
        }
    }

    @Override
    public T mapRow(ResultSet rs, int rowNum) throws SQLException {
        T obj = null;
        try {
            obj = (T) this.targetClazz.newInstance();
            ResultSetMetaData metaData = rs.getMetaData();
            int columnLength = metaData.getColumnCount();
            String columnName = null;

            for (int i = 0; i < columnLength; i++) {
                columnName = metaData.getColumnName(i);
                Class fieldClazz = fieldMap.get(camel(columnName)).getType();
                Field field = fieldMap.get(camel(columnName));
                field.setAccessible(true);

                if (fieldClazz == int.class || fieldClazz == Integer.class) {
                    field.set(obj,rs.getInt(columnName));
                } else if (fieldClazz == boolean.class || fieldClazz == Boolean.class){
                }
                field.setAccessible(false);
            }
        } catch (Exception e) {
        }
        return obj;
    }

//驼峰转换,将下划线转变成驼峰
    private  String camel(String str){
        Pattern pattern = Pattern.compile("_(\\w)");
        Matcher matcher = pattern.matcher(str);
        StringBuffer sb = new StringBuffer(str);
        if (matcher.find()) {
            sb = new StringBuffer();
            matcher.appendReplacement(sb,matcher.group(1).toUpperCase());
            matcher.appendTail(sb);
        } else {
            return sb.toString();
        }
        return camel(sb.toString());
    }
}

JDBC测试:

list= jdbcTemplate.query("select* from tbl_emp", new BaseRowMapper(Employee.class));

DbUtils和Spring Jdbc,这两个对JDBC做了轻量级封装的框架(工具类),解决了:

  1. 对操作数据的增删改查的方法进行封装。
  2. 无论是QueryRunner(DbUtils核心方法)还是JDBCTemplate,都可以传入一个数据源初始化,也就是资源管理这一部分的事情,可以交给专门的数据源组件去做,不用手段创建和关闭。
  3. 可以帮组我们映射结果集,无论是映射成List、Map还是POJO。

这两个工具存在一些不足:

  1. SQL语句都是写在代码里的,存在硬编码问题。
  2. 参数只能按照固定位置的顺序传入,它是通过占位符去替换的,不能传入对象和Map,不能自动映射。
  3. 在方法里面,可以把结果集映射成实体类,但是不能直接把实体类映射成数据录的记录(没有自动生产SQL的功能)。
  4. 查询没有缓存的功能,性能不够好。

Hibernate

ORM的全拼:Object Relational Mapping,也就是对象与关系的映射。对象是程序里的对象,关系是它与数据库里面的数据关系。

Configuration configuration = new Configuration();
        configuration.configure();
        
        //创建Session工厂
        SessionFactory factory = configuration.buildSessionFactory();
        //创建Session
        Session session = factory.openSession();
        //获取事务对象
        Transaction transaction = session.getTransaction();
        //开启事务
        transaction.begin();
        //数据操作
        session.save("");
        //提交事务
        transaction.commit();
        //关闭Session
        session.close();

Hibernate特性

  1. 根据数据库方言自动生成SQL,移植性好;
  2. 自动管理连接资源(支持数据源);
  3. 实现了对象和关系型数据的完全映射,操作对象就像操作数据库一样;
  4. 提供了缓存功能机制。

但是Hibernate在业务复杂的项目中使用存在一些问题:

  1. 使用get(), update(), save()对象的这种方法,实际操作的所有字段,没有办法指定部分字段(不灵活)
  2. 自动生成SQL的方式,如果要基于SQL去做一些优化的话,是非常困难的
  3. 不支持动态SQL,比如分表中的表明、条件、参数变化,无法根据条件自动生成SQL。

MyBatis

“半自动化”的ORM框架,不会自动生成全部的SQL语句,主要解决的是SQL和对象的映射关系。SQL和代码是分离的。

2. MyBatis实际使用案例

编程式使用

MyBatis有两种配置文件:全局式配置文件和映射器配置文件
创建一个全局配置文件,这里面是对MyBatis的核心行为的控制,例如:myBatis-config.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">
<configuration>

    <properties resource="db.properties"></properties>
    <settings>
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!-- 控制全局缓存(二级缓存),默认 true-->
        <setting name="cacheEnabled" value="true"/>

        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->
        <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
        <!--
                <setting name="localCacheScope" value="STATEMENT"/>
        -->
        <setting name="localCacheScope" value="SESSION"/>
    </settings>

    <typeAliases>
        <typeAlias alias="blog" type="com.gupaoedu.domain.Blog" />
    </typeAliases>

<!--    <typeHandlers>
        <typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
    </typeHandlers>-->

    <!-- 对象工厂 -->
<!--    <objectFactory type="com.gupaoedu.objectfactory.GPObjectFactory">
        <property name="gupao" value="666"/>
    </objectFactory>-->

<!--    <plugins>
        <plugin interceptor="com.gupaoedu.interceptor.SQLInterceptor">
            <property name="gupao" value="betterme" />
        </plugin>
        <plugin interceptor="com.gupaoedu.interceptor.MyPageInterceptor">
        </plugin>
    </plugins>-->

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
            <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="BlogMapper.xml"/>
        <mapper resource="BlogMapperExt.xml"/>
    </mappers>

</configuration>

db.properties:数据源一般交给spring管理

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
jdbc.username=root
jdbc.password=123456

映射器配置文件:Mapper.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.gupaoedu.mapper.BlogMapper">
    <!-- 声明这个namespace使用二级缓存 -->
<!--    <cache/>-->

    <!-- 使用Redis作为二级缓存 -->
<!--
    <cache type="org.mybatis.caches.redis.RedisCache"
           eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
-->
        <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
               size="1024"
               eviction="LRU"
               flushInterval="120000"
               readOnly="false"/>

    <resultMap id="BaseResultMap" type="blog">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
<!--
        <result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.gupaoedu.type.MyTypeHandler"/>
-->
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="author_id" property="authorId" jdbcType="INTEGER"/>
    </resultMap>

    <!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
    <resultMap id="BlogWithAuthorResultMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <!-- 联合查询,将author的属性映射到ResultMap -->
        <association property="author" javaType="com.gupaoedu.domain.Author">
            <id column="author_id" property="authorId"/>
            <result column="author_name" property="authorName"/>
        </association>
    </resultMap>

    <!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
    <resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <association property="author" javaType="com.gupaoedu.domain.Author"
                     column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
    </resultMap>

    <!--  查询文章带评论的结果(一对多) -->
    <resultMap id="BlogWithCommentMap" type="com.gupaoedu.domain.associate.BlogAndComment" extends="BaseResultMap" >
        <collection property="comment" ofType="com.gupaoedu.domain.Comment">
            <id column="comment_id" property="commentId" />
            <result column="content" property="content" />
        </collection>
    </resultMap>

    <!--  按作者查询文章评论的结果(多对多) -->
    <resultMap id="AuthorWithBlogMap" type="com.gupaoedu.domain.associate.AuthorAndBlog" >
        <id column="author_id" property="authorId" jdbcType="INTEGER"/>
        <result column="author_name" property="authorName" jdbcType="VARCHAR"/>
        <collection property="blog" ofType="com.gupaoedu.domain.associate.BlogAndComment">
            <id column="bid" property="bid" />
            <result column="name" property="name" />
            <result column="author_id" property="authorId" />
            <collection property="comment" ofType="com.gupaoedu.domain.Comment">
                <id column="comment_id" property="commentId" />
                <result column="content" property="content" />
            </collection>
        </collection>
    </resultMap>

    <!-- ===============以上是resultMap定义================= -->

    <select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" >
        select * from blog where bid = #{bid}
    </select>

    <!-- $只能用在自定义类型和map上 -->
    <select id="selectBlogByBean"  parameterType="blog" resultType="blog" >
        select bid, name, author_id authorId from blog where name = '${name}'
    </select>

    <select id="selectBlogList" resultMap="BaseResultMap" >
        select bid, name, author_id authorId from blog
    </select>

    <!-- 动态SQL where 和 if  -->
    <select id="selectBlogListIf" parameterType="blog" resultMap="BaseResultMap" >
        select bid, name, author_id authorId from blog
        <where>
            <if test="bid != null">
                AND bid = #{bid}
            </if>
            <if test="name != null and name != ''">
                AND name LIKE '%${name}%'
            </if>
            <if test="authorId != null">
                AND author_id = #{authorId}
            </if>
        </where>
    </select>

    <!-- 动态SQL choose -->
    <select id="selectBlogListChoose" parameterType="blog" resultMap="BaseResultMap" >
        select bid, name, author_id authorId from blog
        <where>
            <choose>
                <when test="bid !=null">
                    bid = #{bid, jdbcType=INTEGER}
                </when>
                <when test="name != null and name != ''">
                    AND name LIKE CONCAT(CONCAT('%', #{name, jdbcType=VARCHAR}),'%')
                </when>
                <when test="authorId != null ">
                    AND author_id = #{authorId, jdbcType=INTEGER}
                </when>
                <otherwise>
                </otherwise>
            </choose>
        </where>
    </select>

    <!-- 动态SQL set -->
    <update id="updateByPrimaryKey" parameterType="blog">
        update blog
        <set>
            <if test="name != null">
                name = #{name,jdbcType=VARCHAR},
            </if>
            <if test="authorId != null">
                author_id = #{authorId,jdbcType=CHAR},
            </if>
        </set>
        where bid = #{bid,jdbcType=INTEGER}
    </update>

    <!-- 动态SQL trim -->
    <insert id="insertBlog" parameterType="blog">
    insert into blog
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="bid != null">
                bid,
            </if>
            <if test="name != null">
                name,
            </if>
            <if test="authorId != null">
                author_id,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="bid != null">
                #{bid,jdbcType=INTEGER},
            </if>
            <if test="name != null">
                #{name,jdbcType=VARCHAR},
                <!-- #{name,jdbcType=VARCHAR,typeHandler=com.gupaoedu.type.MyTypeHandler}, -->
            </if>
            <if test="authorId != null">
                #{authorId,jdbcType=INTEGER},
            </if>
        </trim>
    </insert>

    <!-- foreach 动态SQL 批量插入 -->
    <insert id="insertBlogList" parameterType="java.util.List">
        insert into blog (bid, name, author_id)
        values
        <foreach collection="list" item="blogs" index="index"  separator=",">
            ( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
        </foreach>
    </insert>

    <!-- foreach 动态SQL 批量删除 -->
    <delete id="deleteByList" parameterType="java.util.List">
        delete from blog where bid in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item.bid,jdbcType=INTEGER}
        </foreach>
    </delete>

    <!-- foreach 动态SQL 批量更新-->
    <update id="updateBlogList">
        update blog set
        name =
        <foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
            when #{blogs.bid} then #{blogs.name}
        </foreach>
        ,author_id =
        <foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
            when #{blogs.bid} then #{blogs.authorId}
        </foreach>
        where bid in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item.bid,jdbcType=INTEGER}
        </foreach>
    </update>

    <!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
    <select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>

    <!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
    <select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>

    <!-- 嵌套查询 -->
    <select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
        select author_id authorId, author_name authorName
        from author where author_id = #{authorId}
    </select>

    <!-- 根据文章查询评论,一对多 -->
    <select id="selectBlogWithCommentById" resultMap="BlogWithCommentMap" >
        select b.bid, b.name, b.author_id authorId, c.comment_id commentId, c.content
        from blog b, comment c
        where b.bid = c.bid
        and b.bid = #{bid}
    </select>

    <!-- 根据作者文章评论,多对多 -->
    <select id="selectAuthorWithBlog" resultMap="AuthorWithBlogMap" >
        select b.bid, b.name, a.author_id authorId, a.author_name authorName, c.comment_id commentId, c.content
        from blog b, author a, comment c
        where b.author_id = a.author_id and b.bid = c.bid
    </select>

    <!-- 手动实现翻页,没有对应方法,取消注释会报错 -->
<!--    <select id="selectBlogPage" parameterType="map" resultMap="BaseResultMap">
        select * from blog limit #{curIndex} , #{pageSize}
    </select>-->

    <!-- 自动生成的Example -->
    <sql id="Base_Column_List">
        bid, name, author_id
    </sql>
    <sql id="Example_Where_Clause">
        <where>
            <foreach collection="oredCriteria" item="criteria" separator="or">
                <if test="criteria.valid">
                    <trim prefix="(" prefixOverrides="and" suffix=")">
                        <foreach collection="criteria.criteria" item="criterion">
                            <choose>
                                <when test="criterion.noValue">
                                    and ${criterion.condition}
                                </when>
                                <when test="criterion.singleValue">
                                    and ${criterion.condition} #{criterion.value}
                                </when>
                                <when test="criterion.betweenValue">
                                    and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                                </when>
                                <when test="criterion.listValue">
                                    and ${criterion.condition}
                                    <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
                                        #{listItem}
                                    </foreach>
                                </when>
                            </choose>
                        </foreach>
                    </trim>
                </if>
            </foreach>
        </where>
    </sql>

    <select id="selectByExample" parameterType="com.gupaoedu.domain.BlogExample" resultMap="BaseResultMap">
        select
        <if test="distinct">
            distinct
        </if>
        'true' as QUERYID,
        <include refid="Base_Column_List" />
        from blog
        <if test="_parameter != null">
            <include refid="Example_Where_Clause" />
        </if>
        <if test="orderByClause != null">
            order by ${orderByClause}
        </if>
    </select>
</mapper>

MyBatis的目的是简化JDBC的操作,它提供了一个可以执行CRUD的对象:SqlSession接口。可以把它理解为跟数据库的一个连接,或者一次会话。

SqlSession的创建:
因为数据源、Mybatis核心行为的控制(例如是否开启缓存)都是在全局配置文件中,所以必须根据全局配置文件创建,这里不是直接new出来的,而是通过一个工厂类创建的。

 String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            Blog blog = (Blog) session.selectOne("com.gupaoedu.mapper.BlogMapper.selectBlogById", 1);
            System.out.println(blog);
        } finally {
            session.close();
        }

最后通过SqlSession接口上的方法传入Statement Id来执行Mapper映射器中的SQL。这样的调用方式解决了重复代码、资源管理、SQL耦合、结果集映射这几个问题。
不过仍然存在一些问题:
1. Statement Id是硬编码,维护不方便。
2. 不能在编译时进行类型检查,如果namespace或者Statement Id出错,只在运行时报错。

优化:定义一个Mapper接口的方式,这个接口全路径必须跟Mapper.xml里面的namespace对应起开,方法也要跟Statement Id 一一对应。

 String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
     try{
       BlogMapper mapper = session.getMapper(BlogMapper.class);
        Blog blog = mapper.selectBlogByName("MySQL从入门到改行");
     } finally{
     session.close;
     }
     

MyBatis的核心特性(Mybatis可以解决哪些主要问题):

  1. 使用连接池对连接进行管理
  2. SQL和代码分离,集中管理
  3. 结果集映射
  4. 参数映射和动态SQL
  5. 重复SQL的提取
  6. 缓存管理
  7. 插件机制

核心对象的生命周期

在编程式的demo中,我们发现Mybatis的几个核心对象:SQLSessionFactoryBuiler、SQLSessionFactory、SqlSession、Mapper。在一些分布式的应用里面,多线程高并发的场景中如果要写出高效的代码,必须了解这4个对象的生命周期。

1. SqlSessionFactoryBuiler

SqlSessionFactoryBuiler是用来创建SqlSessionFactory的。而SqlSessionFactory只需要一个,所有只要创建了一个SqlSessionFactory,它的使命就完成了,也就没有存在的意义,所以它的生命周期只存在于方法的内部。

2. SqlSessFactory(单例)

SqlSessionFactory是用来创建Session的,每次应用程序访问数据库都需要创建一个会话。因为我们一直友会话的需要,所以SqlSessionFactory应该存在于整个生命周期中(作用域是应用作用域)。创建SqlSession只需要一个实例来做这件事就行了。否则会产生混乱和浪费资源。所以采用单例模式。

3. SqlSession

SqlSession是一个会话,不是线程安全的,不能在线程间共享。所以在请求开始的时候创建一个SqlSession对象,在请求结束或方法执行完毕的时候要及时关闭它。

4. Mapper

Mapper(实际上是一个代理对象)是从SqlSession中获取的。它的做用是发送SQL来操作数据库的数据,它应该在一个SqlSession事务方法之内。

3.核心配置解读

中文官网:https://mybatis.org/mybatis-3/zh/index.html

全局配置文件mybatis-config.xml

configuration

configuration是整个配置文件的根标签,实际上也对应着MyBatis里面最重要的配置类Configuration。

properties

第一个一级标签,用来配置参数信息,比如最常见的数据库连接信息。可以用resource引用应用里面的相对路径,也可以用url指定本地服务器或者网络的绝对路径

settings

settings里面是MyBatis的一些核心配置。

typeAliases

typeAlias是类型的别名,只要用来简化类名全路径的拼写。例如

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
</typeAliases>

MyBatis里面有系统预定好的类型别名,在TypeAliasRegistry中。

typeHandlers

由于Java类型和数据库的JDBC类型不是一一对应的(比如String与varchar、char、text),所以我们把java对象转换为数据库的值,把数据库的值转换成Java对象,需要经过一定的转换这两个方向的转换需要用到TypeHandler。
MyBatis内置了很多的TypeHandler(type包下)它们全部注册在TypeHandlerRegistry中,它们都继承了抽象类BaseTypeHandler,泛型就是要处理的Java数据类型。

自定义TypeHandler时候,需要继承BaseTypeHandler。有4个抽象方法必须实现。

从Java类型到JDBC类型
setNonNullParameter:设置非空参数
从JDBC类型到Java类型
getNullableResult:获取空结果集(根据列名),一般调用这个
getNullableResult:获取空结果集(根据下标值)
getNullableResult:存储过程用的

举例:数据库保存使用逗号分类的字符串:例如“1,2,4”,而返回给程序的时候是整形数组{1,2,4}。

第一步:自定义typeHandler

@Override
    public void setNonNullParameter(PreparedStatement ps, int i, Integer[] parameter, JdbcType jdbcType) throws SQLException {
        // 设置 Integer[] 类型的参数的时候调用,Java类型到JDBC类型
        // 注意只有在字段上添加typeHandler属性才会生效
        StringJoiner sj = new StringJoiner(",");
        for (int j = 0; j < parameter.length; j++) {
            sj.add(parameter[i].toString());
        }
        ps.setString(i,sj.toString());
    }

    @Override
    public Integer[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String str = rs.getString(columnName);
        String[] parameter = str.split(",");
        Integer [] param = new Integer[parameter.length];
        for (int i = 0; i < parameter.length; i++) {
            param[i] = Integer.parseInt(parameter[i]);
        }
        return param;
    }

第二步:注册TypeHandler

<typeHandlers>
        <typeHandler handler="com.gupaoedu.type.MyTypeHandler"></typeHandler>
    </typeHandlers>

第三步:配置Mapper.xml

<resultMap id="BaseResultMap" type="blog">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.gupaoedu.type.MyTypeHandler"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="author_id" property="authorId" jdbcType="INTEGER"/>
    </resultMap>

在插入时实现自定义TypeHandler

<insert id="insert" parameterType="com.lhl.mybatis.beans.Test" useGeneratedKeys="true" keyProperty="id">
    insert into test(id, nums, name)
    values (#{id,jdbcType=INTEGER},#{nums,jdbcType=INTEGER},#{name,jdbcType=VARCHAR,javaType=java.lang.String})
  </insert>

等价于

<insert id="insert" parameterType="com.lhl.mybatis.beans.Test" useGeneratedKeys="true" keyProperty="id">
    insert into test(id, nums, name)
    values (#{id,jdbcType=INTEGER},#{nums,jdbcType=INTEGER},#{name,typeHandler=com.lhl.mybatis.typehandler.CustomTypeHandler})
  </insert>
objectFactory

当我们把数据库返回的结果集转换成实体类的时候,需要创建对象的实例。由于不知道处理的类型是什么,有那些属性,所以不能使用new的方式去创建,只能通过反射来创建。
在MyBatis中,它提供了一个工厂类接口,叫做ObjectFactory,专门用来创建对象的实例

plugins

插件是MyBatis的一个很强大的机制,跟许多其他的框架一样,MyBatis预留了插件的接口,让Mybatis更容易扩展。
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler(getParameterObject, setParamters)
  • ResultSetHandler(handleResultSets, handleOutPutParameters)
  • StatementHandler(prepare, parameterize, batch, update, query)

SqlSession是对外提供的接口。而SQLSession的CRUD的方法都是由Executor完成的
Execute是真正的执行器角色,也是实际的SQL逻辑执行的开始。而MyBatis中又把SQL的执行按照过程细分为三个对象:parameterHandler处理参数,StatementHandler执行SQL,ResultSetHandler处理结果集。

environments、environment

environments标签用来管理数据库的环境,比如开发环境、测试环境、生产环境的数据库。
一个environment标签就是一个数据源,代表一个数据库,这里有两个关键的标签,一个是事务管理器,一个是数据源

transactionManager

JDBC:使用Connection对象的commit(), rollback(), close()管理事务。
MANAGED:把事务交给容器管理,比如JBOSS,Weblogic。
如果是Spring+Mybatis,没有必要配置,因为在applicationContext.xml里面配置数据源和事务,覆盖MyBatis的配置

dataSource

数据源,在Java里面,它是对数据库连接的一个抽象。
一般的数据源都会包括连接池管理的功能,所以dataSource又叫做连接池,或带连接池功能的数据源。
一般连接池都会有初始连接数、最大连接数、回收时间等参数,提供提前创建、资源重用、数量控制、超时管理等功能。

MyBatis自带两种数据源:UNPOOLED和POOLED。也可以配置成其他数据库,比如C3P0,、Hikari等。

mappers

配置的是映射器,也就是Mapper.xml的路径。这里配置的目的是让MyBatis在启动的时候去扫描这些映射器,创建映射关系。
4种指定Mapper文件的方式:

  1. 使用相对于类路径的资源引用(resource)
 <mappers>
        <mapper resource="BlogMapper.xml"/>
    </mappers>
  1. 使用完全限定资源定位符(绝对路径)
<mappers>
        <mapper url="file:src/main/resources/BlogMapper.xml"/>
    </mappers>
  1. 使用映射器接口实现类的完全限定类名
 <mappers>
        <mapper class="com.gupaoedu.mapper.BlogMapper"/>
    </mappers>
  1. 将包内的映射器接口实现全部注册为映射器(最常用)
<mappers>
        <mapper class="com.gupaoedu.mapper"/>
    </mappers>

映射器配置文件 Mapper.xml

映射器最主要的配置了SQL语句,也解决了参数映射和结果集映射的问题一共八个标签

cache

给定命名空间的缓存配置(是否开启二级缓存)

cache-ref

其他命名空间的缓存配置的引用。

resultMap

用来描述如何从数据库结果集来加载对象

sql

可被其他语句引用的可重用语句块

CRUD标签

insert、update、delete、select

4. MyBatis最佳实践

动态SQL

MyBatis动态SQL的动态标签主要有4类:if,choose(when, otherwise),trim(where, set),foreach

if——需要判断的时候,条件写在test中
<!-- 动态SQL where 和 if  -->
    <select id="selectBlogListIf" parameterType="blog" resultMap="BaseResultMap" >
        select bid, name, author_id authorId from blog
        <where>
            <if test="bid != null">
                AND bid = #{bid}
            </if>
            <if test="name != null and name != ''">
                AND name LIKE '%${name}%'
            </if>
            <if test="authorId != null">
                AND author_id = #{authorId}
            </if>
        </where>
    </select>
choose(when, otherwise)——需要选择一个条件的时候
<!-- 动态SQL choose -->
    <select id="selectBlogListChoose" parameterType="blog" resultMap="BaseResultMap" >
        select bid, name, author_id authorId from blog
        <where>
            <choose>
                <when test="bid !=null">
                    bid = #{bid, jdbcType=INTEGER}
                </when>
                <when test="name != null and name != ''">
                    AND name LIKE CONCAT(CONCAT('%', #{name, jdbcType=VARCHAR}),'%')
                </when>
                <when test="authorId != null ">
                    AND author_id = #{authorId, jdbcType=INTEGER}
                </when>
                <otherwise>
                </otherwise>
            </choose>
        </where>
    </select>
trim(where, set)——需要去掉where、and、逗号之类的符号的时候
<!-- 动态SQL trim -->
    <insert id="insertBlog" parameterType="blog">
    insert into blog
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="bid != null">
                bid,
            </if>
            <if test="name != null">
                name,
            </if>
            <if test="authorId != null">
                author_id,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="bid != null">
                #{bid,jdbcType=INTEGER},
            </if>
            <if test="name != null">
                #{name,jdbcType=VARCHAR},
                <!-- #{name,jdbcType=VARCHAR,typeHandler=com.gupaoedu.type.MyTypeHandler}, -->
            </if>
            <if test="authorId != null">
                #{authorId,jdbcType=INTEGER},
            </if>
        </trim>
    </insert>
foreach——需要遍历集合的时候
 <!-- foreach 动态SQL 批量插入 -->
    <insert id="insertBlogList" parameterType="java.util.List">
        insert into blog (bid, name, author_id)
        values
        <foreach collection="list" item="blogs" index="index"  separator=",">
            ( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
        </foreach>
    </insert>

    <!-- foreach 动态SQL 批量删除 -->
    <delete id="deleteByList" parameterType="java.util.List">
        delete from blog where bid in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item.bid,jdbcType=INTEGER}
        </foreach>
    </delete>

    <!-- foreach 动态SQL 批量更新-->
    <update id="updateBlogList">
        update blog set
        name =
        <foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
            when #{blogs.bid} then #{blogs.name}
        </foreach>
        ,author_id =
        <foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
            when #{blogs.bid} then #{blogs.authorId}
        </foreach>
        where bid in
        <foreach collection="list" item="item" open="(" separator="," close=")">
            #{item.bid,jdbcType=INTEGER}
        </foreach>
    </update>
Batch Execute

MyBatis的动态标签批量操作存在一定的缺点,比如数据量特别大的时候,拼接出的SQL语句过大。
MySQL的服务端对于接受的数据包有大小限制,max_allowed_packet默认是4M,需要修改默认配置或者手动地控制条数,才可以解决。

在全局配置文件中,可以配置默认的Executor的类型(默认是SIMPLE)。还有一种BatchExecutor。

<setting name="defaultExecutorType" value="BATCH" />

也可以在创建会话的时候指定执行器的类型

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

BaseExecutor的三个子类:BatchExecutor、ResultExecutor、SimpleExecutor
三种类型执行器的区别(通过doUpdate()方法对比)

  1. SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。(默认)
  2. ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement,而是放置在Map内。供下一次使用。简而言之,就是重复使用statement对象。
  3. BatchExecutor:执行update(没有select,JDBC批量处理不支持select),将所有的sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

executeUpdate()是一个语句访问一次数据库,executeBatch()是一批语句访问一次数据库(具体一批发送多少条SQL跟服务端的max_allowed_packet有关)。

BatchExecutor底层是对JDBC ps.addBatch()和ps.executeBatch()的封装。

嵌套(关联)查询/ N+1 / 延迟加载

association:是用于一对一和多对一
collectin:用于一对多

一对一的关联查询的两种配置方式:
在Blog(文章)里面包好一个Author(作者)对象。

  1. 嵌套结果
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
    <resultMap id="BlogWithAuthorResultMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <!-- 联合查询,将author的属性映射到ResultMap -->
        <association property="author" javaType="com.gupaoedu.domain.Author">
            <id column="author_id" property="authorId"/>
            <result column="author_name" property="authorName"/>
        </association>
    </resultMap>


<!-- 根据文章查询作者,一对一,嵌套结果,无N+1问题 -->
    <select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>
  1. 嵌套查询
<!-- 另一种联合查询(一对一)的实现,但是这种方式有“N+1”的问题 -->
    <resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <association property="author" javaType="com.gupaoedu.domain.Author"
                     column="author_id" select="selectAuthor"/> <!-- selectAuthor 定义在下面-->
    </resultMap>

<!-- 根据文章查询作者,一对一,嵌套查询,存在N+1问题,可通过开启延迟加载解决 -->
    <select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>

<!-- 嵌套查询 -->
    <select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
        select author_id authorId, author_name authorName
        from author where author_id = #{authorId}
    </select>

嵌套查询是分两次查询的,当我们查询了Blog信息后,会再发送一条SQL语句到数据库查询作者信息。 存在N+1问题。
在MyBatis中可以通过开启延迟加载的开关来解决这个问题。
在Settings标签里面可以配置

<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->

开启延时加载后,调用blog.getAuthor()以及默认的equals、clone、hashCode、toString 方法时才会发起第二次查询。

触发延迟加载的方法可以通过lazyLoadTriggerMethods标签配置。如果开启了aggressiveLazyLoading=true,除了关联查询的方法和系统方法,其他方法也会触发第二次查询,比如blog.getName()。

翻页

翻页分为逻辑翻页和物理翻页

逻辑翻页(假翻页):把所有的数据查出来,在内存中删选数据。
物料翻页(真翻页):MySQL使用limit语句。Oracle使用rownum语句。SQLServer使用top语句。

通用Mapper

MyBatis常见问题

用注解还是xml配置

常用注解:@Insert、@Select、@Update、@Delete、@Param、@Results、@Result

注解的缺点是SQL无法集中管理,复杂的SQL很难配置,所有在业务复杂的项目中只使用XML配置的形式,业务简单的项目中可以使用注解和XML混合的形式。

Mapper接口无法注入或Invalid bound statument (not found)

遇到这中情况需要检查一下地方:
1. 扫面配置,xml文件和Mapper接口有没有被扫描到
2. namespace的值可接口全类名一致。
3. 检查对应的sql语句ID是否存在。

怎么获取插入的最新自动生成的ID

在MySQL的插入数据使用自增ID的场景,在添加的时候配置useGeneratedKeys=“true” keyProperty=“id”
例如:

 <insert id="insertOnePlayer" parameterType="Player" useGeneratedKeys="true" keyProperty="id">
     		insert into tb_player (id, playName, playNo,team, height)
     		values (
                 #{id,jdbcType=INTEGER},
                 #{playName,jdbcType=VARCHAR},
                 #{playNo,jdbcType=INTEGER},
                 #{team,jdbcType=VARCHAR},
                 #{height,jdbcType=DECIMAL}
     		)
     	</insert>

在insert成功后,MyBatis会将插入的值自动绑定到插入的对象的Id属性中,我们用getId就能获取最新的Id。

blogService.addBlog(blog);
System.out.printIn(blog.getId());
什么时候用#{}, 什么时候用个${}

在Mapper.xml里面配置传入参数,有两种写法:#{}、${}。作为OGNL表达式都可以实现参数的替换。这两种方式的区别在哪里,首选要清楚PrepareStatement和Statement的区别。

  1. 两个都是接口,PrepareStatement是继承自Statement的。
  2. Statement处理静态的SQL。PrepareStatement主要用于执行带参数的语句。
  3. PrepareStatement的addBatch()方法一次性发送多个查询给数据库。
  4. PS相似SQL只编译一次(对语句进行了缓存,相当于一个函数),例如语句相同参数不同,可以减少编译次数。
  5. PS可以防止SQL注入。

MyBatis任意语句的默认值:PREPARED

这两个符号的解析方式是不一样的
“#”会解析为PrepareStatement的参数标记符,参数部分用“?”代替,传入的参数会经过类型检查和安全检查。

“$”只会做字符串的替换

#和$的区别:
1. 是否防止SQL注入:美元符号方式不会对符号进行转义,不能防止SQL注入
2. 性能:¥方式没有预编译,不会缓存。

结论:
1. 能用#的地方都用#
2. 常量的替换,例如排序条件中的字段明后才能,不用加单引号,可以使用¥

如何实现模糊查询like
  1. 字符串拼接,在Java代码中拼接%%,直接like。因为没有预编译,存在SQL注入风险,不推荐使用。
  2. CONCAT(推荐)
<when test="empName != null amd empName != "">
AND e.emp_name LIKE CONCAT(CONCAT('%', #{emp_name, jdbcType=VARCHAR}),'%')
</when>
  1. bind标签
<select id="getEmpList_bind" resultType="empResultMap" parameterType="Employee">
        <bind name="pattern1" value="'%' + empName + '%'" />
        SELECT * from tbl_emp
        <where>
            <if test="empId != null">
                emp_id = #{empId, jdbcType= INTEGER}
            </if>
        </where>
        ORDER BY emp_id
    </select>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值