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);
}
}
练习
「资料来源:饥人谷」