Mybatis筆記

一、概述

框架:封裝通用功能,軟體開發中的半成品,簡化開發過程

輕量級的,持久層框架,負責完成java和資料庫的通訊。

程式碼分布:DAO+Service

MyBatis 本是apache的一個開源項目iBatis, 2010年這個項目由apache software foundation 遷移到了google code,並且改名為MyBatis 。2013年11月遷移到Github。

iBATIS一詞來源於「internet」和「abatis」的組合,是一個基於Java的持久層框架。iBATIS提供的持久層框架包括SQL Maps和Data Access Objects(DAO)

1. 誕生背景

Java的原生資料庫通訊API( jdbc ),使用過於繁瑣,而且隨著數據變得複雜越發變得繁冗。

如下一個極其簡單的查詢動作,足以說明問題:

// 1> 自己管理 所有底層對象:驅動類,Connection,Statement,ResultSet
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("mysql:jdbc://localhost:3306/db9?\
                                                     useUnicode=true&characterEncoding=utf8");
PreparedStatement preparedStatement = connection.prepareStatement("select id,name,age from t_user");
ResultSet resultSet = preparedStatement.executeQuery();
// 2> 自己處理數據轉換過程:【 數據表格 ==> java對象 】
ArrayList<User> users = new ArrayList<User>();
while(resultSet.next()){
	Integer id = resultSet.getInt("id");
    String name = resultSet.getString("name");
    Integer age = resultSet.getInt("age");
    User user = new User(id,name,age);
    users.add(user);
}

2. ORM

概念:Object Relational Mapping,對象關係映射。

目標:在【java對象】 和 【關係表】 建立映射

:簡化兩者之間的通訊過程。可以直接通訊。( ops: 如上的過程不再用自己處理 )

細節:ORM是持久層框架(MyBatis,Hibernate),的重要底層設計思路。為簡化持久層開發提供驅動

: java持久層框架將jdbc納入底層,然後上層架設orm,使開發者脫離jdbc的繁瑣

MyBatis對ORM的踐行方式:

二、編碼過程

  • Jdk環境:jdk1.8
  • 資料庫環境:MySQL 5.1
  • Mybatis:3.4.5

1. 搭建流程

1.1 導入依賴

// pom.xml
<!-- mybatis 依賴 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
</dependency>
<!-- mysql驅動 依賴 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.25</version>
</dependency>
//pom.xml,使得src/main/java下的xml文件可以進入編譯範圍
    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

1.2 定義O和R

create table t_user(
    id int primary key auto_increment,
    name varchar(20),
    gender char(1),-- tinyint(1)
    create_time datetime
)default charset=utf8 engine=innodb;
class User{
    private Integer id;
    private String name;
    private Boolean gender;
    private Date createTime;
    //get/set....
}

1.3 M-定義映射文件

DAO介面定義不變,映射文件即Mapper文件替換之前的DAO實現

public interface UserDAO {
    public List<User> queryAll();
    public User queryOne(@Param("id") Integer id);
    public List<User> queryManyByName(@Param("name")  String name);
    public List<User> queryManyByDate(@Param("createTime") Date craeteTime);
    // 明確為參數定義別名,用於mapper文件中獲取
    public List<User> queryUserByIdAndName(@Param("id") Integer id, @Param("name") String name);
}
// UserDAO.xml 重點搞清楚 何為映射
<!-- 不用定義dao的實現類,此映射文件等價於dao的實現 -->
<!-- namespace="dao介面路徑"-->
<?xml version="1.0" encoding="UTF-8"?>
<!-- dtd:document type definition 配置文件規範 -->
<!DOCTYPE mapper 
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhj.dao.UserDAO">
    <!--
    根據id查詢用戶,User queryOne(Integer id)
        select:配置查詢語句
            id:可以通過id找到執行的statement,對應dao中的一個方法名
 parameterType: 參數類型 【int long float double string boolean date 自建類型(com.zhj.domain.User)】
    resultType:結果類型,查詢的結果會被封裝到對應類型的對象中
           #{}:相當於佔位符
          #{id}:就是把 「queryOne」 方法的名為id的參數填充到佔位上
		
     -->
    <select id="queryOne" parameterType="int" resultType="com.zhj.domain.User">
        select id as id,name as name,gender as gender,create_time as createTime
        from t_user
        where id=#{id}
    </select>

    <!-- 注意:返回類型實際是List<User> , 但此處只需定義User即可 -->
    <select id="queryAll" resultType="com.zhj.domain.User">
        select id,name,gender,create_time as createTime
        from t_user
    </select>

    <select id="queryManyByName" parameterType="string" resultType="com.zhj.domain.User">
        select id,name,gender,create_time as createTime
        from t_user
        where name like #{name}
    </select>
    <select id="queryManyByDate" parameterType="date" resultType="com.zhj.domain.User">
        select id,name,gender,create_time as createTime
        from t_user
        where create_time=#{createTime}
    </select>
    
    <!-- 注意:此時方法有多個參數,【#{xx}】取值時,需要@param支援 -->
    <select id="queryUsers" resultType="com.qianfeng.pojo.User" 
            parameterType="com.qianfeng.pojo.User">
        select id,name,gender,birth from t_user where id=#{id} or name like #{name}
    </select>
</mapper>

1.4 搭建配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 使用 id為「development」的環境資訊 -->
    <environments default="development">
        <environment id="development">
            <!-- 配置JDBC事務控制,由mybatis進行管理 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置數據源,採用mybatis連接池 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <!-- 【&】是特殊字元,【&amp;】是【&】的轉義 -->
                <property name="url" value="jdbc:mysql://localhost:3306/db9?useUnicode=true&amp;characterEncoding=utf8" />
                <property name="username" value="root" />
                <property name="password" value="111111" />
            </dataSource>
        </environment>
    </environments>
    <!-- 載入映射文件 -->
    <mappers>
        <!-- 使用資源的路徑
        <mapper resource="com/zhj/dao/UserDAO.xml"/> -->
        <!-- 載入某個包下的映射文件 (推薦)
            要求:Mapper介面的名稱與映射文件名稱一致 且 必須在同一個目錄下 -->
        <package name="com.zhj.dao"/>
    </mappers>
