38MyBatis

MyBatis是什么

  • 一个ORM框架
  • 国内基本上都在用
  • 简单、方便

为什么我们需要ORM

如果使用JDBC查询:

感受到痛苦了吗?

ORM(Object Relationship Mapping)

  • 对象关系映射
    • 自动完成对象到数据库的映射
  • Association
    • 自动装配对象

一个ORM框架能够实现数据库到Java中对象的映射

MyBatis从高层往下看关系,避免陷入代码和配置的深渊中

  • 在应用的眼中,数据库就是一个URL,连接就是通过URL进行的

  • MyBatis是什么呢,工作再JVM中的一小段程序,它所做的事情是什么呢?

    • 通过调用底层的JDBC去跟数据库打交道,然后把它变成Object

    • 如果JVM把Object丢给MyBatis,MyBatis把Object拆成SQL通过JDBC传给数据库

从零开始MyBatis

引入依赖包

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.9</version>
</dependency>

配置MyBatis

String resource = "db/mybatis/config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

resources目录

不是代码之外的所有东西称为资源,资源放在/main/resources/目录下

/main/resources/db/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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
        <mapper resource="db/mybatis/MyMapper.xml"/>
  </mappers>
</configuration>

配置日志框架,可以极大地提高排查问题的效率

config.xml

<configuration>
  <settings>
    <setting name="logImpl" value="LOG4J"/>
  </settings>
</configuration>

引入log4j依赖

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
            <scope>compile</scope>
        </dependency>
  • log4j.properties
# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

有关Mapper的一切

  • Mapper:接口由MyBatis动态代理
    • 优点:方便
    • 缺点:SQL复杂的时候不够方便
  • Mapper :用XML编写复杂SQL
    • 优点:可以方便地使用MyBatis的强大功能
    • 缺点:SQL和代码分离

