1. 程式人生 > 實用技巧 >時序預測 02 - 異常檢測 同比(chain) 方法的一些實踐、調參總結

時序預測 02 - 異常檢測 同比(chain) 方法的一些實踐、調參總結

Mybatis入門

HelloWorld

準備資料庫

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test01` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `test01`;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (  
`id` int(11) NOT NULL AUTO_INCREMENT,  
`username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,  
`address` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,  
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*Data for the table `user` */
insert  into `user`(`id`,`username`,`address`) values (1,'javaboy123','www.javaboy.org'),(3,'javaboy','spring.javaboy.org'),(4,'張三','深圳'),(5,'李四','廣州'),(6,'王五','北京');

建立Maven工程,匯入依賴:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.17</version>
</dependency>

建立UserMapper

<?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="org.javaboy.mymapper">
    
</mapper>

建立一個新的 mapper ,需要首先給它取一個 namespace,這相當於是一個分隔符,因為我們在專案中,會存在很多個 Mapper,每一個 Mapper 中都會定義相應的增刪改查方法,為了避免方法衝突,也為了便於管理,每一個 Mapper 都有自己的 namespace,而且這個 namespace 不可以重複。

接下來,在 Mapper 中,定義一個簡單的查詢方法,根據 id 查詢一個使用者:

<?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="org.javaboy.mymapper">
    <select id="getUserById" resultType="org.javaboy.mybatis.model.User">
        select * from user where id=#{id};
    </select>
</mapper>

在 Mapper 中,首先定義一個 select ,id 表示查詢方法的唯一識別符號,resultType 定義了返回值的型別。在 select 節點中,定義查詢 SQL,#{id},表示這個位置用來接收外部傳進來的引數。

定義的 User 實體類,如下:

public class User {
    private Integer id;
    private String username;
    private String address;
		//get  set  toString
}

接下來,建立 MyBatis 配置檔案,如果是第一次使用,可以參考官網,拷貝一下配置檔案的頭資訊,如果需要多次使用這個配置檔案,可以在 IDEA 中建立該配置檔案的模板:

<?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="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="123"/>
            </dataSource>
        </environment>
    </environments>
    
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
    
</configuration>

在這個配置檔案中,我們只需要配置 environments 和 mapper 即可,environment 就是 MyBatis 所連線的資料庫的環境資訊,它放在一個 environments 節點中,意味著 environments 中可以有多個 environment,為社麼需要多個呢?開發、測試、生產,不同環境各一個 environment,每一個 environment 都有一個 id,也就是它的名字,然後,在 environments 中,通過 default 屬性,指定你需要的 environment。每一個 environment 中,定義一個數據的基本連線資訊。

在 mappers 節點中,定義 Mapper,也就是指定我們上一步所寫的 Mapper 的路徑。

最後,我們來載入這個主配置檔案:

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        User user = (User) sqlSession.selectOne("org.javaboy.mymapper.getUserById", 3);
        System.out.println(user);
        sqlSession.close();
    }
}

首先,我們載入主配置檔案,生成一個 SqlSessionFactory,再由 SqlSessionFactory 生成一個 SqlSession,一個 SqlSession 就相當於是我們的一個會話,類似於 JDBC 中的一個連線,在 SQL 操作完成後,這個會話是可以關閉的。

在這裡,SqlSessionFactoryBuilder 用於建立 SqlSessionFacoty,SqlSessionFacoty 一旦建立完成就不需要SqlSessionFactoryBuilder 了,因為 SqlSession 是通過 SqlSessionFactory 生產,所以可以將 SqlSessionFactoryBuilder 當成一個工具類使用,最佳使用範圍是方法範圍即方法體內區域性變數。SqlSessionFactory 是一個介面,介面中定義了 openSession 的不同過載方法,SqlSessionFactory 的最佳使用範圍是整個應用執行期間,一旦建立後可以重複使用,通常以單例模式管理 SqlSessionFactory。

SqlSession 中封裝了對資料庫的操作,如:查詢、插入、更新、刪除等。通過 SqlSessionFactory 建立 SqlSession,而 SqlSessionFactory 是通過 SqlSessionFactoryBuilder 進行建立。SqlSession 是一個面向使用者的介面, sqlSession 中定義了資料庫操作,預設使用 DefaultSqlSession 實現類。每個執行緒都應該有它自己的 SqlSession 例項。SqlSession 的例項不能共享使用,它也是執行緒不安全的。因此最佳的範圍是請求或方法範圍。絕對不能將 SqlSession 例項的引用放在一個類的靜態欄位或例項欄位中。開啟一個 SqlSession;使用完畢就要關閉它。通常把這個關閉操作放到 finally 塊中以確保每次都能執行關閉。

基於上面幾點,我們可以對 SqlSessionFactory 進行封裝:

public class SqlSessionFactoryUtils {
    private static SqlSessionFactory SQL_SESSION_FACTORY = null;
public static SqlSessionFactory getInstance() {
        if (SQL_SESSION_FACTORY == null) {
            try {
                SQL_SESSION_FACTORY = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return SQL_SESSION_FACTORY;
    }
}

這樣,在需要使用的時候,通過這個工廠方法來獲取 SqlSessionFactory 的例項。

增刪改查

前面的 HelloWorld ,我們做了一個查詢的 Demo,這裡我們來看另外四種常見的操作。

增---主鍵自增長

新增記錄,id 有兩種不同的處理方式,一種就是自增長,另一種則是 Java 程式碼傳一個 ID 進來,傳一個 ID 進來,這個 ID 可以是一個 UUID,也可以是其他的自定義的 ID。在 MyBatis 中,對這兩種方式都提供了相應的支援。

  • 主鍵自增長

首先我們在 Mapper 中,新增 SQL 插入語句:

<insert id="addUser" parameterType="org.javaboy.mybatis.model.User">
    insert into user (username,address) values (#{username},#{address});
</insert>

這裡有一個 parameterType 表示傳入的引數型別。引數都是通過 # 來引用。

然後,在 Java 程式碼中,呼叫這個方法:

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        User user = new User();
        user.setUsername("趙六");
        user.setAddress("北京");
        int insert = sqlSession.insert("org.javaboy.mymapper.addUser", user);
        System.out.println(insert);
        sqlSession.commit();
        sqlSession.close();
    }
}

注意,SQL 插入完成後,一定要提交,即 sqlSession.commit()

