【筆記】拉勾Java工程師高薪訓練營-第一階段 開源框架源碼解析-模組一 持久層框架涉及實現及MyBatis源碼分析-任務二:Mybatis基礎回顧及高級應用

以下筆記是我看完影片之後總結整理的,部分較為基礎的知識點也做了補充,如有問題歡迎溝通。

任務二:Mybatis基礎回顧及高級應用

2.1 Mybatis相關概念回顧

基於ORM(對象/關係資料庫映射)的半自動輕量級持久層框架

使用ORM的效果:操作實體類,等同於操作資料庫表

半自動的效果:支援訂製化SQL語句,擁有對SQL優化的權力

2.2 Mybatis環境搭建回顧

環境搭建包括以下六步:

1 添加Mybatis和MySql連接的依賴包到pom文件
2 創建user數據表
3 編寫User實體類
4 編寫映射文件Usermapper.xml
5 編寫核心文件SqlMapConfig.xml
6 編寫測試類

2.3 MybatisCRUD回顧

執行增刪改操作之後,要提交事務,相應操作才能生效

@Test
public void test1() throws IOException {
    // 1 Resources工具類,載入配置文件,將配置文件載入成位元組輸入流
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    // 2 解析了配置文件,並創建SqlSessionFactory工廠
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    // 3 生產SqlSession,openSession默認會開啟一個事務,但不會提交
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 4 sqlSession調用CRUD方法
    List<User> users = sqlSession.selectList("user.findAll");
    for (User user : users) {
        System.out.println(user);
    }
    // 5 手動提交事務,如果要自動提交事務,創建sqlSession時需要sqlSessionFactory.openSession(true)
    sqlSession.commit();
    sqlSession.close();
}

2.4 Mybatic相關配置文件回顧

#{ }調用的就是對象的get方法
UserMapper.xml文件內容回顧:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
    <!--
        namespace : 名稱空間,與id組成sql的唯一標識
        parameterType : 參數類型
        resultType : 返回值類型
    -->

    <!--查詢用戶-->
    <select id="findAll" resultType="com.jlgl.pojo.User">
        select * from user
    </select>

    <!--添加用戶-->
    <insert id="saveUser" parameterType="com.jlgl.pojo.User">
        insert into user values(#{id},#{username},#{password},#{birthday})
    </insert>

    <!--修改-->
    <update id="updateUser" parameterType="com.jlgl.pojo.User">
        update user set username=#{username} where id= #{id}
    </update>

    <!--刪除-->
    <!--只有一個參數傳入,且為基本類型時,形參可以隨便寫-->
    <delete id="deleteUser" parameterType="java.lang.Integer" >
        delete from user where id=#{id}
    </delete>


</mapper>

sqlMapConfig.xml文件內容回顧:

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--配置運行環境-->
    <environments default="development">
        <environment id="development">
            <!--當前事務交由JDBC管理-->
            <transactionManager type="JDBC"/>
            <!--當前使用Mybatis提供的連接池-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://120.48.8.213:3306/audio_2020"/>
                <property name="username" value="root"/>
                <property name="password" value="root1028*1"/>
            </dataSource>
        </environment>
    </environments>
    <!--引入映射配置文件-->
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
</configuration>

2.5 Mybatis相關API回顧

// 1 Resources工具類,載入配置文件,將配置文件載入成位元組輸入流
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2 解析了配置文件,並創建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3 生產SqlSession,openSession默認會開啟一個事務,但不會提交
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4 sqlSession調用CRUD方法
List<User> users = sqlSession.selectList("user.findAll");
for (User user : users) {
    System.out.println(user);
}
// 5 手動提交事務,如果要自動提交事務,創建sqlSession時需要sqlSessionFactory.openSession(true)
sqlSession.commit();
sqlSession.close();

2.6 Mybatic的dao層傳統開發方式

具體包括以下三步:

1 創建介面IUserDao,聲明CRUD方法
2 創建實現類UserDaoImpl,實現IUserDao介面,完成CRUD方法的重寫
3 在業務邏輯中調用對應CRUD方法

2.7 Mybatic的dao層代理開發方式

只用編寫持久層介面即可,不需要編寫介面實現類,是通過動態代理對象完成CRUD操作
(下面的截圖中有個錯誤,介面名和mapper的namespace裡面應該同名,都為UserDao或UserMapper)

1) Mapper.xm文件中的 namespace 與 mapper 介面的全限定名相同 
2) Mapper 介面方法名和 Mapper.xml中定義的每個 statement 的 id 相同
3) Mapper 介面方法的輸入參數類型和 mapper.xml 中定義的每個 sql 的 parameterType 的類型相同 
4) Mapper 介面方法的輸出參數類型和 mapper.xml 中定義的每個 sql 的 resultType 的類型相同