</configuration>

2. 測試使用

2.1 核心API

  • SqlSessionFactoryBuilder:該對象負責載入MyBatis配置文件 並 構建SqlSessionFactory實例
  • SqlSessionFactory:每一個MyBatis的應用程式都以一個SqlSessionFactory對象為核心。負責創建SqlSession對象實例。
  • SqlSession:等價於jdbc中的Connection,用於完成數據操作。
//1、讀取配置文件
String resource = "configuration.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//2、根據配置文件創建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//3、SqlSessionFactory創建SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();

2.2 數據操作

  • 通過SqlSession獲得DAO實現(Mapper)
  • 通過DAO實現完成具體的數據操作
// 獲得UserDAO的實現
// UserDAO userMapper  = sqlSession.getMapper(UserDAO.class);
UserDAO userDAO  = sqlSession.getMapper(UserDAO.class);
//根據id查詢
userDAO.queryOne(1);
//查詢所有
userDAO.queryAll();
//根據姓名模糊查詢
userDAO.queryManyByName("%zhj%");
//根據日期查詢
Date date = new GregorianCalendar(2019, 11, 12).getTime();
userDAO.queryManyByDate(date);

回收資源:操作最後,關閉sqlSession

sqlSession.close();//關閉sqlSession

2.3 分頁操作

定義分頁資訊類:

class Page{
    private Integer pageNum; //頁號
    private Integer pageSize; //每頁幾條數據
    private Integer offset; //偏移量,即從哪條查起
    //offset的get方法中動態計算偏移量
    public Integer getOffset() {
        return (pageNum-1)*pageSize;
    }
    //其他set/get
}

定義DAO介面:

interface User{
    ...
	public List<User> queryUserByPage(Page page);
}

定義Mapper:

<!-- #{offset} 取值時實際是會調用Page類中的getOffset()方法。 -->
<select id="queryUserByPage" parameterType="com.zhj.domain.Page" resultType="User">
	select id,name,gender,create_time from t_user
    limit #{offset},#{pageSize}
</select>

測試:

UserDAO userDAO = sqlSession.getMapper(UserDAO.class);
Page page = new Page(3,3);//第3頁,每頁3條
List<User> users = userDAO.queryUserByPage(page);

3. 增刪改操作

3.1 DAO介面

public interface UserDAO {
    ...
    public Integer insertUser(User user);
    public Integer deleteUser(Integer id);
    public Integer updateUser(User user);
}

