天天看點

MyBatis

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 架構:

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 接口相同的目錄:

MyBatis

這樣,我們就不需要在 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 在全局範圍内啟用或禁用延遲加載。禁用時,所有查詢将熱加載
aggressiveLazyLoading 啟用時,有延遲加載屬性的對象将被完全加載後調用懶惰的任何屬性。否則,每一個屬性是按需加載。
multipleResultSetsEnabled 允許或不允許從一個單獨的語句(需要相容的驅動程式)要傳回多個結果集。
useColumnLabel 使用列标簽,而不是列名。在這方面,不同的驅動有不同的行為。參考驅動文檔或測試兩種方法來決定你的驅動程式的行為如何。
useGeneratedKeys 允許 JDBC 支援生成的密鑰。相容的驅動程式是必需的。此設定強制生成的鍵被使用,如果設定為 true,一些驅動會不相容性,但仍然可以工作。 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。
mapUnderscoreToCamelCase 從經典的資料庫列名 A_COLUMN 啟用自動映射到駱駝辨別的經典的 Java 屬性名 aColumn。
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。
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
proxyFactory 指定代理工具,MyBatis将會使用建立懶加載能力的對象。 CGLIB JAVASSIST

typeAliases

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

MyBatis 自帶的别名

别名 映射的類型
_byte byte
_long long
_short short
_int int
_integer
_double double
_float float
_boolean boolean
string String
Byte
Long
Short
Integer
integer
Double
Float
Boolean
date Date
decimal 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 任何相容的數字或十進制小數類型
StringTypeHandler CHAR和VARCHAR類型
ClobTypeHandler CLOB和LONGVARCHAR類型
NStringTypeHandler NVARCHAR和NCHAR類型
NClobTypeHandler NCLOB類型
ByteArrayTypeHandler byte[] 任何相容的位元組流類型
BlobTypeHandler BLOB和LONGVARBINARY類型
DateTypeHandler Date(java.util) TIMESTAMP類型
DateOnlyTypeHandler DATE類型
TimeOnlyTypeHandler TIME類型
SqlTimestampTypeHandler Timestamp(java.sql)
SqlDateTypeHandler Date(java.sql)
SqlTimeTypeHandler Time(java.sql)
ObjectTypeHandler 任意 其他或未指定類型
EnumTypeHandler Enumeration類型 VARCHAR-任何相容的字元串類型,作為代碼存儲(而不是索引)。

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

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

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

首先,在資料表中添加一個 favorites 字段:

MyBatis

然後,在 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 集合存入到資料庫中之後,就變成一個字元串了:

MyBatis

讀取的配置,有兩個地方,一個可以在 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 映射檔案,是 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 全局參數中配置開啟二級緩存。

上一篇: SSM整合
下一篇: SpringMVC

繼續閱讀