2.8 Mybatis的properties深入


載入外部配置文件,必須放在第一行

2.9 Mybatis的typeAliases深入

XXXMapper.xml的resultType和parameterType必須要寫全限定類名比較麻煩,可以給一個別名

如果pojo數量較大,採用package批量起別名,別名就是包下的類名,且別名不區分大小寫

別名的配置是寫在sqlMapConfig.xml文件里

<typeAliases>
    <!--給一個類起別名-->
    <!--<typeAlias type="com.jlgl.pojo.User" alias="user"></typeAlias>-->
    <!--批量起別名,別名就是包下的類名,切別名不區分大小寫-->
    <package name="com.jlgl.pojo"/>
</typeAliases>

對於常用類型,Mybatis已經起好了別名

2.10 Mybatis的動態SQL-if標籤回顧

直接使用if標籤,需要在sql語句的最後先加一個where 1=1,然後再跟標籤,否則有可能直接生成以下語句,會報錯

select * from user where;

但是where 1=1 的寫法雖然能行,但還是不太優雅,所以可以用標籤

<!--多條件組合查詢-->
<select id="findByCondition" parameterType="user" resultType="user">
    select * from user
    <where>
        <if test="id!=null">
            and id=#{id}
        </if>
        <if test="username !=null">
            and username=#{username}
        </if>
    </where>
</select>

2.11 Mybatis的動態SQL-foreach標籤回顧

多值查詢,就是比如要查多個id的user,然後將他們封裝進一個List裡面

<!--多值查詢-->
<select id="findByIds" parameterType="list" resultType="user">
    select * from user
    <where>
        <foreach collection="array" open="id in (" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

SQL片段抽取,由於” select * from user “這段內容頻繁出現,因此可以將這一片段抽取出來以便復用

<!--抽取SQL片段-->
<sql id="selectUser">
    select * from user
</sql>

<!--多值查詢-->
<select id="findByIds" parameterType="list" resultType="user">
    <include refid="selectUser"></include>
    <where>
        <foreach collection="array" open="id in (" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

2.12 Mybatis的複雜映射-一對一映射

一個訂單對應一個用戶,查詢一個訂單並查出對應用戶資訊

希望將User資訊封裝到Order的User屬性里,resultType=”order”直接這麼寫,無法實現查詢結果到Order的封裝,因為User對象封裝不進去,所以要使用resultMap

<select id="findOrderAndUser" resultType="order">
    SELECT * from orders o,user u WHERE o.uid=u.id
</select>

resultMap:手動配置實體屬性與表欄位的映射關係

resultMap各個屬性的含義:
id:給這個resultMap起一個名字,用於之後引用
type:被封裝數據所對應的全限定類名

result各個屬性的含義:
property:封裝對象的類屬性名
column:對應表的欄位名

association(resultMap裡面某個屬性類的相關資訊)各個屬性含義:
property:對應屬性名
javaType:屬性對應的Java類全限定類名

<resultMap id="orderMap" type="com.jlgl.pojo.Order">
    <result property="id" column="id"></result>
    <result property="ordertime" column="ordertime"></result>
    <result property="total" column="total"></result>
    <association property="user" javaType="com.jlgl.pojo.User">
        <result property="id" column="uid"></result>
        <result property="username" column="username"></result>
    </association>
</resultMap>

引入映射文件,也可以用,一定要和介面文件的包同名,如果不同名,就會報下面錯誤資訊:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.jlgl.dao.IUserMapper.findOrderAndUser

	at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:225)
	at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:48)
	at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:65)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58)
	at com.sun.proxy.$Proxy5.findOrderAndUser(Unknown Source)

2.13 Mybatis的複雜映射-一對多映射

因為封裝的是集合,就不能使用association,而是collection

user和order都有id,那麼在IUserMapper.xml裡面如何知道哪個id封裝給對應的id呢?需要做個區分column=”uid”

<resultMap id="userMap" type="com.jlgl.pojo.User">
    <result property="id" column="uid"></result>
    <result property="username" column="username"></result>
    <!--  因為封裝的是集合,就不能使用association,而是collection  -->
    <collection property="orderList" ofType="com.jlgl.pojo.Order">
        <result property="id" column="id"></result>
        <result property="ordertime" column="ordertime"></result>
        <result property="total" column="total"></result>
        <result property="uid" column="uid"></result>
    </collection>