3.2 映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace="dao介面路徑"-->
<mapper namespace="com.zhj.dao.UserDAO">
    ....
    ....
    <!-- 添加用戶
        1>注意:#{方法參數對象的屬性名}
        2>parameterType 和 resultType中都可以使用 預設包
        3>增加時id補全兩種方式
     -->
    <insert id="insertUser" parameterType="com.zhj.domain.User">
        <selectKey resultType="int" keyProperty="id" order="AFTER">
            SELECT last_insert_id()
        </selectKey>
        insert into tt2 (name,gender,create_time)
        values(#{name},#{gender},#{createTime})
    </insert>
    <insert id="insertUser2" parameterType="com.zhj.domain.User">
        insert into tt2 (name,gender,create_time)
        values(#{name},#{gender},#{createTime})
    </insert>

    <!--
        刪除用戶
     -->
    <delete id="deleteUser" parameterType="int">
        delete from tt2
        where id=#{id}
    </delete>

    <!--
        修改用戶
     -->
    <update id="updateUser" parameterType="com.zhj.domain.User">
        update tt2 set name=#{name},gender=#{gender},create_time=#{createTime}
        where id= #{id}
    </update>
</mapper>

3.3 測試使用

注意:增刪改操作需要控制事務,否則操作不會同步到資料庫

//事務控制
SqlSession sqlSession =  sqlSessionFactory.openSession();// 隨著session獲得,事務會自動開啟
.... //數據操作
.... //數據操作
sqlSession.commit();//提交事務
sqlSession.rollback();//回滾事務

增刪改操作

try{
    ....
	userDAO.updateUser(user);//除了需要控制事務之外,和查詢操作使用無異
    /**User u = new User(...);
    userDAO.insertUser(u);**/
    /**userDAO.deleteUser(1)**/
	sqlSession.commit();//提交事務
}catch(Exception e){
    e.printStackTrace();
    sqlSession.rollback();//回滾事務
}finally{
    if(sqlSession!=null){
    	sqlSession.close();//回收資源
    }
}

3.4 增加細節

ID缺失!

User user = new User("zhj",true,new Date());//此時user沒有id
userDAO.insertUser(user);
sqlSession.commit();//此時數據已經插入到資料庫中,資料庫中有id,但user依然沒有id
System.out.println(user.getId());//沒有id
//則無法得知插入的數據是哪一條,如果後續程式需要此id,則出現bug!

兩種解決方案:

1> selectKey標籤

2> useGenerateKeys 和 keyProperty屬性

<insert id="insertUser" parameterType="com.zhj.domain.User">
	<!-- AFTER:此中語句在插入語句之後執行
         resultType=「int」: 此中語句執行後的返回類型是 int
         keyProperty="id": 此中語句返回值要 傳遞給當前方法參數中的id屬性 (com.zhj.domain.User的id屬性)
         select last_insert_id():mysql特性語句,返回當前事務中,最近一次插入的數據的id-->        
    <selectKey resultType="int" keyProperty="id" order="AFTER">
        select last_insert_id()
    </selectKey>
    insert into t_user (name,gender,create_time)
    values(#{name},#{gender},#{createTime})
</insert>
<!-- useGeneratedKeys="true" 聲明此處添加中id用的是自動增長
     keyProperty="id" 將id值 傳遞給當前方法的參數中的id屬性 (com.zhj.domain.User的id屬性)-->
<insert id="insertUser" parameterType="com.zhj.domain.User" useGeneratedKeys="true" 
        keyProperty="id">
    insert into t_user (name,gender,create_time)
    values(#{name},#{gender},#{createTime})
</insert>

三、別名

在mapper文件中,parameterTyperesultType中使用類型時:

<select id="xxx" parameterType="com.zhj.domain.Page" resultType="com.zhj.domain.User">

除mybatis自動映射的類型外,其他類型都要定義完整路徑,相對繁瑣。可用如下兩種方式簡化:

// configuration.xml
<configuration>
    ...
    <typeAliases>
        <!-- 1. 單獨為每個類定義別名,則 "Page"等價於"com.zhj.domain.Page"
		<typeAlias type="com.zhj.domain.Page" alias="Page"/>
        <typeAlias type="com.zhj.domain.User" alias="User"/>-->
        <!-- 2. 定義預設包,當mapper中的類型沒有定義包時,使用此配置作為默認包;
 			 則 「Page」 會自動認為是 「com.zhj.domain」中的「Page」,即「com.zhj.domain.Page」
			 則 「User」 會自動認為是 「com.zhj.domain」中的「User」,即「com.zhj.domain.User」-->
        <package name="com.zhj.domain"/>
    </typeAliases>
    ...
</configuration>

有如上別名配置後,mapper中:

<select id="xxx" parameterType="Page" resultType="User">

四、參數分離

在mybatis的配置文件中有一項重要且可能需要經常改動的配置,即,資料庫連接參數。

可以單獨進行管理,方便後續維護。

<!-- 如下四項參數 -->
<dataSource type="POOLED">
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/db9?useUnicode=true&amp;characterEncoding=utf8"/>
    <property name="username" value="root" />
    <property name="password" value="111111" />
</dataSource>

1. 單獨定義參數文件

# 在resources目錄下,創建 jdbc.properties文件
# 參數名=參數值
jdbc.user=root
jdbc.password=111111
jdbc.url=jdbc:mysql://localhost:3306/db9?useUnicode=true&characterEncoding=utf8
jdbc.driver=com.mysql.jdbc.Driver

2. 載入參數文件

// 在 configuration.xml中
<configuration>
    <!-- 載入參數文件 -->
    <properties resource="jdbc.properties"></properties>
    ....
    ....
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <!-- 使用 ${參數名} 獲取參數文件中的值。如此這四項參數需要修改時,需要改動參數文件即可 -->
                <property name="driver" value="${jdbc.driver}" />
                <property name="url" value="${jdbc.url}" />
                <property name="username" value="${jdbc.user}" />
                <property name="password" value="${jdbc.password}" />
            </dataSource>
        </environment>
    </environments>

五、關聯關係

項目中的表都不是孤立的,會彼此關聯。在操作數據時,需要聯合多張表。

我們需要將表間的關係映射清楚,mybatis可以提供很好的多表聯合操作支援,比如:

查詢所有用戶及其所有訂單 查詢所有學生及其所學課程 等操作,可以省去很多冗繁程式碼,直接拿到結果。

關聯關係種類:1對1 1對多 多對多

Person(公民) Passport(護照) 1對1

User(用戶) Order(訂單) 1對多

Student(學生) Course(課程) 多對多

1. 一對一

1.1 建表

drop table IF EXISTS t_passport;
drop table IF EXISTS t_person;
create table t_person(
  id int primary key AUTO_INCREMENT,
  name VARCHAR(50),
  age SMALLINT
)DEFAULT CHARSET = utf8 ENGINE =innodb;

create table t_passport(
  id int primary key AUTO_INCREMENT,
  note VARCHAR(50),
  create_time DATE,
  person_id int UNIQUE,
  FOREIGN KEY(person_id) REFERENCES t_person(id)
)DEFAULT CHARSET = utf8 ENGINE =innodb;

insert into t_person(name,age) values("zhangsan",18);
insert into t_person(name,age) values("lisi",19);
insert into t_passport(note,create_time,person_id) values("pass1",'2019-05-07',1);
insert into t_passport(note,create_time,person_id) values("pass2",'2019-05-07',2);

1.2 建類

public class Person {
    private Integer id;
    private String name;
    private Integer age;
    //關係屬性
    private Passport passport;
    //set/get
}
public class Passport {
    private Integer id;
    private String note;
    private Date createTime;
    //關係屬性
    private Person person;
    //set/get
}

1.3 查詢

查詢某個Person,及其Passport

//PersonDAO :查詢某個Person的資訊及其Passport的資訊
public Person queryPersonAndPassport(Integer id);
//映射文件
<select id="queryPersonAndPassport" resultType="Person" parameterType="int">
    select
        t_person.id as pid,
        name,
        age,
        t_passport.id as passid,
        note,
        create_time,
        person_id
    from t_person join t_passport
    ON t_person.id = t_passport.person_id
    where t_person.id=#{id};
</select>
如上映射文件,存在問題 ==> 關係屬性:passport,需要多個列映射到這一個屬性上,但並沒有映射清楚

若要完成上圖中的映射,不能再使用默認的同名映射規則,需要訂製映射規則。

resultMap— 映射規則訂製利器

<!-- 定義查詢中的各個列 與 屬性的映射規則 -->
<!-- id=標識   type=返回類型(將列映射到User的屬性上) -->
<resultMap id="person_passport" type="Person">
    <!-- 主鍵列pid,用 id標籤配置; property=屬性名  column=列名 -->
    <id property="id" column="pid"/>
    <!-- 常規列用 result標籤配置; property=屬性名  column=列名 -->
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <!-- 重點:關係屬性【Passport passport】是一個對象
		 用 association標籤配置,如下配置就是在完成上圖中 未完成之映射
         property=屬性名  javaType=該屬性的類型 -->
    <association property="passport" javaType="Passport">
        <!-- 將passid列 映射到 passport的id屬性中 -->
        <id property="id" column="passid"/>
        <!-- 將note列 映射到 passport的note屬性中 -->
        <result property="note" column="note"/>
        <!-- 將create_time列 映射到 passport屬性的createTime屬性中 -->
        <result property="createTime" column="create_time"/>
    </association>
</resultMap>

<!-- 刪除之前的resultType, 改為使用resultMap="person_passport" 
     即,採用resultMap中定義的映射規則去封裝對象-->
<select id="queryPersonAndPassport" resultMap="person_passport" parameterType="int">
    select
        t_person.id as pid,
        name,
        age,
        t_passport.id as passid,
        note,
        create_time,
        person_id
    from t_person join t_passport
    ON t_person.id = t_passport.person_id
    where t_person.id=#{id};
</select>

1.4 測試

// 查詢id=1的person及其passport
Person person = personDAO.queryPersonAndPassport(1);
// 從person中獲取passport
Passport passport = person.getPassport();

**思考題 ==> 請自主設計,完成以下: **

查詢所有Person及其Passport:public List<Person> queryAll();

查詢某個Passport及其Person數據:public Passport queryPassportAndPerson(Integer id)

細節:

增刪改,沒有任何特別需要改動的,正常定義<insert> <update> <delete> 即可

2. 一對多

2.1 建表

drop table IF EXISTS t_order;
drop table IF EXISTS t_user;
create table t_user( # 用戶表 
  id int primary key AUTO_INCREMENT,
  name VARCHAR(50),
  gender char(1),
  regist_time DATE
)DEFAULT CHARSET = utf8 ENGINE =innodb;

create table t_order( # 訂單表
  id int primary key AUTO_INCREMENT,
  price VARCHAR(50),
  note VARCHAR(50),
  create_time DATE,
  user_id int,
  FOREIGN KEY(user_id) REFERENCES t_user(id)
)DEFAULT CHARSET = utf8 ENGINE =innodb;
insert into t_user(name,gender,regist_time) values('zhangsan','1','2019-12-12');
insert into t_user(name,gender,regist_time) values('lisi','0','2019-12-11');
insert into t_order(price,note,create_time,user_id) values(3000.55,'哈哈','2019-12-12',1);
insert into t_order(price,note,create_time,user_id) values(600.55,'哈哈2','2019-12-12',1);
insert into t_order(price,note,create_time,user_id) values(900.55,'呵呵2','2019-12-12',2);
insert into t_order(price,note,create_time,user_id) values(4000.55,'呵呵2','2019-12-12',2);

2.2 建類

public class User {
    private Integer id;
    private String name;
    private Boolean gender;
    private Date registTime;
    //關係屬性,存儲用戶的多個訂單
    private List<Order> orders;
    //set/get
}
public class Order {
    private Integer id;
    private BigDecimal price;
    private String note;
    private Date createTime;
    //關係屬性
    private User user;
    //set/get
}

2.3 查詢

//DAO
public queryOneUserAndOrders(Integer id);//根據用戶id查詢用戶及其所有訂單
<!-- 按照之前的思路,映射如此定義,但此時,resultType為User,所以只能接收User本身的資訊!
     那如何將查詢中得到的Order資訊也保存到User中呢?
     
	 User中保存Order資訊的屬性名為「orders」,則此屬性需要有一個和它同名的列才可以接收值!
	 但是該屬性的值又不可能來自某一個列,而是多個列的值!

	 我們該如何配置,才可以將多列的值對應到一個屬性上呢?
-->
<select id="queryOneUserAndOrders" parameterType="int" resultType="User">
	select t_user.id as uid,
			name,
			gender,
		    regist_time as registTime,
		   t_order.id as oid,
    		note,price,
    		create_time as createTime
    from t_user join t_order
    ON t_user.id = t_order.user_id
    where t_user.id=#{id}
</select>
如上映射文件,存在問題 ==> 關係屬性:orders,需要多個列映射到這一個屬性上,但並沒有映射清楚

若要完成上圖中的映射,不能再使用默認的同名映射規則,需要訂製映射規則。

resultMap— 映射規則訂製利器

<!-- 定義查詢中的各個列 與 屬性的映射規則 -->
<!-- id=標識   type=最終返回類型(將列映射到User的屬性上) -->
<resultMap id="user_orders" type="User">
    <!-- 主鍵列單獨用id標籤標識
    	 property=類中屬性名  column=查詢中的列名-->
    <id property="id" column="uid"/>
    <!-- 常規屬性都用result標籤 -->
    <result property="name" column="name"/>
    <result property="gender" column="gender"/>
    <result property="registTime" column="registTime"/>
    <!-- 重點:關係屬性【List<Order> orders】,非主鍵也非常規屬性,而是一個List
         用collection標籤配置,用於完成上圖中未完成之映射。
         property=屬性名   ofType=集合中的泛型類型-->
    <collection property="orders" ofType="Order">
        <!-- Order的主鍵列 -->
        <id property="id" column="oid"/>
        <!-- 常規屬性用result標籤 -->
        <result property="note" column="note"/>
        <result property="price" column="price"/>
        <result property="createTime" column="createTime"/>
    </collection>
</resultMap>
<!-- 此處是resultType換為resultMap,進而引用了如上定義的「user_orders」 -->
<select id="queryOneUserAndOrders" parameterType="int" resultMap="user_orders"> 
    select t_user.id as uid,
			name,
			gender,
		    regist_time as registTime,
		   t_order.id as oid,
    		note,price,
    		create_time as createTime
    from t_user join t_order
    ON t_user.id = t_order.user_id
    where t_user.id=#{id}
</select>

2.4 測試

//查詢用戶1,及其訂單資訊
User user = userDAO.queryOneUserAndOrders(1);
//獲取用戶1的訂單資訊
List<Order> orders = user.getOrders();

**思考題 ==> 請自主設計,完成以下: **

查詢所有User及其Order:public List<User> queryAll();

查詢某個Order及其User:public Order queryOrderAndUser(Integer orderid)

細節:

增刪改,沒有任何特別需要改動的,正常定義<insert> <update> <delete> 即可

3. 多對多

3.1 建表

drop table IF EXISTS t_student;
drop table IF EXISTS t_course;
drop table IF EXISTS t_student_course;

create table t_student(
  id int primary key AUTO_INCREMENT,
  name VARCHAR(50),
  age SMALLINT
)DEFAULT CHARSET = utf8 ENGINE =innodb;

create table t_course(
  id int primary key AUTO_INCREMENT,
  title VARCHAR(50)
)DEFAULT CHARSET = utf8 ENGINE =innodb;

create table t_student_course(
  student_id int,
  course_id int,
  FOREIGN KEY (student_id) REFERENCES t_student(id),
  FOREIGN KEY (course_id) REFERENCES t_course(id),
  PRIMARY KEY (student_id,course_id)
)DEFAULT CHARSET = utf8 ENGINE =innodb;

insert into t_student (name,age) values("zhangsan",18);
insert into t_student (name,age) values("lisi",19);
insert into t_course (title) values("java基礎");
insert into t_course (title) values("mybatis");
insert into t_student_course values(1,1);
insert into t_student_course values(1,2);
insert into t_student_course values(2,1);
insert into t_student_course values(2,2);

3.2 建類

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    //關係屬性
    private List<Course> courses;
    //set/get
}
public class Course {
    private Integer id;
    private String title;
    //關係屬性
    private List<Student> students;
    //set/get
}
public class StudentCourse {
    private Integer studentId;
    private Integer courseId;
    //set/get
}

3.3 查詢

//StudentDAO
public List<Student> queryStudents();
//StudentDAO.xml 映射文件
<!-- 定義映射規則 和一對多中使用一樣-->
<resultMap id="student_course" type="Student">
    <id column="sid" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <!-- 此處使用collection和一對多中一樣 -->
    <collection property="courses" ofType="Course">
        <id column="cid" property="id"/>
        <result column="title" property="title"/>
    </collection>
</resultMap>
<!-- 查詢,並根據映射規則完成數據封裝 -->
<select id="queryStudents" resultMap="student_course">
    select
        t_student.id as sid,
        name,
        age,
        t_course.id as cid,
        title
    from t_student join t_student_course
    ON t_student.id = t_student_course.student_id
    JOIN t_course
    ON t_student_course.course_id = t_course.id;
</select>

3.4 測試

StudentDAO studentDAO = sqlSession.getMapper(StudentDAO.class);
//查詢所有學生及其所有課程
List<Student> students = studentDAO.queryStudents();
//獲取每個學生的所有課程
for(Student stu:students){
    System.out.println(stu);
    for(Course cour:stu.getCourses()){
        System.out.println(cour);
    }
}

3.5 增加測試

//StudentDAO.xml
<mapper namespace="com.zhj.dao.StudentDAO">
    <!-- 增加Student到 t_student表                     ( ops:後續操作需要新增學生的id )-->
    <insert id="insertStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="id">
        insert into t_student(name,age) values(#{name},#{age})
	</insert> 
</mapper>
//StudentCourseDAO.xml
<mapper namespace="com.zhj.dao.StudentCourseDAO">
	<!-- 為學生添加課程 或 為課程添加學生 -->
    <insert id="insertStudentCourse" parameterType="StudentCourse">
        insert into t_student_course(student_id,course_id) values(#{studentId},#{courseId})
    </insert>
</mapper>
//添加學生 趙六,並為其關聯課程 1
// 獲得 學生DAO 和 學生課程DAO
StudentDAO studentDAO = sqlSession.getMapper(StudentDAO.class);
StudentCourseDAO scDAO = sqlSession.getMapper(StudentCourseDAO.class);
// 新建學生,並添加
Student student = new Student(null, "趙六", 22);
studentDAO.insertStudent(student);
// 新建學生趙六和課程1的關係,並添加 (為趙六添加課程 1)
StudentCourse sc = new StudentCourse(student.getId(),1);
scDAO.insertStudentCourse(sc);
// 提交
sqlSession.commit();

4. 關係的方向(了解)

單向關係:A B 雙方,A中有B,或B中有A

雙向關係:A B雙方,A中有B,且B中有A

//互相持有彼此,即為雙向關係
class User{
    ...
    private List<Order> orders;
}
class Order{
    ...
	private User user;
}
//Order不持有User,或User不持有Order,即為單向關係
class User{
    ...
    private List<Order> orders;
}
class Order{
    ...
}

六、# 和 $

1. 各自特點

//如果用$,則必須用 @Param註解,否則${name}會認為是要從參數中取名為name的屬性
public List<User> test3(@Param("name") String a);
<!-- 注意${} 就是在做字元拼接,所以此處用了【『${name}』】而不是【${name}】 
     類比jdbc的sql語句的拼接:
        String name="zhj";  
        String sql = "select ... from tt2 where name='"+name+"'";//此處是要加單引號的
     注意:sql拼接時,有sql注入的風險
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
    select id,name,gender,create_time as createTime
    from tt2
    where name = '${name}'
</select>
<!-- 注意#{} 就是在做佔位符,所以此處用了【#{name}】而不是【』#{name}『】 
     類比jdbc的sql語句的拼接:
        String name="zhj";  
        String sql = "select ... from tt2 where name=?「;//此處是不加單引號的
		...
		prepareStatement.executeQuery(sql,name);//佔位符賦值
-->
<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
    select id,name,gender,create_time as createTime
    from tt2
    where name = #{name}
</select>

2. 使用$場景

<select id="test3" parameterType="string" resultType="com.zhj.domain.User">
    select id,name,gender,create_time as createTime
    from tt2
    order by id ${name}
</select>
<select id="test4" parameterType="string" resultType="com.zhj.domain.User">
    select id,name,gender,create_time as createTime
    from ${tn}
    where name = #{name}
</select>
<!-- 用在列名上亦可 -->
userDAO.test3("desc");
userDAO.test3("asc");
userDAO.test4("t_user");
userDAO.test4("t_admin");

八、動態sql

在映射文件中,定義了要執行的sql語句,mybatis支援在sql語句中填充一些邏輯,是的sql語句可以呈現不同的語義,

即動態sql

1. IF

在sql中 注入 if ,可以讓sql更加靈活,讓一個查詢語句,可以應對更多查詢情景。

重點:== != > < >= <= and or

: 比較字元串需要加引號,比較數字、布爾、null不用加引號

常用:對參數是否為空的判斷,動態決定sql語句的組成

情景:對用戶可以有通過name 或 gender的搜索,如果沒有if動態邏輯,則是要定義多個<select>

<select id="queryUsers" parameterType="User" resultType="User">
    SELECT id,name,gender,regist_time
    FROM t_user
    WHERE
    <if test="name != null and name!=''">
        name=#{name}
    </if>
    <if test="gender != null">
        AND gender=#{gender}
    </if>
</select>
<!-- 如上如果gender為null,name不為null,則sql為:
     SELECT id,name,gender,regist_time
    FROM t_user
    WHERE name=#{name}
-->
<!-- 如上如果gender不為null,name不為null,則sql為:
     SELECT id,name,gender,regist_time
    FROM t_user
    WHERE name=#{name} AND gender=#{gender}
-->
<!-- 補充:比較的其他用法 -->
<select id="queryUsers" parameterType="User" resultType="User">
    SELECT id,name,gender,regist_time
    FROM t_user
    WHERE
    <if test="name == null or name=='zhj'">
        name=#{name}
    </if>
    <if test="id>=1">
        AND id>#{id}
    </if>
    <if test="gender == false">
        AND gender=#{gender}
    </if>
</select>

2. Choose

如果在多個判斷中,只會有一個是可以成立的,可以使用Choose標籤

<!-- 如果id不為空就按id查詢。如果id為空但name不為空就按name查詢。如果都為空,就查詢所有男性用戶。 -->
<select id="queryUsers" parameterType="User" resultType="User">
    SELECT id,name,gender,regist_time
    FROM t_user
    WHERE
    <choose> <!-- 從上到下,有任何一個when標籤,判斷成功則停止判斷 -->
        <when test="id != null">  <!-- 判斷 -->
            id > #{id}
        </when>
        <when test="name !=null"> <!-- 判斷 -->
            name = #{name}
        </when>
        <otherwise>  <!-- 以上判斷都不成立時,執行 -->
            gender = '1'
        </otherwise>
    </choose>
</select>

注意,在sql中的判斷,本意不在於判斷,而在於將sql動態的適應不同場景,簡化開發。

3. Where

動態sql在使用中,存在一些問題:

<select id="queryUsers" parameterType="User" resultType="User">
    SELECT id,name,gender,regist_time FROM t_user
    WHERE
    <if test="name != null">
        name=#{name}
    </if>
    <if test="id>=1">
        AND id>#{id}
    </if>
</select>
<!-- 如果 name=null,id=3,則sql變為:
     SELECT id,name,gender,regist_time FROM t_user
     WHERE AND id>#{id}  
-->
<!-- 如果 name=null,id=0,則sql變為:
     SELECT id,name,gender,regist_time FROM t_user
     WHERE 
-->

<where> 標籤中如果沒有成立的條件,則不會拼接where語句

<where> 標籤中如果以 and 或 or開頭,會去將其去除。

<select id="queryUsers" parameterType="User" resultType="User">
    SELECT id,name,gender,regist_time
    FROM t_user
    <where>
        <if test="name == null">
            name=#{name}
        </if>
        <if test="id>=1">
            AND id>#{id}
        </if>
    </where>
</select>
<!-- 如果 name=null,id=3,則sql變為:
     SELECT id,name,gender,regist_time FROM t_user
     WHERE id>#{id}  
-->
<!-- 如果 name=null,id=0,則sql變為:
     SELECT id,name,gender,regist_time FROM t_user 
-->

4. Set

<set>標籤主要用於 update

<!-- 只更新非空的欄位 -->
<update id="updateUser" parameterType="User">
    UPDATE t_user2
    SET
    <if test="name != null">
        name = #{name},
    </if>
    <if test="gender != null">
        gender = #{gender},
    </if>
    <if test="registTime != null">
        regist_time = #{registTime}
    </if>
    WHERE id = #{id}
</update>
<!-- 如果registTime=null,name或gender!=null,則sql為:
	 UPDATE t_user2
     SET name = #{name},gender = #{gender},
     WHERE id = #{id}
	 注意:多了逗號,sql語法錯誤
-->

使用 <set>:

<update id="updateUser" parameterType="User">
    UPDATE t_user2
    <set>
        <if test="name != null">
            name = #{name},
        </if>
        <if test="gender != null">
            gender = #{gender},
        </if>
        <if test="registTime != null">
            regist_time = #{registTime}
        </if>
    </set>
    WHERE id = #{id}
</update>
<!-- 如果registTime=null,name或gender!=null,則sql為:
	 UPDATE t_user2
     SET name = #{name},gender = #{gender}
     WHERE id = #{id}
	 注意:<set>會自動補充一個 」set語句「,並將最後一個逗號去除
-->

5. Trim

<select id="queryUsers" parameterType="User" resultType="User">
    SELECT id,name,gender,regist_time
    FROM t_user2
    <trim prefix="where" prefixOverrides="and|or">
        <if test="name != null">
            name=#{name}
        </if>
        <if test="id>=10">
            OR id>#{id}
        </if>
        <if test="gender == false">
            AND gender=#{gender}
        </if>
    </trim>
</select>
<!-- <trim prefix="where" prefixOverrides="and|or">  等價於 <where>
     prefix="where":會添加一個where關鍵字
     prefixOverrides="and|or":會去除開頭的and 或 or
-->
<update id="updateUser" parameterType="User">
    UPDATE t_user2
    <trim prefix="set" suffixOverrides=",">
        <if test="name != null">
            name = #{name},
        </if>
        <if test="gender != null">
            gender = #{gender},
        </if>
        <if test="registTime != null">
            regist_time = #{registTime}
        </if>
    </trim>
    WHERE id = #{id}
</update>
<!-- <trim prefix="set" suffixOverrides=","> 等價於 <set>
     prefix="set":會添加一個set關鍵字
	 suffixOverrides=",":會去除最後的逗號
-->

6. Foreach

批量查詢:id為 1,3,5的數據

//DAO聲明為: public List<User> queryUsers2(List<Integer> ids);
<select id="queryUsers2" resultType="User">
    SELECT id,name,gender,regist_time
    FROM t_user2
    WHERE id IN 
    <foreach collection="list" open="(" separator="," close=")" item="id" index="ind">
        #{id}
    </foreach>
</select>
<!--
	<foreach collection="list" open="(" separator="," close=")" item="id" index="ind">
    collection="list" 代表參數是List,如果是數組,要用array
    open="("   以「(」 開頭
	close=")"  以「)」 結尾
    separator=","   值之間用「,」分隔
    item="id"   每次遍歷的值的臨時變數
    #{id}   獲取每次遍歷的值
    如上:標籤的效果是 (值1,值2,值3)
    示例:如果傳入 List{1 3 5},則最終的sql:
    【SELECT id,name,gender,regist_time
      FROM t_user2
      WHERE id IN (1,3,5)】
-->

批量添加:

insert into t_user (name,gender) values(‘xx’,1),(‘xxx’,0),(‘xxxx’,1)

<!-- 批量添加 -->
<insert id="insertUsers" parameterType="java.util.List">
    insert into t_user2 (name,gender,regist_time) values
    <foreach collection="list" item="user" index="ind" close="" open="" separator=",">
        (#{user.name},#{user.gender},#{user.registTime})
    </foreach>
</insert>
List<User> users = ...;
System.out.println(userDAO.insertUsers(users));

7. Sql

復用sql語句

<!-- 定義一段sql -->
<sql id="order">
    id,note,price,create_time as createTime
</sql>
<!-- 引用sql -->
<select id="queryOrder" parameterType="int" resultType="Order">
    select 
    <include refid="order"/>
    from t_order
    where id = #{id}
</select>

九、快取

快取:將資料庫的數據臨時的存儲起來,以更好的支援查詢。

問題:如果有數據,查詢頻繁且更新極少,此種數據如果依然每次到資料庫查詢,效率偏低。

解決:將如上數據,臨時的存儲到記憶體中,提供對外界的查詢服務,進而減少和資料庫的通訊,提高查詢效率。

原理:當查詢數據時,查詢結果會被快取在某個記憶體區域中,核心存儲結構={sql:查詢結果};

每次發起查詢時,會先找到快取,從中嘗試獲取數據,如果沒有找到數據,再去查資料庫,並將在數**

庫中查到的結果存入快取,以供後續查詢使用。

MyBatis作為持久層框架,快取管理自然是他的本職工作。

支援了兩種快取:一級快取,二級快取

1. 一級快取

存儲位置:SqlSession;即一個SqlSession對象發起查詢後,查詢結果會快取在自己內部

有效範圍:同一個SqlSession的多次查詢;即,同一個SqlSession的多次相同sql的查詢可以使用一級快取

開啟:不用任何配置,默認啟動。

清除:sqlSession.clearCache();

2. 二級快取

2.1 概述

**存儲位置:SqlSessionFactory;同一個SqlSessionFactory創建的所有SqlSession發起的查詢,查詢結果都會快取在 **

SqlSessionFactory內部。

有效範圍:同一個SqlSessionFactory

開啟:默認開啟,但需要制定哪個DAO的Mapper需要使用二級快取,定義一個 <cache>即可

注意:二級快取必須在sqlSession.commit()sqlSession.close() 之後才生效

清除:sqlSession.rollback();//則查詢的結果不會進入二級快取

2.2 應用

二級快取使用:

<mapper namespace="com.zhj.dao.UserDAO">
    <!-- 當前mapper中的所有查詢,都進入二級快取 
         快取數據中涉及的pojo一定要實現  Serialiable。
    -->
    <cache></cache>
    <select>...</select>
    .....
</mapper>
UserDAO userDAO1 = sqlSession1.getMapper(UserDAO.class);
UserDAO userDAO2 = sqlSession2.getMapper(UserDAO.class);
userDAO1.queryOne(1);
userDAO2.queryOne(1);
// 在開啟了二級快取的情況下,如上程式碼依然會查詢兩次資料庫。
// userDAO1.queryOne(1);之後快取只在sqlSession1中,並未進入二級快取。userDAO2.queryOne(1);無法使用
UserDAO userDAO1 = sqlSession1.getMapper(UserDAO.class);
UserDAO userDAO2 = sqlSession2.getMapper(UserDAO.class);
userDAO1.queryOne(1);
sqlSession1.commit();//close()也可以,因為close內部流程和commit內部流程有對快取的相同處理
userDAO2.queryOne(1);
// 此時如上程式碼會只查詢一次資料庫。
// sqlSession1.commit();執行時,會將查到的數據序列化,存入二級快取中。userDAO2.queryOne(1)可以使用

思考題:如下程式碼會如何查詢資料庫 ?

SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();

UserDAO userDAO= sqlSession.getMapper(UserDAO.class);
UserDAO userDAO2= sqlSession2.getMapper(UserDAO.class);
UserDAO userDAO3= sqlSession3.getMapper(UserDAO.class);

userDAO.queryOne(1);
userDAO.queryOne(2);
sqlSession.close();

userDAO2.queryOne(3);
sqlSession2.close();

userDAO3.queryOne(1);
userDAO3.queryOne(2);
userDAO3.queryOne(3);
//請分析如上查詢,會觸發哪些資料庫查詢

2.3 結構

二級快取存儲結構

2.4 清除

二級快取是以 namespace 為單位組織的,當某個 namespace 中發生數據改動,則 namespace 中快取的所有數據會被mybatis清除。

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis_config.xml"));

SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserDAO userMapper = sqlSession.getMapper(UserDAO.class);
UserDAO userMapper2 = sqlSession2.getMapper(UserDAO.class);
UserDAO userMapper3 = sqlSession3.getMapper(UserDAO.class);

userMapper.queryUserById(1);
sqlSession.close();//二級快取生效

// userMapper中的所有二級快取被清除
userMapper2.updateUser(new User(1,"zs",true,new Date()));
sqlSession2.commit();

// 再次查詢,二級快取中已沒有數據,會查詢資料庫
userMapper3.queryUserById(1);

2.5 cache-ref

和關係屬性相關

注意如果<collection>中沒有使用select關聯查詢,則不存在此問題。

<mapper namespace="com.zhj.dao.UserDAO">
    <cache/>
    <resultMap id="user_orders" type="User">
        <id property="id" column="uid"/>
        <result property="name" column="name"/>
        <result property="gender" column="gender"/>
        <result property="registTime" column="registTime"/>
        <collection property="orders" select="com.zhj.dao.OrderDAO.queryOrderOfUser" column="id"              	       fetchType="eager/lazy"/>
    </resultMap>
    <select id="queryOne" parameterType="int" resultMap="user_orders">
        select id,name,gender,regist_time as registTime
        from t_user
        where id=#{id}
    </select>
</mapper>
<mapper namespace="com.zhj.dao.OrderDAO">
    <!-- 使用cache-ref 則OrderDAO的快取數據,會存放於com.zhj.dao.UserDAO分支下,
    	 與UserDAO的快取數據存儲於同一個Perpetual對象中
    -->
    <cache-ref namespace="com.zhj.dao.UserDAO"/>
    <select id="queryOrderOfUser" parameterType="int" resultType="Order">
        select id as oid,note,price,create_time as createTime
        from t_order
        where user_id = #{userId}
    </select>
</mapper>
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
//User 和 關係屬性Order 都被快取
User user = userDAO.queryOne(1);
user.getOrders().size();
sqlSession.commit();

OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //有二級快取數據可用 (OrderDAO中必須有 <cache>或<cache-ref>)
UserDAO userDAO = sqlSession1.getMapper(UserDao.class);
userDAO.queryOne(1);
//數據改動,此時會清空User快取,並清空Order快取(因為order中是 <cache-ref>,如果是<cache>則此處只會清空User快取)
userDAO.insertUser(new User(null,"new_user",true,new Date()));
sqlSession.commit();

OrderDAO orderDAO = sqlSession2.getMapper(OrderDAO.lass);
orderDAO.queryOrderOfUser(1); //重新查詢資料庫

十、PageHelper

1. 使用過程

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>RELEASE</version>
</dependency>
<!-- 
    plugins在配置文件中的位置必須符合要求,否則會報錯,順序如下:
    properties?, settings?, 
    typeAliases?, typeHandlers?, 
    objectFactory?,objectWrapperFactory?, 
    plugins?, 
    environments?, databaseIdProvider?, mappers?
-->
<plugins>
    <!-- com.github.pagehelper為PageHelper類所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 頁號自動回歸到合理數值 -->
        <property name="reasonable" value="true"/>
    </plugin>
</plugins>



<!-- spring等價配置 
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="plugins">
    <array>
      <bean class="com.github.pagehelper.PageInterceptor"></bean>
    </array>
  </property>
</bean>
-->
//使用:
PageHelper.startPage(2,3);// 第2頁,每頁3條數據,pageNum,pageSize
PageHelper.orderBy("id desc");//可以選擇設置排序(可選)
List<User> users = mapper.queryAllUsers();//PageHelper後的第一個查詢語句,會被PageHelp增強處理(可觀測mysql日誌)
for (User user : users) {// users中已經是分頁數據
    System.out.println(user);
}	
//包裝一個PageInfo,其中會持有所有分頁會用到的資訊:當前頁號,每頁多少條,共多少頁,是否為第一頁/最後一頁,是否有下一頁等。
PageInfo<User> pageInfo=new PageInfo<User>(users);
PageInfo對象 概覽
注意:如果是多表查詢,則會有如下效果:
select t_user.id,name,gender,regist_time,
            t_order.id orderId,price,note,create_time
        from t_user JOIN t_order
                ON t_user.id = t_order.user_id LIMIT 2, 2 
#是對大表做了分頁,此時數據可能不完整,比如用戶有10個訂單,卻只能查到2個,或部分。

2. 重要提示

PageHelper.startPage方法重要提示

只有緊跟在PageHelper.startPage方法後的第一個Mybatis的查詢(Select)方法會被分頁。

請不要配置多個分頁插件

請不要在系統中配置多個分頁插件(使用Spring時,mybatis-config.xmlSpring<bean>配置方式,請選擇其中一種,不要同時配置多個分頁插件)!

分頁插件不支援帶有for update語句的分頁

對於帶有for update的sql,會拋出運行時異常,對於這樣的sql建議手動分頁,畢竟這樣的sql需要重視。

Tags: