Skip to content

Latest commit

 

History

History
501 lines (334 loc) · 17.4 KB

File metadata and controls

501 lines (334 loc) · 17.4 KB

手写自己的MyBatis框架

核心内容

1、存放参数和结果映射关系、存放 SQL 语句,我们需要定义一个配置类;

2、执行对数据库的操作,处理参数和结果集的映射,创建和释放资源,我们需要定 义一个执行器;

3、有了这个执行器以后,我们不能直接调用它,而是定义一个给应用层使用的 API, 它可以根据 SQL 的 id 找到 SQL 语句,交给执行器执行;

4、直接使用 id 查找 SQL 语句太麻烦了,我们干脆把存放 SQL 的命名空间定义成一 个接口,把 SQL 的 id 定义成方法,这样只要调用接口方法就可以找到要执行的 SQL。这 个时候我们需要引入一个代理类

操作流程

image-20220227134401636

1、定义接口 Mapper 和方法,用来调用数据库操作。 Mapper 接口操作数据库需要通过代理类。

2、定义配置类对象 Configuration。

3、定义应用层的 API SqlSession。它有一个 getMapper()方法,我们会从配置类 Configuration 里面使用 Proxy.newProxyInatance()拿到一个代理对象 MapperProxy。

4、有了代理对象 MapperProxy 之后,我们调用接口的任意方法,就是调用代理对 象的 invoke()方法。

5、代理对象 MapperProxy 的 invoke()方法调用了 SqlSession 的 selectOne()。

6、SqlSession 只是一个 API,还不是真正的 SQL 执行者,所以接下来会调用执行

5 器 Executor 的 query()方法。

7、执行器 Executor 的 query()方法里面就是对 JDBC 底层的 Statement 的封装, 最终实现对数据库的操作,和结果的返回。

Mybatis-V1版本

public class MSqlSession {
    // 配置类
    private MConfiguration configuration;
    // 执行器
    private MExecutor executor;

    public MSqlSession(MConfiguration configuration, MExecutor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    /**
     * 调用Executor执行单条执行
     * @param statementId
     * @param parameter
     * @param <T>
     * @return
     */
    public <T> T selectOne(String statementId, Object parameter) {
        String sql = MConfiguration.sqlMappings.getString(statementId);
        return executor.query(sql, parameter);
    }

    /**
     * 获取一个代理对象
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class clazz) {
        return configuration.getMapper(clazz, this);
    }
}
public class MConfiguration {

    public final static ResourceBundle sqlMappings;

    static {
        sqlMappings = ResourceBundle.getBundle("mssql");
    }
    /**
     * 返回接口的代理对象
     * @param clazz
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class clazz, MSqlSession sqlSession) {
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                new Class[]{clazz},
                new MMapperProxy(sqlSession));
    }
}
public class MMapperProxy implements InvocationHandler {
    private MSqlSession sqlSession;

    public MMapperProxy(MSqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String statementId = method.getDeclaringClass().getName() + "." + method.getName();
        return sqlSession.selectOne(statementId, args[0]);
    }
}
public interface BlogMapper {
    /**
     * 根据主键查询文章
     * @param bid
     * @return
     */
    public Blog selectBlogById(Integer bid);

}
public class MExecutor {
    public <T> T query(String sql, Object parameter) {
        Connection conn = null;
        Statement stmt = null;
        Blog blog = new Blog();

        try {
            // 注册 JDBC 驱动
            Class.forName("com.mysql.cj.jdbc.Driver");

            // 打开连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/gp-mybatis", "root", "admin@123456");

            // 执行查询
            stmt = conn.createStatement();

            ResultSet rs = stmt.executeQuery(String.format(sql, parameter));

            // 获取结果集
            while (rs.next()) {
                Integer bid = rs.getInt("bid");
                String name = rs.getString("name");
                Integer authorId = rs.getInt("author_id");
                blog.setAuthorId(authorId);
                blog.setBid(bid);
                blog.setName(name);
            }
            System.out.println(blog);

            rs.close();
            stmt.close();
            conn.close();
        } catch (SQLException se) {
            se.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (stmt != null) stmt.close();
            } catch (SQLException se2) {
            }
            try {
                if (conn != null) conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            }
        }
        return (T)blog;
    }
}