</resultMap>

2.14 Mybatis的複雜映射-多對多映射

用戶角色模型,一個用戶有多個角色,一個角色被多個用戶使用,查詢需求是查詢所有用戶的同時查到關聯的所有角色

user表
role表
user_role表,用戶角色中間表,至少兩個欄位,這兩個欄位作為聯合主鍵,也作為外鍵,分別指向另外兩張表的主鍵

2.15 Mybatis註解CRUD回顧

使用註解開發,無需再編寫配置文件

@Insert::實現新增
@Update:實現更新
@Delete:實現刪除
@Select:實現ັ查詢
@Result:實現結果封裝
@Resultsғ:可以和@Result一起使用,封裝多個結果集
@One:實現一對一結果集封裝
@Many:實現一對多結果集封裝

{}的值要與實體的保持一致,這樣才能調用get方法

Junit的@Before的作用是在執行@Test之前執行一些邏輯

2.16 Mybatis註解一對一回顧

@One對應的是association

one=@One(select = “namespace.id”)要做的就是根據uid,再去查詢關聯的用戶
所以要在IUserMapper裡面寫好根據id查詢的語句

即用了註解,也使用了xml配置文件,就會引起衝突,所以只能用一個

// 查詢訂單的同時還查詢該訂單所屬用戶
@Results({
        @Result(property = "id", column = "id"),
        @Result(property = "ordertime", column = "ordertime"),
        @Result(property = "total", column = "total"),
        @Result(property = "user", column = "uid", javaType = User.class,
                one = @One(select = "com.jlgl.dao.IUserMapper.findUserById"))})
@Select("select * from orders")
List<Order> findOrderAndUser();

2.17 Mybatis註解一對多回顧

較為基礎,不再贅述

2.18 Mybatis註解多對多回顧

較為基礎,不再贅述

2.19 Mybatis快取概念回顧

快取就是記憶體中的一些數據,數據來自對資料庫查詢結果的快取,可以避免與資料庫的直接交互

一級快取的級別就是sqlSession,底層結構就是一個Map

二級快取的範圍跨sqlSession,是mapper級別(namespace級別)

2.20 Mybatis一級快取回顧

一級快取默認是開啟的

public void firstLevelCache(){
    // 使用debug,看控制台的日誌輸出
    // 第一次查詢
    User userById1 = userMapper.findUserById(1);
    // 第二次查詢
    User userById2 = userMapper.findUserById(1);
    System.out.println(userById1 == userById2);
}

聊聊MyBatis快取機制 – 美團技術團隊

結論1:第一次查詢,若快取中沒有,則會從資料庫查詢,然後存到快取中
結論2:做增刪改操作,並進行了事務提交,就會刷新一級快取
結論3:sqlSession.clearCache()也可以直接清空以及快取,從而直接從資料庫查詢

2.21 Mybatis一級快取原理與源碼分析

看源碼之前,要帶著這幾個問題:
問題1:一級快取究竟是什麼
問題2:什麼時候被創建
問題3:一級快取的工作流程是什麼

分析源碼的時候要先找到入口,這裡就是SqlSession
翻閱源碼的時候,要注意類與類之間的關係,比如從SqlSession介面裡面的clearCache,我們要找到對應的實現類,

問題1:一級快取究竟是什麼
答案:快取就是一個HashMap對象
能夠看到private Map<Object, Object> cache = new HashMap();,所以快取就是一個HashMap對象

private Map<Object, Object> cache = new HashMap();

問題2:什麼時候被創建
答案:
Executor就是執行器,所以一般應該在這裡創建

CacheKey cacheKey = new CacheKey();
// 其實就是statementId
cacheKey.update(ms.getId());
// 分頁參數
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 底層的sql語句
cacheKey.update(boundSql.getSql());

// 還有一個獲取environment的程式碼也值得關注一下
if (this.configuration.getEnvironment() != null) {
    cacheKey.update(this.configuration.getEnvironment().getId());
}

問題3:一級快取的工作流程是什麼
答案:Executor的query方法裡面具體創建、查詢了一級快取

list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

2.22 Mybatis二級快取回顧

二級快取需要配置才會開啟,一級快取是默認開啟的

配置需要兩步:

1 sqlMapConfig.xml裡面配置
2 在xxxMapper.xml或介面配置開啟二級快取
<!--開啟二級快取-->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings> 
// 開啟二級快取
@CacheNamespace 
public interface IUserMapper {
}