  • 使用 UUID 做主鍵

也可以使用 UUID 做主鍵,使用 UUID 做主鍵,又有兩種不同的思路,第一種思路,就是在 Java 程式碼中生成 UUID,直接作為引數傳入到 SQL 中,這種方式就和傳遞普通引數一樣,另一種方式,就是使用 MySQL 自帶的 UUID 函式來生成 UUID。

這裡我們使用第二種方式,因為第一種方式沒有技術含量(自己練習)。使用 MySQL 自帶的 UUID 函式,整體思路是這樣:首先呼叫 MySQL 中的 UUID 函式,獲取到一個 UUID,然後,將這個 UUID 賦值給 User 物件的 ID 屬性,然後再去執行 SQL 插入操作,再插入時使用這個 UUID。

注意,這個實驗需要先將資料的 ID 型別改為 varchar

<insert id="addUser2" parameterType="org.javaboy.mybatis.model.User">
    <selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
        select uuid();
    </selectKey>
    insert into user (id,username,address) values (#{id},#{username},#{address});
</insert>
  • selectKey 表示查詢 key
  • keyProperty 屬性表示將查詢的結果賦值給傳遞進來的 User 物件的 id 屬性
  • resultType 表示查詢結果的返回型別
  • order 表示這個查詢操作的執行時機,BEFORE 表示這個查詢操作在 insert 之前執行
  • 在 selectKey 節點的外面定義 insert 操作

最後,在 Java 程式碼中,呼叫這個方法:

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        User user = new User();
        user.setUsername("趙六");
        user.setAddress("北京");
        int insert = sqlSession.insert("org.javaboy.mymapper.addUser2", user);
        System.out.println(insert);
        sqlSession.commit();
        sqlSession.close();
    }
}

刪除操作比較容易,首先在 UserMapper 中定義刪除 SQL:

<delete id="deleteUserById" parameterType="java.lang.Integer">
    delete from user where id=#{id}
</delete>

然後,在 Java 程式碼中呼叫該方法:

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        int delete = sqlSession.delete("org.javaboy.mymapper.deleteUserById", 2);
        System.out.println(delete);
        sqlSession.commit();
        sqlSession.close();
    }
}

這裡的返回值為該 SQL 執行後,資料庫受影響的行數。

修改操作,也是先定義 SQL:

<update id="updateUser" parameterType="org.javaboy.mybatis.model.User">
    update user set username = #{username} where id=#{id};
</update>

最後在 Java 程式碼中呼叫:

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        User user = new User();
        user.setId(1);
        user.setUsername("javaboy");
        int update = sqlSession.update("org.javaboy.mymapper.updateUser", user);
        System.out.println(update);
        sqlSession.commit();
        sqlSession.close();
    }
}

呼叫的返回值,也是執行 SQL 受影響的行數。

HelloWorld 中展示了根據 id 查詢一條記錄,這裡來看一個查詢所有:

<select id="getAllUser" resultType="org.javaboy.mybatis.model.User">
    select * from user;
</select>

然後在 Java 程式碼中呼叫:

public class Main {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = factory.openSession();
        List<User> list = sqlSession.selectList("org.javaboy.mymapper.getAllUser");
        System.out.println(list);
        sqlSession.commit();
        sqlSession.close();
    }
}

Mybatis架構介紹

看完前面的 HelloWorld,接下來我們通過一張網路圖片來看下 MyBatis 架構:

  1. mybatis 配置:mybatis-config.xml,此檔案作為 mybatis 的全域性配置檔案,配置了 mybatis 的執行環境等資訊。另一個 mapper.xml 檔案即 sql 對映檔案,檔案中配置了操作資料庫的 sql 語句。此檔案需要在 mybatis-config.xml 中載入。
  2. 通過 mybatis 環境等配置資訊構造 SqlSessionFactory 即會話工廠
  3. 由會話工廠建立 sqlSession 即會話,操作資料庫需要通過 sqlSession 進行。
  4. mybatis 底層自定義了 Executor 執行器介面操作資料庫,Executor 介面有兩個實現,一個是基本執行器、一個是快取執行器。
  5. Mapped Statement 也是 mybatis 一個底層封裝物件,它包裝了 mybatis 配置資訊及 sql 對映資訊等。mapper.xml 檔案中一個 sql 對應一個 Mapped Statement 物件,sql 的 id 即是Mapped statement 的 id。
  6. Mapped Statement 對 sql 執行輸入引數進行定義,包括 HashMap、基本型別、pojo,Executor 通過 Mapped Statement 在執行 sql 前將輸入的 java 物件對映至 sql 中,輸入引數對映就是 jdbc 程式設計中對 preparedStatement 設定引數。
  7. Mapped Statement 對 sql 執行輸出結果進行定義,包括 HashMap、基本型別、pojo,Executor 通過 Mapped Statement 在執行 sql 後將輸出結果對映至 java 物件中,輸出結果對映過程相當於 jdbc 程式設計中對結果的解析處理過程。

MyBatis 所解決的 JDBC 中存在的問題

  1. 資料庫連結建立、釋放頻繁造成系統資源浪費從而影響系統性能,如果使用資料庫連結池可解決此問題。解決:在 mybatis-config.xml 中配置資料鏈接池,使用連線池管理資料庫連結。
  2. Sql語句寫在程式碼中造成程式碼不易維護,實際應用 sql 變化的可能較大,sql 變動需要改變 java 程式碼。解決:將 Sql 語句配置在 XXXXmapper.xml 檔案中與 java 程式碼分離。
  3. 向 sql 語句傳引數麻煩,因為 sql 語句的 where 條件不一定,可能多也可能少,佔位符需要和引數一一對應。解決:Mybatis 自動將 java 物件對映至 sql 語句,通過 statement 中的 parameterType 定義輸入引數的型別。
  4. 對結果集解析麻煩,sql 變化導致解析程式碼變化,且解析前需要遍歷,如果能將資料庫記錄封裝成 pojo 物件解析比較方便。解決:Mybatis 自動將 sql 執行結果對映至 java 物件,通過 statement 中的 resultType 定義輸出結果的型別。

引入Mapper

前面我們所寫的增刪改查是存在問題的。主要問題就是冗餘程式碼過多,模板化程式碼過多。例如,我想開發一個 UserDao,可能是下面這樣:

public class UserDao {
    private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getInstance();

    public User getUserById(Integer id) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = (User) sqlSession.selectOne("org.javaboy.mybatis.mapper.UserDao.getUserById", id);
        sqlSession.close();
        return user;
    }

    public Integer addUser(User user) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        int insert = sqlSession.insert("org.javaboy.mybatis.mapper.UserDao.addUser", user);
        sqlSession.commit();
        sqlSession.close();
        return insert;
    }

    public Integer addUser2(User user) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        int insert = sqlSession.insert("org.javaboy.mybatis.mapper.UserDao.addUser2", user);
        sqlSession.commit();
        sqlSession.close();
        return insert;
    }

    public Integer deleteUserById(Integer id) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        int delete = sqlSession.delete("org.javaboy.mybatis.mapper.UserDao.deleteUserById", id);
        sqlSession.commit();
        sqlSession.close();
        return delete;
    }

    public Integer updateUser(User user) {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        int delete = sqlSession.delete("org.javaboy.mybatis.mapper.UserDao.updateUser", user);
        sqlSession.commit();
        sqlSession.close();
        return delete;
    }

    public List<User> getAllUser() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> users = sqlSession.selectList("org.javaboy.mybatis.mapper.UserDao.getAllUser");
        sqlSession.close();
        return users;
    }
}