Mybatis-V2版本

1、配置文件

创建了全局配置文件 mybatis.properties,存放 SQL 连接信息、缓存开关、插件地 址、Mapper 接口地址。 全局配置文件在 Configuration 配置类的构造器中解析。

2、参数处理

创建 ParameterHandler,调用 psmt 的 set 方法。propertie 文件中 SQL 语句的%d 占位符改成?。

3、结果集处理

创建 ResultSetHandler,在其中创建 pojo 对象,获取 ResultSet 值,通过反射给 pojo 对象赋值。 实 体 类 的 转 换 关 系 通 过 @Entity 注 解 ( 保 存 在 MapperRegistry 中 ) , 从 MapperProxyFactory(构造函数)——MapperProxy 一路传递到 ResultSetHandler 中。

4、语句执行处理 创建 StatementHandler,在 Executor 中调用。封装获取连接的方法。 执行查询前调用 ParameterHandler,执行查询后调用 ResultSetHandler。

5、支持注解配置

SQL 定义了一个@Select 注解,加在方法上。 在 Configuration 构 造 函 数 中 的 parsingClass() 中 解 析 , 保 存 在mappedStatements 中(一个 HashMap)。

注意:在 properties 中和注解上同时配置 SQL 语句,注解会覆盖 properties。 properties 中对表达三个对象的映射关系并不适合,所以暂时用--分隔。注意类型 前面不能有空格。

6、支持查询缓存

定 义 了 一 个 CachingExecutor , 当 全 局 配 置 中 的 cacheEnabled=true 时 , Configuration 的 newExecutor()方法会对 SimpleExecutor 进行装饰,返回被装饰过的 Executor。CachingExecutor 中用 HashMap 维护缓存。

在 DefaultSqlSession 调用 Executor 时,会先走到装饰器 CachingExecutor。

定义了一个 CacheKey 用于计算缓存 Key,主要根据 SQL 语句和参数计算。

7、支持插件

定义了一个@Intercepts 注解,目前还只能拦截 Executor 的方法,所以属性只要配 置方法名称。

定义 Interceptor 接口,是所有自定义插件必须实现的接口。

定义 InterceptorChain 容器,用来存放解析过的拦截器。在 Configuration 中创建 Executor 的时候,会调用它的 pluginAll()方法,对 Executor 循环代理。

定义 Invocation 包装类,用于在执行完自定义插件逻辑后调用 Executor 的原方法。

定义 Plugin 代理类,提供了一个 wrap()方法用于产生代理对象。当 Executor 被代 理后,所有的方法都会走到 invoke()方法中,进一步调用自定义插件的 intercept()方法

Mybatis常见问题

1、MyBatis 解决了什么问题? 或:为什么要用 MyBatis? 或:MyBatis 的核心特性?

1)资源管理(底层对象封装和支持数据源)

2)结果集自动映射

3)SQL 与代码分离,集中管理

4)参数映射和动态 SQL

5)其他:缓存、插件等

2、MyBatis 编程式开发中的核心对象及其作用?

SqlSessionFactoryBuilder 创建工厂类

SqlSessionFactory 创建会话

SqlSession 提供操作接口

MapperProxy 代理

Mapper 接口后,用于找到 SQL

3、Java 类型和数据库类型怎么实现相互映射?

通过 TypeHandler,例如 Java 类型中的 String 要保存成 varchar,就会自动调 用相应的 Handler。如果没有系统自带的 TypeHandler,也可以自定义。

4、SIMPLE/REUSE/BATCH 三种执行器的区别?

SimpleExecutor 使用后直接关闭 Statement:closeStatement(s

// SimpleExecutor.java
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    	Statement stmt = null;
    	//中间省略……
    } finally {
    	closeStatement(stmt);
    }
}

ReuseExecutor 放在缓存中,可复用:PrepareStatement——getStatement()

// ReuseExecutor.Java
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    //中间省略……
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    //中间省略……
    }
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //中间省略……
    if (hasStatementFor(sql)) {
        
       	stmt = getStatement(sql);
      	//中间省略……
    }
}
    
private Statement getStatement(String s) {
    return statementMap.get(s);
}

BatchExecutor 支持复用且可以批量执行 update(),通过 ps.addBatch()实现 handler.batch(stmt);

// BatchExecutor.Java
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    //中间省略……
    final Statement stmt;
        //中间省略……
        stmt = statementList.get(last);
        //中间省略……
        statementList.add(stmt);
        batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
}

7、MyBatis 一级缓存与二级缓存的区别?

一级缓存:在同一个会话(SqlSession)中共享,默认开启,维护在 BaseExecutor 中。

二级缓存:在同一个 namespace 共享,需要在 Mapper.xml 中开启,维护在 CachingExecutor 中。

8、MyBaits 支持哪些数据源类型?

UNPOOLED:不带连接池的数据源。 POOLED : 带 连 接 池 的 数 据 源 , 在 PooledDataSource 中 维 护 PooledConnection。 PooledDataSource 的 getConnection()方法流程图:

image-20220227153552235

JNDI:使用容器的数据源,比如 Tomcat 配置了 C3P0。

自定义数据源:实现 DataSourceFactory 接口,返回一个 DataSource。

当 MyBatis 集成到 Spring 中的时候,使用 Spring 的数据源。

9、关联查询的延迟加载是怎么实现的?

动态代理(JAVASSIST、CGLIB),在创建实体类对象时进行代理,在调用代理 对象的相关方法时触发二次查询。

10、MyBatis 翻页的几种方式和区别?

逻辑翻页:通过 RowBounds 对象。

物理翻页:通过改写 SQL 语句,可用插件拦截 Executor 实现

11、怎么解决表字段变化引起的 MBG 文件变化的问题?

Mapper 继 承 : 自 动 生 成 的 部 分 不 变 , 创 建 接 口 继 承 原 接 口 , 创 建 MapperExt.xml。在继承接口和 MapperExt.xml 中修改。

通用 Mapper:提供支持泛型的通用 Mapper 接口,传入对象类型。

13、解析全局配置文件的时候,做了什么?

创建 Configuration,设置 Configuration 解析 Mapper.xml,设置 MappedStatement

14、没有实现类,MyBatis 的方法是怎么执行的?

MapperProxy 代理,代理类的 invoke()方法中调用了 SqlSession.selectOne()

15、接口方法和映射器的 statement id 是怎么绑定起来的? (怎么根据接口方法拿到 SQL 语句的?)

MappedStatement 对象中存储了 statement 和 SQL 的映射关系

16、四大对象是什么时候创建的?

Executor:openSession()

StatementHandler、ResultsetHandler、ParameterHandler:

执行 SQL 时,在 SimpleExecutor 的 doQuery()中创建

17、ObjectFactory 的 create()方法什么时候被调用?

第一次被调用,创建 DefaultResultHandler 的时候:

DefaultResultSetHandler 类中:

handleResultSet new DefaultResultHandle

image-20220227154112039

18、MyBatis 哪些地方用到了代理模式?

接口查找 SQL:MapperProxy

日志输出:ConnectionLogger、StatementLogger

连接池:PooledDataSource 管理的 PooledConnection

延迟加载:ProxyFactory(JAVASSIST、CGLIB)

插件:Plugin

Spring 集成:SqlSessionTemplate 的内部类 SqlSessionInter

19、MyBatis 主要的执行流程?涉及到哪些对象?

image-20220227154211696

20、MyBatis 插件怎么编写和使用?原理是什么?(画图)

使用:继承 Interceptor 接口,加上注解,在 mybatis-config.xml 中配置

原理:动态代理,责任链模式,使用 Plugin 创建代理对象

在被拦截对象的方法调用的时候,先走到 Plugin 的 invoke()方法,再走到 Interceptor 实现类的 intercept()方法,最后通过 Invocation.proceed()方法调用被 拦截对象的原方法

21、JDK 动态代理,代理能不能被代理?