//如果要禁用快取,要求每次都從資料庫查詢
@Options(useCache = false)
@Select("select * from user where id=#{id}")
public User findUserById(Integer id);

2.23 Mybatis利用redis實現二級快取

利用redis實現二級快取需要三步:

1 啟動redis
2 導入Mybatis實現的redisCache或自己編寫redisCache
3 連接redis
4 更換註解裡面的implemention
5 執行程式碼

PerpetualCache這個類就是Mybatis默認實現快取的類
不管是PerpetualCache,還是我們要自定義快取類,都要實現Cache介面

@CacheNamespace(implementation = PerpetualCache.class)可以指定實現類
public interface IUserMapper {
}

Mybatis自帶的二級快取,是為單伺服器工作的,無法實現分散式快取,所以我們要通過redis實現分散式二級快取,不過Mybatis已經實現好了

@CacheNamespace(implementation = RedisCache.class)
public interface IUserMapper {
}

2.24 Mybatis-redisCache源碼分析

看源碼之前,要帶著這幾個問題:
問題1:如何進行快取的存取
問題2:存儲使用的是怎樣的數據結構

redis.properties,且必須在resources文件夾下

通過程式碼可以看出,使用的是hset,即哈希表

public void putObject(final Object key, final Object value) {
    this.execute(new RedisCallback() 
    {
        public Object doWithRedis(Jedis jedis) {
        jedis.hset(RedisCache.this.id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));
return null;
}});
}

2.25 Mybatis插件機制介紹與原理

一般情況下,開源框架都會提供插件或其他形式的拓展點,供開發者自行拓展。這樣的好處是顯而易見的,一是增加了框架的靈活性開發者可以結合實際需求,對框架進行拓展,使其能夠更好的工作。

Mybatis的四大組件分別是:

Executor̵
StatementHandler̵
ParameterHandler̵
ResultSetHandler

插件就是通過對四大核心組件進行攔截,然後增強功能,而底層使用的就是動態代理


如果我們需要自定義插件,可以如此定義:

2.26 Mybatis插件機制-自定義Mybatis插件

自定義插件編寫步驟:

1 編寫程式碼
2 sqlMapConfig註冊插件
@Intercepts({
        // 可以配置多個
        @Signature(type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class})
})
public class Myplugin implements Interceptor {
    /**
     * 攔截方法:只要攔截的目標對象的目標方法被執行,就會執行intercept方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("對方法進行了增強");
        // 這句話就是讓原方法執行
        return invocation.proceed();
    }

    /**
     * 把當前的攔截器生成代理對象,存到攔截器中
     *
     * @param o
     * @return
     */
    @Override
    public Object plugin(Object o) {
        Object wrap = Plugin.wrap(o, this);
        return wrap;
    }

    /**
     * 獲取配置文件的參數
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("獲取到的配置文件的參數是:"+properties);
    }
}

sqlMapConfig註冊插件

<!--配置插件-->
<plugins>
    <plugin interceptor="com.jlgl.plugin.Myplugin">
        <property name="name" value="tom"/>
    </plugin>
</plugins>
控制台輸出:
獲取到的配置文件的參數是:{name=tom}
對方法進行了增強

2.27 Mybatis插件機制-plugin源碼分析

入口Plugin,通過implements InvocationHandler能夠看出,這個對象就是動態代理對象處理類,並一定是重寫了invoke方法

public class Plugin implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }
}

2.28 Mybatis插件機制-pageHelper

分頁插件使用需要三步:

1 導入pageHelper到pom
2 sqlMapConfig註冊插件,需要配置方言
3 編寫查詢程式碼

sqlMapConfig註冊插件,需要配置方言

<plugin interceptor="com.github.pagehelper">
    <!--因為不同資料庫的sql分頁語句不同,所以要配置對應的方言-->
    <property name="dialect" value="mysql"/>
</plugin>

Java查詢程式碼

@Test
public void pageHelperTest(){
    PageHelper.startPage(1,2);
    List<User> users = userMapper.selectUser();
    for (User user : users) {
        System.out.println(user);
    }
    
    // 可以查詢獲得很多分頁相關的數據
    PageInfo<User> userPageInfo = new PageInfo<>(users);
    userPageInfo.getFirstPage();
}

2.29 Mybatis插件機制-通用mapper

單表增刪改查的數據重複書寫沒有意義,可以直接使用通用mapper,只要寫好實體類就可以了(當然實體類的生成也不需要手寫,也有對應的自動化生成方法)

注意Example和Criteria的操作

此部分較為基礎,不再贅述