MyMapper.xml

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="MyMapper">
    <select id="getUserByPage" parameterType="map" resultType="User">
        select id,name,tel,address
        from user
        <if test="username != null">
            where name = #{username}
        </if>
        limit #{offset},#{limit}
    </select>

    <select id="countUser" parameterType="string" resultType="int">
        select count(*)
        from user
        <if test="username != null">
            where name = #{username}
        </if>
    </select>

    <insert id="batchInsertUsers" parameterType="map">
        insert into user (name,tel,address)
        values
        <foreach item="user" collection="users" separator=",">
            (#{user.name},#{user.tel},#{user.address})
        </foreach>
    </insert>

    <update id="updateUser" parameterType="User">
        update user
        <set>
            <if test="name != null">name=#{name},</if>
            <if test="tel != null">tel=#{tel},</if>
            <if test="address != null">address=#{address},</if>
        </set>
        where id=#{id}
    </update>

    <delete id="deleteUserById" parameterType="int">
        delete from user where id=#{id}
    </delete>

    <select id="selectUserById" parameterType="int" resultType="User">
        select id,name,tel,address
        from user
        where id = #{id}
    </select>

    <select id="getInnerJoinOrders" resultMap="Order">
        select  o.id as order_id,u.id as user_id,
                u.name as user_name,g.name as goods_name,
                g.price as goods_price,o.goods_num as goods_num,
                (o.goods_price * o.goods_num) as total_price
        from "ORDER" o
            inner join GOODS g on o.GOODS_ID = g.ID
            inner join USER u on o.USER_ID = u.ID
        where g.name is not null and u.name is not null
    </select>

    <resultMap id="Order" type="com.github.hcsp.mybatis.entity.Order">
        <result property="id" column="order_id"/>
        <result property="totalPrice" column="total_price"/>
        <association property="user" javaType="User">
            <result property="id" column="user_id"/>
            <result property="name" column="user_name"/>
        </association>
        <association property="goods" javaType="com.github.hcsp.mybatis.entity.Goods">
            <result property="name" column="goods_name"/>
            <result property="price" column="goods_price"/>
        </association>
    </resultMap>
</mapper>
  • parameterType
    • 参数的#{}和${}
    • select * from User where name = '${name}'
      • ${}是直接替换掉,String类型需要用引号括起来
    • `select * from User where name = #{name}
    • 参数的#{}和${}不同
      • {}防SQL注入,${}不防

    • 参数是按照Java Bean约定读取的:getter/setter
  • resultType
    • typeAlias 设置别名
    • 写参数是按照Java Bean约定的:getter/setter
  • Association

动态SQL—MyBatis的灵魂

if

如果 test="title != null"条件为true 则包含AND title like #{title}

否则不包含

<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

坑:当传入的是null的时候

<select id="findUserLikeName"
     resultType="User">
  SELECT * FROM User
  WHERE
  <if test="name != null">
    name like #{name}
  </if>
</select>

得到:SELECT * FROM User WHERE

解决:

<select id="findUserLikeName"
     resultType="User">
  SELECT * FROM User
  <if test="name != null">
    WHERE
    name like #{name}
  </if>
</select>

choose

choose就是if else

<when test="name == wangwu">判断条件中name和wangwu 是变量,加单引号说明跟常量比较<when test="name == 'wangwu'">

  • 如果传进来的name是wangwu则执行WHERE name = 'wangwu',否则执行 WHERE name = 'zhangsan'

foreach

item="item" 声明了#{item} 语句中的占位符

<select id="selectIds" resultType="User">
  SELECT *
  FROM User
  WHERE ID in
  <where>
    <foreach item="item" index="index" collection="ids"
        open="ID in (" separator="," close=")" nullable="true">
          #{item}
    </foreach>
  </where>
</select>

open="ID in (" separator="," close=")" nullable="true">

在拼接字符串之前加一个东西,之后加一个东西

script

动态SQL放到注解里面,实用性不强,比较啰嗦

分页limit

    <select id="getUserByPage" parameterType="map" resultType="User">
        select id,name,tel,address
        from user
        <if test="username != null">
            where name = #{username}
        </if>
        limit #{offset},#{limit}
    </select>

limit #{offset},#{limit} offset=100,limit=5 从第100条开始取5条数据

limit 5等价于limit 0,5 offset不写等价于0 取前5条数据

练习代码

/**
 * 根据传入的参数查找用户名为username的用户,返回分页后的结果。
 *
 * @param username 传入的用户名
 * @param pageSize 分页搜索,每页显示的条数
 * @param pageNum  分页的页码,从1开始
 * @return 查找结果,若username为null,则返回所有用户的列表
 */
public Pagination<User> getUserByPage(String username, int pageSize, int pageNum) {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        Map<String, Object> map = new HashMap<>();
        map.put("username", username);
        map.put("offset", (pageNum - 1) * pageSize);
        map.put("limit", pageSize);
        List<User> users = session.selectList("MyMapper.getUserByPage", map);

        int count = session.selectOne("MyMapper.countUser", username);

        int totalPage = (count % pageSize == 0) ? count / pageSize : count / pageSize + 1;

        return Pagination.pageOf(users, pageSize, pageNum, totalPage);
    }
}

map.put("offset", (pageNum - 1) * pageSize); 第2页 每页8条 (2-1) * 8

map.put("limit", pageSize); 每页8条

limit #{offset},#{limit} 从第8条开始取8条数据

统计有多少用户

    <select id="countUser" parameterType="string" resultType="int">
        select count(*)
        from user
        <if test="username != null">
            where name = #{username}
        </if>
    </select>

总页数

  • 不能整除则页码+1
int totalPage = (count % pageSize == 0) ? count / pageSize : count / pageSize + 1;

用MyBatis的foreach批量插入User

    <insert id="batchInsertUsers" parameterType="map">
        insert into user (name,tel,address)
        values
        <foreach item="user" collection="users" separator=",">
            (#{user.name},#{user.tel},#{user.address})
        </foreach>
    </insert>

Mybatis association 结果映射

/** 一个订单 */
public class Order {
    private Integer id;
    /** 订单中的用户 */
    private User user;
    /** 订单中的商品 */
    private Goods goods;
    /** 订单中的总成交金额 */
    private BigDecimal totalPrice;
}

Order里面还有User、Goods对象

返回SQL的列都会被按照Order指定的结果映射方式变成一个一个的对象

    <select id="getInnerJoinOrders" resultMap="Order">
        select  o.id as order_id,u.id as user_id,
                u.name as user_name,g.name as goods_name,
                g.price as goods_price,o.goods_num as goods_num,
                (o.goods_price * o.goods_num) as total_price
        from "ORDER" o
            inner join GOODS g on o.GOODS_ID = g.ID
            inner join USER u on o.USER_ID = u.ID
        where g.name is not null and u.name is not null
    </select>

    <resultMap id="Order" type="com.github.hcsp.mybatis.entity.Order">
        <result property="id" column="order_id"/>
        <result property="totalPrice" column="total_price"/>
        <association property="user" javaType="User">
            <result property="id" column="user_id"/>
            <result property="name" column="user_name"/>
        </association>
        <association property="goods" javaType="com.github.hcsp.mybatis.entity.Goods">
            <result property="name" column="goods_name"/>
            <result property="price" column="goods_price"/>
        </association>
    </resultMap>

MyBatis中的设计模式浅析

SqlSessionFactory 工厂模式

xxxFactory 生产xxx的工厂

可以根据传入的参数动态的产生产品

SqlSessionFactory是工厂,SqlSession是产品

public interface SqlSession extends Closeable{}

产品和工厂是接口是抽象的没有实现的,这种模式是抽象工厂模式

抽象工厂模式

好处

使用它的产品的时候,不需要关心它的实现是什么,工厂可以动态的根据具体的情况提供特定的产品实现

SqlSessionFactory是生产猫的一个机器,你想要一个可以抓老鼠的猫,你不关心这个猫是黑猫、白猫 是什么锚,只关心有个猫可以抓老鼠,然后这个生产猫的机器动态的根据现在什么猫更适合抓老鼠,动态的把符合这个接口约定的产品给生产出来传递给你

MyBatis源码

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

单例模式

在JVM中只存在一个实例

构造器变成了private,意味着不能通过常规的new的方式新建它,怎么还能活的这个类的实例呢?

只能调用它的instance()

MyBatis源码

当前线程对应的ErrorContext实例为null的时候就新建一个,返回给它,否则的话就直接返回,保证线程中ErrorContext只有一个实例

public class ErrorContext {

  private ErrorContext() {
  }

  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }
}

自己写单例 MySingleton

public class MySingleton {
    private static final MySingleton INSTANCE = new MySingleton();

    private MySingleton() {
    }

    public static MySingleton getInstance() {
        return INSTANCE;
    }
}

如果不想类加载的时候就创建

public class MySingleton {
    private static MySingleton INSTANCE = null;

    private MySingleton() {
    }

    public static MySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new MySingleton();
        }
        return INSTANCE;
    }
}

这个写法有多线程隐患

代理模式

    public void deleteUserById(Integer id) {
        try (SqlSession session = sqlSessionFactory.openSession(true)) {
            UserMapper userMapper = session.getMapper(UserMapper.class);
            userMapper.deleteUserById(1);
        }
    }

    interface UserMapper {
        @Delete("delete from user where id = #{id}")
        void deleteUserById(@Param("id") Integer id);
    }

调用了似乎什么都没做的接口的方法的时候,jdk实现了动态代理转发给了其中的MapperProxy代理

MyBatis源码

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

装饰器模式

典型的例子是Cache

有一个东西对外提供一些功能,想要增加一些功能怎么办呢?要改这个东西的具体实现吗?

  • 可能不需要,在外面再包一层,使得外界的人调用这个方法的时候,外面的这个东西可以动态的决定,是不是调用原先被包裹的方法

有一个数据库查询的方法,你想为它实现带缓存的功能,需不需要去改这个方法呢?

  • 不需要,只需要在外面包一层带缓存功能的实现,使得外界请求来的时候都首先去缓存里面查,如果有就返回,缓存没有再去数据库执行真正的查询

MyBatis源码CachingExecutor.java

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

模板模式

父类提供一些模板方法的实现,子类通过覆盖这些模板方法实现丰富多彩的功能

父类 BaseExecutor.java

父类定义了一系列的模板操作,返回的doUpdate是抽象的,由子类决定具体去做什么

protected abstract int doUpdate(MappedStatement ms, Object parameter)
    throws SQLException;

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

子类 BatchExecutor

public class BatchExecutor extends BaseExecutor {

  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;

  private final List<Statement> statementList = new ArrayList<>();
  private final List<BatchResult> batchResultList = new ArrayList<>();
  private String currentSql;
  private MappedStatement currentStatement;

  public BatchExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);//fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    //fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

例子:

https://pdai.tech/md/dev-spec/pattern/17_template.html

适配器模式

MyBatis 支持很多日志框架

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j (deprecated since 3.5.9)
  • JDK logging

这些框架每个都有不同的接口,MyBatis 难道要为每一个接口都设计一种相应的实现吗?

  • 不需要,使用适配器模式,使用统一的日志接口,为每一种日志框架实现一个适配器,使得上层调用统一的日志接口,这也是封装的好处

MyBatis源码

public interface Log {

  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

Jdk14Log的实现

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.logging.jdk14;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.ibatis.logging.Log;

/**
 * @author Clinton Begin
 */
public class Jdk14LoggingImpl implements Log {

  private final Logger log;

  public Jdk14LoggingImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isLoggable(Level.FINE);
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isLoggable(Level.FINER);
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(Level.SEVERE, s, e);
  }

  @Override
  public void error(String s) {
    log.log(Level.SEVERE, s);
  }

  @Override
  public void debug(String s) {
    log.log(Level.FINE, s);
  }

  @Override
  public void trace(String s) {
    log.log(Level.FINER, s);
  }

  @Override
  public void warn(String s) {
    log.log(Level.WARNING, s);
  }

}

练习

Mybatis

「资料来源:饥人谷」

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注