22、MyBatis 集成到 Spring 的原理是什么?

SqlSessionTemplate 中有内部类SqlSessionInterceptor对DefaultSqlSession 进行代理;

MapperFactoryBean 继 承 了 SqlSessionDaoSupport 获 取 SqlSessionTemplate;

接口注册到 IOC 容器中的 beanClass 是 MapperFactoryBean。

23、DefaulSqlSession 和 SqlSessionTemplate 的区别是什么?

1)为什么 SqlSessionTemplate 是线程安全的?

其内部类 SqlSessionInterceptor 的 invoke()方法中的 getSqlSession()方法:

如果当前线程已经有存在的 SqlSession 对象,会在 ThreadLocal 的容器中拿到 SqlSessionHolder,获取 DefaultSqlSession。

如果没有,则会 new 一个 SqlSession,并且绑定到 SqlSessionHolder,放到 ThreadLocal 中。

SqlSessionTemplate 中在同一个事务中使用同一个 SqlSession。

调用 closeSqlSession()关闭会话时,如果存在事务,减少 holder 的引用计数。否 则直接关闭 SqlSessio

2)在编程式的开发中,有什么方法保证 SqlSession 的线程安全?

SqlSessionManager 同时实现了 SqlSessionFactory、SqlSession 接口,通过 ThreadLocal 容器维护 SqlSession

常见问题

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

在 MySQL 的插入数据使用自增 ID 这种场景,有的时候我们需要获得最新的自增 ID, 比如获取最新的用户 ID。常见的做法是执行一次查询,max 或者 order by 倒序获取最 大的 ID(低效、存在并发问题)。在 MyBatis 里面还有一种更简单的方式:

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

<insert id="insert" parameterType="com.gupaoedu.domain.Blog">
    insert into blog (bid, name, author_id)
    values (#{bid,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{author,jdbcType=CHAR})
</insert>
blogService.addBlog(blog);
System.out.println(blog.getBid());

如何实现模糊查询 LIKE

1、字符串拼接

在 Java 代码中拼接%%(比如 name = "%" + name + "%"; ),直接 LIKE。因为没 有预编译,存在 SQL 注入的风险,不推荐使用。

2、CONCAT(推荐)

<when test="empName != null and empName != ''">
	AND e.emp_name LIKE CONCAT(CONCAT('%', #{emp_name, jdbcType=VARCHAR}),'%')
</when>

3、bind 标签

<select id="getEmpList_bind" resultType="empResultMap" parameterType="Employee">
    <bind name="pattern1" value="'%' + empName + '%'" />
    <bind name="pattern2" value="'%' + email + '%'" />
    SELECT * FROM tbl_emp
    <where>
        <if test="empId != null">
        	emp_id = #{empId,jdbcType=INTEGER},
        </if>
        <if test="empName != null and empName != ''">
        	AND emp_name LIKE #{pattern1}
        </if>
        <if test="email != null and email != ''">
       	 	AND email LIKE #{pattern2}
        </if>
    </where>
    ORDER BY emp_id
</select>

什么时候用#{},什么时候用${}?

在 Mapper.xml 里面配置传入参数,有两种写法:#{} 、${}。作为 OGNL表达式,都可以实现参数的替换。这两种方式的区别在哪里?什么时候应该用哪一种? 要搞清楚这个问题,我们要先来说一下 PrepareStatement 和 Statement 的区别。

1、两个都是接口,PrepareStatement 是继承自 Statement 的;

2、Statement 处理静态 SQL,PreparedStatement 主要用于执行带参数的语句;

3、PreparedStatement 的 addBatch()方法一次性发送多个查询给数据库;

4、PS 相似 SQL 只编译一次(对语句进行了缓存,相当于一个函数),比如语句相 同参数不同,可以减少编译次数;

5、PS 可以防止 SQL 注入。

MyBatis 任意语句的默认值:PREPARED 这两个符号的解析方式是不一样的:

#会解析为 Prepared Statement 的参数标记符,参数部分用?代替。传入的参数会 经过类型检查和安全检查

$只会做字符串替换