Mapper代理 "上一節" 中直接利用session+id來執行sql的方式存在一些問題 session執行sql時都需要提供要執行sql的id,而這個id是字元串類型,意味著id是否正確在編譯期間是無法獲知的,必須等到運行時才能發現錯誤, sql需要的參數和返回值類都不明確,這也增加了出錯的概率 ...
Mapper代理
上一節中直接利用session+id來執行sql的方式存在一些問題
- session執行sql時都需要提供要執行sql的id,而這個id是字元串類型,意味著id是否正確在編譯期間是無法獲知的,必須等到運行時才能發現錯誤,
- sql需要的參數和返回值類都不明確,這也增加了出錯的概率
理想的狀況:像調用方法一樣調用sql,既避免了直接寫id的問題,也可以明確指定方法的參數類型和返回值類型
解決方案:
MyBatis通過動態代理來解決,簡單的說動態代理(動態生成),就是在運行過程中自動產生一個對象,用它來代理原本已經存在的對象的方法
MyBatis中本來由Executor(被代理對象)來完成sql的執行,現在由代理對象(自動生成)來代理Executor完成,代理對象會將我們的操作轉交給Executor
問題是:MyBatis怎麼知道代理對象是什麼樣的對象呢?,這就需要為MyBatis提供Mapper介面,這個介面就是對mapper.xml中的sql語句的聲明,與DAO層的介面一毛一樣
使用步驟
創建介面類
package com.kkb.mapper; import com.kkb.pojo.Products; public interface ProductsMapper { //根據name查詢一個Products public Products selectProductByName(String name); }
提供響應的sql映射
<mapper namespace="com.kkb.mapper.ProductsMapper"> <select id="selectProductByName" parameterType="string" resultType="com.kkb.pojo.Products"> select *from products where pname = #{name} </select> </mapper>
獲取代理對象 執行方法完成操作
@Test public void proxyTest(){ SqlSession session = factory.openSession(); ProductsMapper mapper = session.getMapper(ProductsMapper.class); Products product = mapper.selectProductByName("泰國咖喱"); System.out.println(product); session.close(); }
註意事項:
- 必須保證mapper.xml中的namespace與介面的全限定名稱一致
- 方法的名稱必須與對應的sql statement的id一致
- 方法的參數必須與對應的sql statement的parameterType一致
- 方法的返回值必須與對應的sql statement的resultType一致
XML配置
MyBatis 的配置文件包含了會深深影響 MyBatis 行為的設置和屬性信息。 配置文檔的頂層結構如下:
- configuration(配置)
- properties(屬性)
- settings(設置)
- typeAliases(類型別名)
- typeHandlers(類型處理器)
- objectFactory(對象工廠)
- plugins(插件)
- environments(環境配置)
- environment(環境變數)
- transactionManager(事務管理器)
- dataSource(數據源)
- environment(環境變數)
- databaseIdProvider(資料庫廠商標識)
- mappers(映射器)
註意配置文件各個節點個層次是固定,需按照上述的順序書寫否則報錯,
單獨使用MyBatis的場景是很少的,後續都會將其與Spring進行整合,並由Spring來對MyBatis進行配置,加粗的為需要重點關註,其餘的瞭解即可,若遇到特殊需求可查閱官方文檔
屬性(properties)
properties可從配置文件或是properties標簽中讀取需要的參數,使得配置文件各個部分更加獨立
內部properties標簽
外部配置文件
jdbc.properties位於resource下
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql:///mybatisDB?serverTimezone=Asia/Shanghai&characterEncoding=utf8
user = root
password = admin
引用方式
當內部和外部屬性出現同名時,則優先使用外部的;
別名(typeAliases)
typeAliases用於為Java的類型取別名,從而簡化mapper中類名的書寫
為某個類定義別名
在mapper.xml中就可以直接使用別名 不區分大小寫
當指定package時將批量為包下所有類指定別名為類名小寫
<typeAliases>
<package name="com.kkb.pojo"/>
</typeAliases>
使用package批量設置時很容易出現別名衝突,這是就需要使用@Alias註解來為衝突的類單獨設置別名
@Alias("products1")
public class Products {
private int pid;
private String pname;
private float price;
private Date pdate;
private String cid;
.....}
下麵列出MyBatis已存在的別名
_byte | byte |
---|---|
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
映射(Mappers)
在mapper.xml文件中定義sql語句後,就必須讓MyBatis知道,到哪裡去找這些定義好的sql,這就需要在配置文件中指出要載入的mapper的位置;MyBatis支持4種方式來載入mapper文件
<mappers>
<!-- 1.指定資源文件的相對目錄 相對於classpath
maven項目會自動將java和resources都添加到classpath中
所以相對與resources來指定路徑即可-->
<mapper resource="mapper/ProductsMapper.xml"/>
<!-- 2.指定文件的絕對路徑,MyBatis支持但是一般不用-->
<mapper url="file:///Users/jerry/Downloads/MYB/src/main/resources/mapper/ProductsMapper.xml"/>
<!-- 3.通過指定介面 讓MyBatis自動去查找對應的Mapper
這種方式要求映射文件和介面處於同一目錄下,並且名稱相同
要保證上述要求只需要在resources下創建於介面包名相同的目錄即可
註意:運行前先clean,否則可能因為之前已經存在target而不會重新編譯,導致無法載入新建的mapper文件
-->
<mapper class="com.kkb.mapper.ProductsMapper"/>
<!-- 4.指定包名稱,掃描包下所有介面和映射文件
這種方式同樣要求映射文件和介面處於同一目錄下,並且名稱相同-->
<package name="com.kkb.mapper"/>
</mappers>
第3,4種方式映射文件目錄示例:
動態SQL
動態SQL指的是SQL語句不是固定死的,可以根據某些條件而發生相應的變化,這樣的需求非常多見
例如:頁面提供的搜索功能,要根據用戶給出的一個或多個條件進行查詢
在JDBC時代,我們通過Java代碼來判斷某個條件是否有效然後拼接SQL語句,這是非常繁瑣的,MyBatis的動態SQL很好的解決了這個問題,使判斷邏輯變得簡潔直觀;
在Mapper中通過標簽來完成動態SQL的生成
1. if
從頁面接受參數後我們會將參數打包為對象,然後將對象作為參數傳給MyBatis執行查詢操作,sql語句需要根據是否存在參數而動態的生成
mapper:
<!-- 根據姓名或cid進行搜索-->
<select id="searchProducts" parameterType="products" resultType="products">
select *from products
where 1=1
<if test="pname != null">
and pname like '%${pname}%'
</if>
<if test="cid != null">
and cid = #{cid}
</if>
</select>
測試:
@Test
public void searchTest(){
SqlSession session = factory.openSession();
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
//查詢條件對象
Products condition = new Products();
condition.setName("新疆");
condition.setCid("s001");
//執行查詢
List<Products> product = mapper.searchProducts(condition);
System.out.println(product);
session.close();
}
where1=1
用於保持sql的正確性,當不存在條件時,則查詢全部
2.where
where的作用就是用於取出上面的where 1=1,因為這會讓人看起來產生疑惑,其作用是將內部語句中的第一個and去除
<select id="searchProducts" parameterType="products" resultType="products">
select *from products
<where>
<if test="pname != null">
and pname like '%${pname}%'
</if>
<if test="cid != null">
and cid = #{cid}
</if>
</where>
</select>
3. foreach
當一個條件中中需要需要多個參數時則需要將多個參數拼接到一起,例如: in, not in
mapper:
<select id="searchProducts" parameterType="products" resultType="products">
select *from products
<where>
<if test="pname != null">
and pname like '%${pname}%'
</if>
<if test="cid != null">
and cid = #{cid}
</if>
<if test="ids != null">
<foreach collection="ids" open="and pid in (" close=")" separator="," item="id" index="i">
#{id}
</foreach>
</if>
</where>
</select>
<!--
<if test="ids != null"> 這裡不僅判斷屬性是否為空還判斷集合中是否有元素
foreache 標簽屬性說明:
強調:動態sql本質就是在拼接字元串,帶著自己拼接sql的思路來編寫動態sql會更好理解
collection 要遍歷的集合
open 拼接的首碼
close 拼接的尾碼
separator 拼接元素之間的分隔符
item 遍歷得到的臨時變數名
index 當前元素的索引(不常用)
-->
測試代碼:
@Test
public void searchTest(){
SqlSession session = factory.openSession();
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
//查詢條件對象
Products condition = new Products();
int[] ids = new int[]{1,2,3,4,5,};
condition.setIds(ids);
//執行查詢
List<Products> product = mapper.searchProducts(condition);
System.out.println(product);
session.close();
}
註意需要為POJO(Products
)對象增加ids屬性
4. set
set標簽用於更新語句,當同事要更新多個欄位時,我們需要留意當前是否是最後一個set,避免在後面出現,
符號,使用set標簽後可自動去除最後的逗號
mapper:
<update id="updateProductTest" parameterType="products">
update products
<set>
<if test="pname != null and pname != ''">
pname = #{pname},
</if>
<if test="price != null and price > 0">
price = #{price},
</if>
<if test="pdate != null">
pdate = #{pdate},
</if>
<if test="cid != null and cid != ''">
cid = #{cid},
</if>
</set>
where pid = #{pid}
</update>
測試代碼:
@Test
public void updateTest2(){
SqlSession session = factory.openSession();
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
//獲取已有對象
Products product = mapper.selectProductById(7);
product.setPname("雲南小土豆");
product.setPrice(10.5f);
//執行更新
mapper.updateProductTest(product);
System.out.println(product);
session.commit();
session.close();
}
5. sql與include
Sql中可將重覆的sql提取出來,使用時用include引用即可,最終達到sql重用的目的。 例如可以吧sql語句中的欄位列表提取出來作為通用的sql片段。然後在sql語句中使用 include 節點引用這個sql片段。
<!--提取片段-->
<sql id="fields">pid,pname,price,cid</sql>
<select id="includeTest" resultType="products">
select
<include refid="fields"/> <!-- 引用片段-->
from products
</select>
高級映射
在一些情況下資料庫的記錄和POJO對象無法直接映射,包括兩種情形:
- 資料庫欄位與POJO欄位名稱不同(可以避免);
- 關聯查詢時,需要將關聯表的數據映射為另一個類型的POJO(一對一),或List中(一對多);
在MyBatis中通過resultMap來完成自定義映射
1.自定義欄位與屬性映射
先將Products表中的欄位更名為p_xxx,如下所示:
mapper:
<!--自定義映射關係 id:該映射關係的標識 type:映射到的POJO類型此處為別名-->
<resultMap id="product_resultMap" type="products">
<!--主鍵-->
<id column="p_id" property="pid"/>
<!--其他欄位-->
<result column="p_name" property="pname"/>
<result column="p_price" property="price"/>
<result column="p_date" property="pdate"/>
<result column="p_cid" property="cid"/>
</resultMap>
<!--引用映射關係-->
<select id="selectProductsCustomMapping" resultMap="product_resultMap">
select *from products
</select>
2. 關聯查詢
2.1 關聯關係
兩個表之間記錄的對應關係,分為一對一和一對多,而多堆多則是三張表之間的關係,若掌握了兩張表之間的一對多關係的處理,則多堆多也就不是問題了,因為本質上多對多就是兩個一對多組成的
案例表:
這兩張表之間存在一對一和一對多
站在user表角度來看 一個用戶可能對應多個訂單即一對一
站在order表角度來看 一個訂單隻能對應一個用戶即一對多
POJO類:
Order.java
import java.util.Date;
public class Order {
private int id,user_id;
private String number;
private Date createtime;
private String note;
private User user;
//get/set....
}
User.java
import java.util.Date;
import java.util.List;
public class User {
private int id;
private String name;
private Date birthday;
private String sex,address;
private List<Order> orders;
public User() {
}
//get/set....
}
2.2 一對一映射
需求:根據訂單編號查詢訂單信息以及用戶名稱和地址
sql語句為:
select o.*,u.username,u.address
from orders o join kuser u
on o.user_id = u.id
where o.id = 8;
#要查詢的欄位根據需求來定,這裡只需要查詢用戶的姓名和地址
OrdersMapper.xml:
<!--自定義映射-->
<resultMap id="order_resultMap" type="com.kkb.pojo.Order">
<!--orders表映射-->
<id column="id" property="id"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property="note"/>
<!--關聯的user表映射-->
<association property="user" javaType="user">
<id column="user_id" property="id"/>
<result column="username" property="name"/>
<result column="address" property="address"/>
</association>
</resultMap>
<select id="selectOrderByID" parameterType="int" resultMap="order_resultMap">
select
o.*,u.username,u.address
from
orders o join kuser u
on
o.user_id = u.id
where
o.id = #{oid};
</select>
測試代碼:
@Test
public void test(){
SqlSession session = factory.openSession();
OrdersMapper mapper = session.getMapper(OrdersMapper.class);//記得提供映射介面
Order order = mapper.selectOrderByID(8);
System.out.println(order);
session.close();
}
補充:當連接查詢出現重覆欄位時如:兩個表都有id欄位,MyBatis簡單的取第一個匹配的欄位值,很多時候這是不正確的,我們可以給這個重覆的欄位取個別名來避免,像這樣:
<select id="selectOrderByID" parameterType="int" resultMap="order_resultMap">
select
u.*,o.*,o.id oid
from
orders o join kuser u
on
o.user_id = u.id
where
o.id = #{oid};
</select>
理所當然的 在resultMap中則使用oid來進行映射
<id column="oid" property="id"/>
2.3 一對多映射
需求:根據用戶編號查詢用戶信息以及用戶所有訂單信息
sql語句為:
select u.*,o.*,o.id oid
from kuser u left join orders o
on o.user_id = u.id
where u.id = 1;
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="com.kkb.mapper.UserMapper">
<!--自定義映射-->
<resultMap id="user_resultMap" type="user" autoMapping="true">
<result column="username" property="name"/>
<collection property="orders" ofType="order" autoMapping="true">
<result column="oid" property="id"/>
</collection>
</resultMap>
<select id="selectUserByID" parameterType="int" resultMap="user_resultMap">
select
u.*,o.*,o.id oid
from
kuser u left join orders o
on
o.user_id = u.id
where
u.id = #{uid}
</select>
</mapper>
autoMapping="true" 將自動映射欄位名與屬性名能對應的欄位,我們只需要添加對應不上的即可
測試代碼:
@Test
public void test2(){
SqlSession session = factory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserByID(1);
System.out.println(user);
session.close();
}
註意:無論如何在collection標簽中必須至少存在一個手動映射欄位否則,將不會合併重覆的主記錄(user) 按照官方的說法,建議手動映射id欄位,可提高整體性能:去看看
另外collection標簽中 ofType用於指定元素的類型 javaType指定容器類型
2.4嵌套映射,select
當關聯查詢非常複雜時,可以用嵌套的select,其原理是在映射複雜數據時執行另一個select來完成
<resultMap id="order_resultMap2" type="Order" autoMapping="true">
<id column="id" property="id"/>
<!-- 指定嵌套查詢 column是傳給內層查詢的參數 -->
<association property="user" column="user_id" select="selectUserByUid" javaType="user"/>
</resultMap>
<!-- 外層查詢-->
<select id="selectOrderByID2" parameterType="int" resultMap="order_resultMap2">
select
*
from
orders
where
id = #{id}
</select>
<!-- 嵌套查詢-->
<select id="selectUserByUid" parameterType="int" resultType="user">
select *from kuser where id = #{id}
</select>
這種方式同樣適用於一對多的關聯關係
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>