然後,和這個 UserDao 對應的,還有一個 UserMapper.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="org.javaboy.mybatis.mapper.UserDao">

    <select id="getUserById" resultType="org.javaboy.mybatis.model.User">
        select * from user where id=#{id};
    </select>
    <insert id="addUser" parameterType="org.javaboy.mybatis.model.User">
        insert into user (username,address) values (#{username},#{address});
    </insert>
    <insert id="addUser2" parameterType="org.javaboy.mybatis.model.User">
        <selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
            select uuid();
        </selectKey>
        insert into user (id,username,address) values (#{id},#{username},#{address});
    </insert>

    <delete id="deleteUserById" parameterType="java.lang.Integer">
        delete from user where id=#{id}
    </delete>

    <update id="updateUser" parameterType="org.javaboy.mybatis.model.User">
        update user set username = #{username} where id=#{id};
    </update>

    <select id="getAllUser" resultType="org.javaboy.mybatis.model.User">
        select * from user;
    </select>
</mapper>

此時,我們分析這個 UserDao,發現它有很多可以優化的地方。每個方法中都要獲取 SqlSession,涉及到增刪改的方法,還需要 commit,SqlSession 用完之後,還需要關閉,sqlSession 執行時需要的引數就是方法的引數,sqlSession 要執行的 SQL ,和 XML 中的定義是一一對應的。這是一個模板化程度很高的程式碼。

既然模板化程度很高,我們就要去解決它,原理很簡單,就是前面 Spring 中所說的動態代理。我們可以將當前方法簡化成 一個介面:

package org.javaboy.mapper;

public interface UserMapper {
    User getUserById(Integer id);

    Integer addUser(User user);

    Integer addUser2(User user);

    Integer deleteUserById(Integer id);

    Integer updateUser(User user);

    List<User> getAllUser();
}

這個介面對應的 Mapper 檔案如下(注意,UserMapper.xml 和 UserMapper 需要放在同一個包下面):

<?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="org.javaboy.mybatis.mapper.UserMapper">

    <select id="getUserById" resultType="org.javaboy.mybatis.model.User">
        select * from user where id=#{id};
    </select>
    <insert id="addUser" parameterType="org.javaboy.mybatis.model.User">
        insert into user (username,address) values (#{username},#{address});
    </insert>
    <insert id="addUser2" parameterType="org.javaboy.mybatis.model.User">
        <selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
            select uuid();
        </selectKey>
        insert into user (id,username,address) values (#{id},#{username},#{address});
    </insert>

    <delete id="deleteUserById" parameterType="java.lang.Integer">
        delete from user where id=#{id}
    </delete>

    <update id="updateUser" parameterType="org.javaboy.mybatis.model.User">
        update user set username = #{username} where id=#{id};
    </update>

    <select id="getAllUser" resultType="org.javaboy.mybatis.model.User">
        select * from user;
    </select>
</mapper>

使用這個介面,完全可以代替上面的 UserDao,為什麼呢?因為這個介面提供了 UserDao 所需要的最核心的東西,根據這個介面,就可以自動生成 UserDao:

  • 首先,UserDao 中定義了 SqlSessionFactory,這是一套固定的程式碼
  • UserMapper 所在的包+UserMapper 類名+UserMapper 中定義好的方法名,就可以定位到要呼叫的 SQL
  • 要呼叫 SqlSession 中的哪個方法,根據定位到的 SQL 節點就能確定

因此,我們在 MyBatis 開發中,實際上不需要自己提供 UserDao 的實現,我們只需要提供一個 UserMapper 即可。

然後,我們在 MyBatis 的全域性配置中,配置一下 UserMapper:

<?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="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
                <property name="username" value="root"/>
                <property name="password" value="123"/>
            </dataSource>
        </environment>
    </environments>
    
    <mappers>
        <package name="org.javaboy.mybatis.mapper"/>
    </mappers>
    
</configuration>

然後,載入配置檔案,獲取 UserMapper,並呼叫它裡邊的方法:

public class Main2 {
    public static void main(String[] args) {
        SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
        SqlSession sqlSession = instance.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> allUser = mapper.getAllUser();
        System.out.println(allUser);
    }
}

注意,在 Maven 中,預設情況下,Maven 要求我們將 XML 配置、properties 配置等,都放在 resources 目錄下,如果我們強行放在 java 目錄下,預設情況下,打包的時候這個配置檔案會被自動忽略掉。對於這兩個問題,我們有兩種解決辦法:

  • 不要忽略 XML 配置:

我們可以在 pom.xml 中,新增如下配置,讓 Maven 不要忽略我在 java 目錄下的 XML 配置:

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
        </resource>
    </resources>
</build>
  • 按照 Maven 的要求來

按照 Maven 的要求來,將 xml 檔案放到 resources 目錄下,但是,MyBatis 中預設情況下要求,UserMapper.xml 和 UserMapper 介面,必須放在一起,所以,我們需要手動在 resources 目錄下,建立一個和 UserMapper 介面相同的目錄:

這樣,我們就不需要在 pom.xml 檔案中新增配置了,因為這種寫法同時滿足了 Maven 和 MyBatis 的要求。

全域性配置

properties

properties 可以用來引入一個外部配置,最近常見的例子就是引入資料庫的配置檔案,例如我們在 resources 目錄下新增一個 db.properties 檔案作為資料庫的配置檔案,檔案內容如下:

db.username=root
db.password=123
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql:///test01?serverTimezone=Asia/Shanghai

然後,利用 mybatis-config.xml 配置檔案中的 properties 屬性,引入這個配置檔案,然後在 DataSource 中使用這個配置檔案,最終配置如下:

<configuration>
    <properties resource="db.properties"></properties>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${db.driver}"/>
                <property name="url" value="${db.url}"/>
                <property name="username" value="${db.username}"/>
                <property name="password" value="${db.password}"/>
            </dataSource>
        </environment>
    </environments>
    
    <mappers>
        <package name="org.javaboy.mybatis.mapper"/>
    </mappers>
</configuration>

Setting

Setting(設定) Description(描述) Valid Values(驗證值組) Default(預設值)
cacheEnabled 在全域性範圍內啟用或禁用快取配置任何對映器在此配置下 true or false TRUE
lazyLoadingEnabled 在全域性範圍內啟用或禁用延遲載入。禁用時,所有查詢將熱載入 true or false TRUE
aggressiveLazyLoading 啟用時,有延遲載入屬性的物件將被完全載入後呼叫懶惰的任何屬性。否則,每一個屬性是按需載入。 true or false TRUE
multipleResultSetsEnabled 允許或不允許從一個單獨的語句(需要相容的驅動程式)要返回多個結果集。 true or false TRUE
useColumnLabel 使用列標籤,而不是列名。在這方面,不同的驅動有不同的行為。參考驅動文件或測試兩種方法來決定你的驅動程式的行為如何。 true or false TRUE
useGeneratedKeys 允許 JDBC 支援生成的金鑰。相容的驅動程式是必需的。此設定強制生成的鍵被使用,如果設定為 true,一些驅動會不相容性,但仍然可以工作。 true or false FALSE
autoMappingBehavior 指定 MyBatis 應如何自動對映列到欄位/屬性。NONE自動對映。 PARTIAL 只會自動對映結果沒有巢狀結果對映定義裡面。 FULL 會自動對映的結果對映任何複雜的(包含巢狀或其他)。 NONE, PARTIAL, FULL PARTIAL
defaultExecutorType 配置預設執行人。SIMPLE執行人確實沒有什麼特別的。 REUSE執行器重用準備好的語句。 BATCH執行器重用語句和批處理更新。 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 設定驅動程式等待一個數據庫響應的秒數。 Any positive integer Not Set (null)
safeRowBoundsEnabled 允許使用巢狀的語句RowBounds。 true or false FALSE
mapUnderscoreToCamelCase 從經典的資料庫列名 A_COLUMN 啟用自動對映到駱駝標識的經典的 Java 屬性名 aColumn。 true or false FALSE
localCacheScope MyBatis的使用本地快取,以防止迴圈引用,並加快反覆巢狀查詢。預設情況下(SESSION)會話期間執行的所有查詢快取。如果 localCacheScope=STATMENT 本地會話將被用於語句的執行,只是沒有將資料共享之間的兩個不同的呼叫相同的 SqlSession。 SESSION or STATEMENT SESSION
dbcTypeForNull 指定為空值時,沒有特定的JDBC型別的引數的 JDBC 型別。有些驅動需要指定列的 JDBC 型別,但其他像 NULL,VARCHAR 或 OTHER 的工作與通用值。 JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER OTHER
lazyLoadTriggerMethods 指定觸發延遲載入的物件的方法。 A method name list separated by commas equals,clone,hashCode,toString
defaultScriptingLanguage 指定所使用的語言預設為動態SQL生成。 A type alias or fully qualified class name. org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
callSettersOnNulls 指定如果setter方法或地圖的put方法時,將呼叫檢索到的值是null。它是有用的,當你依靠Map.keySet()或null初始化。注意原語(如整型,布林等)不會被設定為null。 true or false FALSE
logPrefix 指定的字首字串,MyBatis將會增加記錄器的名稱。 Any String Not set
logImpl 指定MyBatis的日誌實現使用。如果此設定是不存在的記錄的實施將自動查詢。 SLF4J or LOG4J or LOG4J2 or JDK_LOGGING or COMMONS_LOGGING or STDOUT_LOGGING or NO_LOGGING Not set
proxyFactory 指定代理工具,MyBatis將會使用建立懶載入能力的物件。 CGLIB JAVASSIST

typeAliases

這個是 MyBatis 中定義的別名,分兩種,一種是 MyBatis 自帶的別名,另一種是我們自定義的別名。

MyBatis 自帶的別名

別名 對映的型別
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal

本來,我們在 Mapper 中定義資料型別時,需要寫全路徑,如下:

<select id="getUserCount" resultType="java.lang.Integer">
    select count(*) from user ;
</select>

但是,每次寫全路徑比較麻煩。這種時候,我們可以用型別的別名來代替,例如用 int 做 Integer 的別名:

<select id="getUserCount" resultType="int">
    select count(*) from user ;
</select>

自定義別名

我們自己的物件,在 Mapper 中定義的時候,也是需要寫全路徑:

<select id="getAllUser" resultType="org.javaboy.mybatis.model.User">
    select * from user;
</select>

這種情況下,寫全路徑也比較麻煩,我們可以給我們自己的 User 物件取一個別名,在 mybatis-config.xml 中新增 typeAliases 節點:

<configuration>
    
    <properties resource="db.properties"></properties>
    <typeAliases>
        <typeAlias type="org.javaboy.mybatis.model.User" alias="javaboy"/>
    </typeAliases>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${db.driver}"/>
                <property name="url" value="${db.url}"/>
                <property name="username" value="${db.username}"/>
                <property name="password" value="${db.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="org.javaboy.mybatis.mapper"/>
    </mappers>
</configuration>

這裡,我們給 User 物件取了一個別名叫 javaboy,然後,我們就可以在 Mapper 中直接使用 javaboy 來代替 User 物件了:

<select id="getAllUser" resultType="javaboy">
    select * from user;
</select>

但是,這種一個一個去列舉物件的過程非常麻煩,我們還可以批量給物件定義別名,批量定義主要是利用包掃描來做,批量定義預設的類的別名,是類名首字母小寫,例如如下配置:

<typeAliases>
    <package name="org.javaboy.mybatis.model"/>
</typeAliases>

這個配置就表示給 org.javaboy.mybatis.model 包下的所有類取別名,預設的別名就是類名首字母小寫。這個時候,我們在 Mapper 中,就可以利用 user 代替 User 全路徑了:

<select id="getAllUser" resultType="user">
    select * from user;
</select>

typeHandlers

在 MyBatis 對映中,能夠自動將 Jdbc 型別對映為 Java 型別。預設的對映規則,如下:

型別處理器 Java型別 JDBC型別
BooleanTypeHandler Boolean,boolean 任何相容的布林值
ByteTypeHandler Byte,byte 任何相容的數字或位元組型別
ShortTypeHandler Short,short 任何相容的數字或短整型
IntegerTypeHandler Integer,int 任何相容的數字和整型
LongTypeHandler Long,long 任何相容的數字或長整型
FloatTypeHandler Float,float 任何相容的數字或單精度浮點型
DoubleTypeHandler Double,double 任何相容的數字或雙精度浮點型
BigDecimalTypeHandler BigDecimal 任何相容的數字或十進位制小數型別
StringTypeHandler String CHAR和VARCHAR型別
ClobTypeHandler String CLOB和LONGVARCHAR型別
NStringTypeHandler String NVARCHAR和NCHAR型別
NClobTypeHandler String NCLOB型別
ByteArrayTypeHandler byte[] 任何相容的位元組流型別
BlobTypeHandler byte[] BLOB和LONGVARBINARY型別
DateTypeHandler Date(java.util) TIMESTAMP型別
DateOnlyTypeHandler Date(java.util) DATE型別
TimeOnlyTypeHandler Date(java.util) TIME型別
SqlTimestampTypeHandler Timestamp(java.sql) TIMESTAMP型別
SqlDateTypeHandler Date(java.sql) DATE型別
SqlTimeTypeHandler Time(java.sql) TIME型別
ObjectTypeHandler 任意 其他或未指定型別
EnumTypeHandler Enumeration型別 VARCHAR-任何相容的字串型別,作為程式碼儲存(而不是索引)。

前面案例中,之所以資料能夠接收成功,是因為有上面這些預設的型別處理器,處理基本資料型別,這些夠用了,特殊型別,需要我們自定義型別處理器。

比如,我有一個使用者愛好的欄位,這個欄位,在物件中,是一個 List 集合,在資料庫中,是一個 VARCHAR 欄位,這種情況下,就需要我們自定義型別轉換器,自定義的型別轉換器提供兩個功能:

  1. 資料儲存時,自動將 List 集合,轉為字串(格式自定義)
  2. 資料查詢時,將查到的字串再轉為 List 集合

首先,在資料表中新增一個 favorites 欄位:

然後,在 User 物件中,新增相應的屬性:

public class User {
    private Integer id;
    private String username;
    private String address;
    private List<String> favorites;

    public List<String> getFavorites() {
        return favorites;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", address='" + address + '\'' +
                ", favorites=" + favorites +
                '}';
    }

    public void setFavorites(List<String> favorites) {
        this.favorites = favorites;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

為了能夠將 List 集合中的資料存入到 VARCHAR 中,我們需要自定義一個型別轉換器:

@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(List.class)
public class List2VarcharHandler implements TypeHandler<List<String>> {
    public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
        StringBuffer sb = new StringBuffer();
        for (String s : parameter) {
            sb.append(s).append(",");
        }
        ps.setString(i, sb.toString());
    }

    public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
        String favs = rs.getString(columnName);
        if (favs != null) {
            return Arrays.asList(favs.split(","));
        }
        return null;
    }

    public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
        String favs = rs.getString(columnIndex);
        if (favs != null) {
            return Arrays.asList(favs.split(","));
        }
        return null;
    }

    public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String favs = cs.getString(columnIndex);
        if (favs != null) {
            return Arrays.asList(favs.split(","));
        }
        return null;
    }
}
  • 首先在這個自定義的型別轉換器上新增 @MappedJdbcTypes 註解指定要處理的 Jdbc 資料型別,另外還有一個註解是 @MappedTypes 指定要處理的 Java 型別,這兩個註解結合起來,就可以鎖定要處理的欄位是 favorites 了。
  • setParameter 方法看名字就知道是設定引數的,這個設定過程由我們手動實現,我們在這裡,將 List 集合中的每一項,用一個 , 串起來,組成一個字串。
  • getResult 方法,有三個過載方法,其實都是處理查詢的。

接下來,修改插入的 Mapper:

<insert id="addUser" parameterType="org.javaboy.mybatis.model.User">
        insert into user (username,address,favorites) values (#{username},#{address},#{favorites,typeHandler=org.javaboy.mybatis.typehandler.List2VarcharHandler});
</insert>

然後,在 Java 程式碼中,呼叫該方法:

public class Main2 {
    public static void main(String[] args) {
        SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
        SqlSession sqlSession = instance.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = new User();
        user.setUsername("風氣");
        user.setAddress("上海");
        List<String> favorites = new ArrayList<String>();
        favorites.add("足球");
        favorites.add("籃球");
        favorites.add("乒乓球");
        user.setFavorites(favorites);
        mapper.addUser(user);
        sqlSession.commit();
    }
}

這樣,List 集合存入到資料庫中之後,就變成一個字串了:

讀取的配置,有兩個地方,一個可以在 ResultMap 中做區域性配置,也可以在全域性配置中進行過配置,全域性配置方式如下:

<configuration>
    <properties resource="db.properties"></properties>
    <typeAliases>
        <package name="org.javaboy.mybatis.model"/>
    </typeAliases>
    
    <typeHandlers>
        <package name="org.javaboy.mybatis.typehandler"/>
    </typeHandlers>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${db.driver}"/>
                <property name="url" value="${db.url}"/>
                <property name="username" value="${db.username}"/>
                <property name="password" value="${db.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="org.javaboy.mybatis.mapper"/>
    </mappers>
</configuration>

接下來去查詢,查詢過程中,就會自動將字串轉為 List 集合了。

Mapper

Mapper 配置的幾種方法:

  • <mapper resource=" " />

使用相對於類路徑的資源,即 XML 的定位,從 classpath 開始寫。

如:

<mapper resource="mapping/User.xml" />
  • <mapper url="" />

使用完全限定路徑,相當於使用絕對路徑,這種方式使用非常少。

如:

<mapper url="file:///D:\demo\xxx\User.xml" />
  • <mapper class=" " />

使用 mapper 介面類路徑,注意:此種方法要求 mapper 介面名稱和 mapper 對映檔名稱相同,且放在同一個目錄中

如:

<mapper class="org.sang.mapper.UserMapper"/>
  • <package name=""/>

註冊指定包下的所有 mapper 介面

如:

<package name="org.sang.mybatis.mapper"/>

注意:此種方法要求 mapper 介面名稱和 mapper 對映檔名稱相同,且放在同一個目錄中。實際專案中,多采用這種方式。

Mapper 對映檔案

Mapper 對映檔案

mapper 對映檔案,是 MyBatis 中最重要的部分,涉及到的細節也是非常非常多。

parameterType

這個表示輸入的引數型別。

$#

這是一個非常非常高頻的面試題,雖然很簡單。在面試中,如果涉及到 MyBatis,一般情況下,都是這個問題。

在 MyBatis 中,我們在 mapper 引用變數時,預設使用的是 #,像下面這樣:

<select id="getUserById" resultType="org.javaboy.mybatis.model.User">
    select * from user where id=#{id};
</select>

除了使用 # 之外,我們也可以使用 $ 來引用一個變數:

<select id="getUserById" resultType="org.javaboy.mybatis.model.User">
    select * from user where id=${id};
</select>

在舊的 MyBatis 版本中,如果使用 $,變數需要通過 @Param 取別名,在最新的 MyBatis 中,無論是 # 還是 $,如果只有一個引數,可以不用取別名,如下:

public interface UserMapper {
    User getUserById(Integer id);
}

既然 #$ 符號都可以使用,那麼他們有什麼區別呢?

我們在 resources 目錄下,新增 log4j.properties ,將 MyBatis 執行時的 SQL 打印出來:

log4j.rootLogger=DEBUG,stdout
log4j.logger.org.mybatis=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n

然後新增日誌依賴:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.5</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.5</version>
</dependency>

然後,我們可以分別觀察 $# 執行時的日誌:

上面這個日誌,是 $ 符號執行的日誌,可以看到,SQL 直接就拼接好了,沒有引數。

下面這個,是 # 執行的日誌,可以看到,這個日誌中,使用了預編譯的方式:

在 JDBC 呼叫中,SQL 的執行,我們可以通過字串拼接的方式來解決引數的傳遞問題,也可以通過佔位符的方式來解決引數的傳遞問題。當然,這種方式也傳遞到 MyBatis 中,在 MyBatis 中,$ 相當於是引數拼接的方式,而 # 則相當於是佔位符的方式。

一般來說,由於引數拼接的方式存在 SQL 注入的風險,因此我們使用較少,但是在一些特殊的場景下,又不得不使用這種方式。

有的 SQL 拼接實際上可以通過資料庫函式來解決,例如模糊查詢:

<select id="getUserByName" resultType="org.javaboy.mybatis.model.User">
    select * from user where username like concat('%',#{name},'%');
</select>

但是有的 SQL 無法使用 # 來拼接,例如傳入一個動態欄位進來,假設我想查詢所有資料,要排序查詢,但是排序的欄位不確定,需要通過引數傳入,這種場景就只能使用 $,例如如下方法:

List<User> getAllUser(String orderBy);

定義該方法對應的 XML 檔案:

<select id="getAllUser" resultType="user">
    select * from user order by ${orderBy}
</select>

測試一下:

SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
SqlSession sqlSession = instance.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allUser = mapper.getAllUser("id");
System.out.println(allUser);

小結:

面試中,遇到這個問題,一定要答出來 Statement 和 PreparedStatement 之間的區別,這個問題才算理解到位了。

簡單型別

簡單資料型別傳遞比較容易,像前面的根據 id 查詢一條記錄就算是這一類的。

這裡再舉一個例子,比如根據 id 修改使用者名稱:

Integer updateUsernameById(String username, Integer id);

再定義該方法對應的 mapper:

<update id="updateUsernameById">
    update user set username = #{username} where id=#{id};
</update>

此時,如果直接呼叫該方法,會丟擲異常:

這裡是說,找不到我們定義的 username 和 id 這兩個引數。同時,這個錯誤提示中指明,可用的引數名是 [arg1, arg0, param1, param2],相當於我們自己給變數取的名字失效了,要使用系統提供的預設名字,預設名字實際上是兩套體系:

第一套就是 arg0、arg1、、、、
第二套就是 param1、param2、、、

注意,這兩個的下標是不一樣的。

因此,按照錯誤提示,我們將引數改為下面這樣:

<update id="updateUsernameById">
    update user set username = #{arg0} where id=#{arg1};
</update>

或者下面這樣:

<update id="updateUsernameById">
    update user set username = #{param1} where id=#{param2};
</update>

這兩種方式,都可以使該方法順利執行。

但是,預設的名字不好記,容易出錯,我們如果想要使用自己寫的變數的名字,可以通過給引數新增 @Param 來指定引數名(一般在又多個引數的時候,需要加),一旦用 @Param 指定了引數型別之後,可以省略掉引數型別,就是在 xml 檔案中,不用定義 parameterType 了:

Integer updateUsernameById(@Param("username") String username, @Param("id") Integer id);

這樣定義之後,我們在 mapper.xml 檔案中,就可以直接使用 username 和 id 來引用變量了。

物件引數

物件引數。

例如新增一個使用者:

Integer addUser(User user);

對應的 mapper 檔案如下:

<insert id="addUser" parameterType="org.javaboy.mybatis.model.User">
    insert into user (username,address,favorites) values (#{username},#{address},#{favorites,typeHandler=org.javaboy.mybatis.typehandler.List2VarcharHandler});
</insert>

我們在引用的時候,直接使用屬性名就能夠定位到物件了。如果物件存在多個,我們也需要給物件新增 @Param 註解,如果給物件添加了 @Param 註解,那麼物件屬性的引用,會有一些變化。如下:

Integer addUser(@Param("user") User user);

如果物件引數添加了 @Param 註解,Mapper 中的寫法就會發生變化:

<insert id="addUser" parameterType="org.javaboy.mybatis.model.User">
    insert into user (username,address,favorites) values (#{user.username},#{user.address},#{user.favorites,typeHandler=org.javaboy.mybatis.typehandler.List2VarcharHandler});
</insert>

注意多了一個字首,這個字首不是變數名,而是 @Param 註解中定義名稱。

如果物件中還存在物件,用 . 繼續取訪問就可以了。

Map 引數

一般不推薦在專案中使用 Map 引數。如果想要使用 Map 傳遞引數,技術上來說,肯定是沒有問題的。

Integer updateUsernameById(HashMap<String,Object> map);

XML 檔案寫法如下:

<update id="updateUsernameById">
    update user set username = #{username} where id=#{id};
</update>

引用的變數名,就是 map 中的 key。基本上和實體類是一樣的,如果給 map 取了別名,那麼在引用的時候,也要將別名作為字首加上,這一點和實體類也是一樣的。

resultType

resultType 是返回型別,在實際開發中,如果返回的資料型別比較複雜,一般我們使用 resultMap,但是,對於一些簡單的返回,使用 resultType 就夠用了。

resultType 返回的型別可以是簡單型別,可以是物件,可以是集合,也可以是一個 hashmap,如果是 hashmap,map 中的 key 就是欄位名,value 就是欄位的值。

輸出 pojo 物件和輸出 pojo 列表在 sql 中定義的 resultType 是一樣的。
返回單個 pojo 物件要保證 sql 查詢出來的結果集為單條,內部使用 sqlSession.selectOne 方法呼叫,mapper 介面使用 pojo 物件作為方法返回值。返回 pojo 列表表示查詢出來的結果集可能為多條,內部使用 sqlSession.selectList 方法,mapper 介面使用 List 物件作為方法返回值。

resultMap

在實際開發中,resultMap 是使用較多的返回資料型別配置。因為實際專案中,一般的返回資料型別比較豐富,要麼欄位和屬性對不上,要麼是一對一、一對多的查詢,等等,這些需求,單純的使用 resultType 是無法滿足的,因此我們還需要使用 resultMap,也就是自己定義對映的結果集。

先來看一個基本用法:

首先在 mapper.xml 中定義一個 resultMap:

<resultMap id="MyResultMap" type="org.javaboy.mybatis.model.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="address" property="address"/>
</resultMap>

在這個 resultMap 中,id 用來描述主鍵,column 是資料庫查詢出來的列名,property 則是物件中的屬性名。

然後在查詢結果中,定義返回值時使用這個 ResultMap:

<select id="getUserById" resultMap="MyResultMap">
    select * from user where id=#{id};
</select>

注意,在舊版的 MyBatis 中,要求實體類一定要有一個無參構造方法,新版的 MyBatis 沒有這個要求。

當然,我們也可以在 resultMap 中,自己指定要呼叫的構造方法,指定方式如下:

<resultMap id="MyResultMap" type="org.javaboy.mybatis.model.User">
    <constructor>
        <idArg column="id" name="id"/>
        <arg column="username" name="username"/>
    </constructor>
</resultMap>

這個就表示使用兩個引數的構造方法取構造一個 User 例項。注意,name 屬性表示構造方法中的變數名,預設情況下,變數名是 arg0、arg1、、、、或者 param1、param2、、、,如果需要自定義,我們可以在構造方法中,手動加上 @Param 註解。

public class User {
    private Integer id;
    private String username;
    private String address;
    private List<String> favorites;

    public User(@Param("id") Integer id, @Param("username") String username) {
        this.id = id;
        this.username = username;
        System.out.println("--------------------");
    }

    public User(Integer id, String username, String address, List<String> favorites) {
        this.id = id;
        this.username = username;
        this.address = address;
        this.favorites = favorites;
        System.out.println("-----------sdfasfd---------");
    }

    public List<String> getFavorites() {
        return favorites;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", address='" + address + '\'' +
                ", favorites=" + favorites +
                '}';
    }

    public void setFavorites(List<String> favorites) {
        this.favorites = favorites;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

動態 SQL

動態 SQL 是 MyBatis 中非常強大的一個功能。例如一些常見的查詢場景:

  • 查詢條件不確定
  • 批量插入
  • ….

這些類似需求,我們都可以通過 MyBatis 提供的動態 SQL 來解決。

MyBatis 中提供的動態 SQL 節點非常多。

if

if 是一個判斷節點,如果滿足某個條件,節點中的 SQL 就會生效。例如分頁查詢,要傳遞兩個引數,頁碼和查詢的記錄數,如果這兩個引數都為 null,那我就查詢所有。

我們首先來定義介面方法:

List<User> getUserByPage(@Param("start") Integer start, @Param("count") Integer count);

介面定義成功後,接下來在 XML 中定義 SQL:

<select id="getUserByPage" resultType="org.javaboy.mybatis.model.User">
    select * from user
    <if test="start !=null and count!=null">
        limit #{start},#{count}
    </if>
</select>

if 節點中,test 表示判斷條件,如果判斷結果為 true,則 if 節點的中的 SQL 會生效,否則不會生效。也就是說,在方法呼叫時,如果分頁的兩個引數都為 null,則表示查詢所有資料:

public class Main2 {
    public static void main(String[] args) {
        SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
        SqlSession sqlSession = instance.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> list = mapper.getUserByPage(null, null);
        System.out.println(list);
        list = mapper.getUserByPage(2, 2);
        System.out.println(list);
        sqlSession.commit();
    }
}

where

where 用來處理查詢引數。例如我存在下面一個查詢函式:

List<User> getUserByUsernameAndId(@Param("id") Integer id, @Param("name") String name);

這個查詢的複雜之處在於:每個引數都是可選的,如果 id 為 null,則表示根據 name 查詢,name 為 null,則表示根據 id 查詢,兩個都為 null,表示查詢所有。

<select id="getUserByUsernameAndId" resultType="org.javaboy.mybatis.model.User">
    select * from user
    <where>
        <if test="id!=null">
            and id>#{id}
        </if>
        <if test="name!=null">
            and username like concat('%',#{name},'%')
        </if>
    </where>
</select>

用 where 節點將所有的查詢條件包起來,如果有滿足的條件,where 節點會自動加上,如果沒有,where 節點也將不存在,在有滿足條件的情況下,where 還會自動處理 and 關鍵字。

public class Main2 {
    public static void main(String[] args) {
        SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
        SqlSession sqlSession = instance.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> list = mapper.getUserByUsernameAndId(2, "java");
        System.out.println(list);
        list = mapper.getUserByUsernameAndId(null, "javaboy");
        System.out.println(list);
        list = mapper.getUserByUsernameAndId(5, null);
        System.out.println(list);
        list = mapper.getUserByUsernameAndId(null, null);
        System.out.println(list);
    }
}

foreach

foreach 用來處理陣列/集合引數。

例如,我們有一個批量查詢的需求:

List<User> getUserByIds(@Param("ids")Integer[] ids);

對應的 XML 如下:

<select id="getUserByIds" resultType="org.javaboy.mybatis.model.User">
    select * from user where id in
    <foreach collection="ids" open="(" close=")" item="id" separator=",">
        #{id}
    </foreach>
</select>

在 mapper 中,通過 foreach 節點來遍歷陣列,collection 表示陣列變數,open 表示迴圈結束後,左邊的符號,close 表示迴圈結束後,右邊的符號,item 表示迴圈時候的單個變數,separator 表示迴圈的元素之間的分隔符。

注意,預設情況下,無論你的陣列/集合引數名字是什麼,在 XML 中訪問的時候,都是 array,開發者可以通過 @Param 註解給引數重新指定名字。

例如我還有一個批量插入的需求:

Integer batchInsertUser(@Param("users") List<User> users);

然後,定義該方法對應的 mapper:

<insert id="batchInsertUser">
    insert into user (username,address) values 
    <foreach collection="users" separator="," item="user">
        (#{user.username},#{user.address})
    </foreach>
</insert>

然後,在 Main 方法中進行測試:

public class Main2 {
    public static void main(String[] args) {
        SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
        SqlSession sqlSession = instance.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = new ArrayList<>();
        User u1 = new User();
        u1.setUsername("zhangsan");
        u1.setAddress("shenzhen");
        users.add(u1);
        User u2 = new User();
        u2.setUsername("lisi");
        u2.setAddress("廣州");
        users.add(u2);
        mapper.batchInsertUser(users);
        sqlSession.commit();
    }
}

sql 片段

大家知道,在 SQL 查詢中,一般不建議寫 *,因為 select * 會降低查詢效率。但是,每次查詢都要把欄位名列出來,太麻煩。這種使用,我們可以利用 SQL 片段來解決這個問題。

例如,我們先在 mapper 中定義一個 SQL 片段:

<sql id="Base_Column">
    id,usename,address
</sql>

然後,在其他 SQL 中,就可以引用這個變數:

<select id="getUserByIds" resultType="org.javaboy.mybatis.model.User">
    select <include refid="Base_Column" /> from user where id in
    <foreach collection="ids" open="(" close=")" item="id" separator=",">
        #{id}
    </foreach>
</select>

set

set 關鍵字一般用在更新中。因為大部分情況下,更新的欄位可能不確定,如果物件中存在該欄位的值,就更新該欄位,不存在,就不更新。例如如下方法:

Integer updateUser(User user);

現在,這個方法的需求是,根據使用者 id 來跟新使用者的其他屬性,所以,user 物件中一定存在 id,其他屬性則不確定,其他屬性要是有值,就更新,沒值(也就是為 null 的時候),則不處理該欄位。

我們結合 set 節點,寫出來的 sql 如下:

<update id="updateUser" parameterType="org.javaboy.mybatis.model.User">    
    update user    
    <set>        
        <if test="username!=null">            
            username = #{username},        
        </if>        
        <if test="address!=null">            
            address=#{address},        
        </if>        
        <if test="favorites!=null">            
            favorites=#{favorites},        
        </if>    
    </set>    
    where id=#{id};
</update>

查詢進階

一對一查詢

在實際開發中,經常會遇到一對一查詢,一對多查詢等。這裡我們先來看一對一查詢。

例如:每本書都有一個作者,作者都有自己的屬性,根據這個,我來定義兩個實體類:

public class Book {
    private Integer id;
    private String name;
    private Author author;

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", author=" + author +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
public class Author {
    private Integer id;
    private String name;
    private Integer age;

    @Override
    public String toString() {
        return "Author{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

然後,在資料庫中,新增兩張表:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test01` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `test01`;

/*Table structure for table `author` */

DROP TABLE IF EXISTS `author`;

CREATE TABLE `author` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

/*Data for the table `author` */

/*Table structure for table `book` */

DROP TABLE IF EXISTS `book`;

CREATE TABLE `book` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `aid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

新增成功後,我們新建一個 BookMapper:

public interface BookMapper {
    Book getBookById(Integer id);
}

BookMapper 中定義了一個查詢 Book 的方法,但是我希望查出來 Book 的同時,也能查出來它的 Author。再定義一個 BookMapper.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="org.javaboy.mybatis.mapper.BookMapper">

    <resultMap id="BookWithAuthor" type="org.javaboy.mybatis.model.Book">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <association property="author" javaType="org.javaboy.mybatis.model.Author">
            <id column="aid" property="id"/>
            <result column="aname" property="name"/>
            <result column="aage" property="age"/>
        </association>
    </resultMap>

    <select id="getBookById" resultMap="BookWithAuthor">
        SELECT b.*,a.`age` AS aage,a.`id` AS aid,a.`name` AS aname FROM book b,author a WHERE b.`aid`=a.`id` AND b.`id`=#{id}
    </select>
</mapper>

在這個查詢 SQL 中,首先應該做好一對一查詢,然後,返回值一定要定義成 resultMap,注意,這裡千萬不能寫錯。然後,在 resultMap 中,來定義查詢結果的對映關係。

其中,association 節點用來描述一對一的關係。這個節點中的內容,和 resultMap 一樣,也是 id,result 等,在這個節點中,我們還可以繼續描述一對一。

由於在實際專案中,每次返回的資料型別可能都會有差異,這就需要定義多個 resultMap,而這多個 resultMap 中,又有一部份屬性是相同的,所以,我們可以將相同的部分抽出來,做成一個公共的模板,然後被其他 resultMap 繼承,優化之後的 mapper 如下:

<?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="org.javaboy.mybatis.mapper.BookMapper">

    <resultMap id="BaseResultMap" type="org.javaboy.mybatis.model.Book">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
    </resultMap>
    
    <resultMap id="BookWithAuthor" type="org.javaboy.mybatis.model.Book" extends="BaseResultMap">
        <association property="author" javaType="org.javaboy.mybatis.model.Author">
            <id column="aid" property="id"/>
            <result column="aname" property="name"/>
            <result column="aage" property="age"/>
        </association>
    </resultMap>

    <select id="getBookById" resultMap="BookWithAuthor">
        SELECT b.*,a.`age` AS aage,a.`id` AS aid,a.`name` AS aname FROM book b,author a WHERE b.`aid`=a.`id` AND b.`id`=#{id}
    </select>

</mapper>

懶載入

上面這種載入方式,是一次性的讀取到所有資料。然後在 resultMap 中做對映。如果一對一的屬性使用不是很頻繁,可能偶爾用一下,這種情況下,我們也可以啟用懶載入。

懶載入,就是先查詢 book,查詢 book 的過程中,不去查詢 author,當用戶第一次呼叫了 book 中的 author 屬性後,再去查詢 author。

例如,我們再來定義一個 Book 的查詢方法:

Book getBookById2(Integer id);
Author getAuthorById(Integer id);

接下來,在 mapper 中定義相應的 SQL:

<resultMap id="BaseResultMap" type="org.javaboy.mybatis.model.Book">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
</resultMap>

<resultMap id="BookWithAuthor2" type="org.javaboy.mybatis.model.Book" extends="BaseResultMap">
    <association property="author" javaType="org.javaboy.mybatis.model.Author"
                 select="org.javaboy.mybatis.mapper.BookMapper.getAuthorById" column="aid" fetchType="lazy"/>
</resultMap>

<select id="getBookById2" resultMap="BookWithAuthor2">
    select * from book where id=#{id};
</select>

<select id="getAuthorById" resultType="org.javaboy.mybatis.model.Author">
    select * from author where id=#{aid};
</select>

這裡,定義 association 的時候,不直接指定對映的欄位,而是指定要執行的方法,通過 select 欄位來指定,column 表示執行方法時傳遞的引數欄位,最後的 fetchType 表示開啟懶載入。

當然,要使用懶載入,還需在全域性配置中開啟:

<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

一對多查詢

一對多查詢,也是一個非常典型的使用場景。比如使用者和角色的關係,一個使用者可以具備多個角色。

首先我們準備三個表:

/*
Navicat MySQL Data Transfer

Source Server         : localhost
Source Server Version : 50717
Source Host           : localhost:3306
Source Database       : security

Target Server Type    : MYSQL
Target Server Version : 50717
File Encoding         : 65001

Date: 2018-07-28 15:26:51
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '資料庫管理員');
INSERT INTO `role` VALUES ('2', 'admin', '系統管理員');
INSERT INTO `role` VALUES ('3', 'user', '使用者');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
SET FOREIGN_KEY_CHECKS=1;

這三個表中,有使用者表,角色表以及使用者角色關聯表,其中使用者角色關聯表用來描述使用者和角色之間的關係,他們是一對多的關係。

然後,根據這三個表,建立兩個實體類:

public class User {
    private Integer id;
    private String username;
    private String password;
    private List<Role> roles;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", roles=" + roles +
                '}';
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
public class Role {
    private Integer id;
    private String name;
    private String nameZh;

    @Override
    public String toString() {
        return "Role{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", nameZh='" + nameZh + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNameZh() {
        return nameZh;
    }

    public void setNameZh(String nameZh) {
        this.nameZh = nameZh;
    }
}

接下來,定義一個根據 id 查詢使用者的方法:

User getUserById(Integer id);

然後,定義該方法的實現:

<resultMap id="UserWithRole" type="org.javaboy.mybatis.model.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <collection property="roles" ofType="org.javaboy.mybatis.model.Role">
        <id property="id" column="rid"/>
        <result property="name" column="rname"/>
        <result property="nameZh" column="rnameZH"/>
    </collection>
</resultMap>

<select id="getUserById" resultMap="UserWithRole">
    SELECT u.*,r.`id` AS rid,r.`name` AS rname,r.`nameZh` AS rnameZh FROM USER u,role r,user_role ur WHERE u.`id`=ur.`uid` AND ur.`rid`=r.`id` AND u.`id`=#{id}
</select>

在 resultMap 中,通過 collection 節點來描述集合的對映關係。在對映時,會自動將一的一方資料集合並,然後將多的一方放到集合中,能實現這一點,靠的就是 id 屬性。

當然,這個一對多,也可以做成懶載入的形式,那我們首先提供一個角色查詢的方法:

List<Role> getRolesByUid(Integer id);

然後,在 XML 檔案中,處理懶載入:

<resultMap id="UserWithRole" type="org.javaboy.mybatis.model.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <collection property="roles" select="org.javaboy.mybatis.mapper.UserMapper.getRolesByUid" column="id" fetchType="lazy">
    </collection>
</resultMap>

<select id="getUserById" resultMap="UserWithRole">
    select * from user  where id=#{id};
</select>

<select id="getRolesByUid" resultType="org.javaboy.mybatis.model.Role">
    SELECT r.* FROM role r,user_role ur WHERE r.`id`=ur.`rid` AND ur.`uid`=#{id}
</select>

定義完成之後,我們的查詢操作就實現了懶載入功能。

查詢快取

Mybatis 一級快取的作用域是同一個 SqlSession,在同一個 sqlSession 中兩次執行相同的 sql 語句,第一次執行完畢會將資料庫中查詢的資料寫到快取(記憶體),第二次會從快取中獲取資料將不再從資料庫查詢,從而提高查詢效率。當一個 sqlSession 結束後該 sqlSession 中的一級快取也就不存在了。Mybatis 預設開啟一級快取。

public class Main2 {
    public static void main(String[] args) {
        SqlSessionFactory instance = SqlSessionFactoryUtils.getInstance();
        SqlSession sqlSession = instance.openSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUserById(1);
        user = userMapper.getUserById(1);
        user = userMapper.getUserById(1);
        System.out.println(user.getUsername());
    }
}

多次查詢,只執行一次 SQL。但是注意,如果開啟了一個新的 SqlSession,則新的 SqlSession 無法就是之前的快取,必須是同一個 SqlSession 中,快取才有效。

Mybatis 二級快取是多個 SqlSession 共享的,其作用域是 mapper 的同一個 namespace,不同的 sqlSession 兩次執行相同 namespace 下的 sql 語句且向 sql 中傳遞引數也相同即最終執行相同的 sql 語句,第一次執行完畢會將資料庫中查詢的資料寫到快取(記憶體),第二次會從快取中獲取資料將不再從資料庫查詢,從而提高查詢效率。Mybatis 預設沒有開啟二級快取需要在 setting 全域性引數中配置開啟二級快取。