從零開始認識並操縱Mybatis
目錄
- 業務介紹
- 版本宣告
- 認識Mybatis
- 入門程式操作步驟
- 原始Dao開發方式存在的問題
- Mapper 動態代理方式
- 優化配置檔案
- 關於Mappers對映器屬性
- 輸入型別和輸出型別
- 動態sql完成多條件查詢
- resultMap 輸出對映,完成訂單資訊
- 使用者訂單關聯查詢
- 結束
@(認識並操縱Mybatis)
宣告:從零開始,並不代表你對java Mybatis一點都不懂的程度哈,本例項只是過一個概貌,詳細內容會分多篇文章拆解
業務介紹
使用者模組的管理,使用者表的維護:
- 新增使用者
- 修改使用者資訊
- 刪除使用者
- 查詢使用者
- id查詢單個
- 使用者名稱模糊查詢
- 查詢所有
- 查詢總數
- 根據ids查詢
- 多條件查詢
使用者和訂單關聯管理
- 根據訂單查詢對應使用者資訊
- 根據使用者查詢所有訂單資訊
版本宣告
只使用Mybatis
,不整合其他框架。Mybatis版本為^3.4
開發工具使用eclipse
使用的是純 java Project
整個流程的詳細專案程式碼可以踩我的github結合觀光:https://github.com/Autom-liu/MybatisLearn
認識Mybatis
是什麼
MyBatis是一個優秀的持久層框架,不算完整的ORM框架
歷史
- MyBatis 本是apache的一個開源專案iBatis,
- 2010年這個專案由apache software foundation 遷移到了google code,並且改名為MyBatis 。
- 2013年11月遷移到Github。
傳統JDBC存在的問題
- 資料庫連線建立、釋放頻繁造成系統資源浪費從而影響系統性能,如果使用資料庫連線池可解決此問題。
- Mybatis 在配置檔案中配置了資料連線池,使用連線池管理資料庫連結。
- Sql語句寫在程式碼中造成程式碼不易維護,實際應用sql變化的可能較大,sql變動需要改變java程式碼。
- Mybatis 將Sql語句配置在配置檔案中與java程式碼分離。
- 向sql語句傳引數麻煩,因為sql語句的where條件不一定,可能多也可能少,佔位符需要和引數一一對應。
- Mybatis自動將java物件對映至sql語句,通過statement中的parameterType定義輸入引數的型別。
- 對結果集解析麻煩,sql變化導致解析程式碼變化,且解析前需要遍歷,如果能將資料庫記錄封裝成pojo物件解析比較方便。
- Mybatis自動將sql執行結果對映至java物件,通過statement中的resultType定義輸出結果的型別。
與完整的ORM框架Hibernate的區別
- Mybatis不完全是一個ORM框架,因為MyBatis需要程式設計師自己編寫Sql語句。mybatis可以通過XML或註解方式靈活配置要執行的sql語句,並將java物件和sql語句對映生成最終執行的sql,最後將sql執行的結果再對映生成java物件。
- Mybatis學習門檻低,簡單易學,程式設計師直接編寫原生態sql,可嚴格控制sql執行效能,靈活度高,非常適合對關係資料模型要求不高的軟體開發,例如網際網路軟體、企業運營類軟體等,因為這類軟體需求變化頻繁,一但需求變化要求成果輸出迅速。但是靈活的前提是mybatis無法做到資料庫無關性,如果需要實現支援多種資料庫的軟體則需要自定義多套sql對映檔案,工作量大。
- Hibernate物件/關係對映能力強,資料庫無關性好,對於關係模型要求高的軟體(例如需求固定的定製化軟體)如果用hibernate開發可以節省很多程式碼,提高效率。但是Hibernate的學習門檻高,要精通門檻更高,而且怎麼設計O/R對映,在效能和物件模型之間如何權衡,以及怎樣用好Hibernate需要具有很強的經驗和能力才行。
- 總之,按照使用者的需求在有限的資源環境下只要能做出維護性、擴充套件性良好的軟體架構都是好架構,所以框架只有適合才是最好。
簡單來說,Mybatis為什麼流行,Hibernate為什麼落幕,終歸在於移動網際網路的世界趨勢,迭代更新快,要求變化雜而多。不可能再像以往那樣,形成完整穩定的需求再開發的階段了....越貼近底層原生越有價值,越是封裝包裝限制也越大
Mybatis架構
入門程式操作步驟
下載和導包
下載連結:https://github.com/mybatis/mybatis-3/releases
目錄結構:
導包:
- Mybatis核心包
- Mybatis依賴包
- 資料庫驅動包 注:這裡使用的是老版本的5.1的包,新版本有很大改動!
準備資料庫和Java Bean
兩張表: 使用者表 + 訂單表 基本建立即可,無其他關聯
準備javaBean 就不多說了,要注意的一點就是,javaBean最好實現
Serializable
序列化介面持久化,還有一點就是命名規範吧,不同人說法不同,有些是直接乾脆和資料庫欄位名一樣,而這裡不那麼一致,使用程式語言中統一的駝峰命名法,而不是資料庫中的短劃線分割。
要相信的是,任何一個框架總會幫我們解決這個對映問題的!
配置Mybatis
在官方給出的文件當中給出了我們這樣的配置:
官方的檔名是: mybatis-config.xml 當然這並不是定死的。
<!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="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
這個是主配置檔案,也就是架構當中的最頂層,基本上參照這個配置copy,稍作修改即可,就不再匯入約束啥的了。
比如在本次配置裡,建立的是src/sqlMap.xml,檔名不是固定的,後邊程式會指定
還有一點就是:不用太在意每個配置項的含義,大概看得懂就行,因為後期往往需要整合spring,這些配置全部不用了,作為入門程式而已,給大家觀光一下
<!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.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="****"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="sqlMap/User.xml"/>
</mappers>
</configuration>
編寫sql對映檔案
這裡可以看到Mapper標籤,是引入其他對映檔案,這個檔案就是架構當中的第二層,多個mapper的組成,每個mapper都是若干個sql語句
到這裡就需要考慮實現業務的sql語句應該如何書寫了,這裡先只貼出增和查的業務,剩下都以舉例完成
sqlMap/User.xml
<mapper namespace="user">
<select id="findUser" resultType="edu.scnu.bean.User" parameterType="Integer">
select * from mb_user where id = #{id}
</select>
<select id="getByUsername" resultType="edu.scnu.bean.User" parameterType="String">
select * from mb_user where username like "%"#{username}"%"
</select>
<insert id="save" parameterType="edu.scnu.bean.User">
insert into mb_user (username,birthday,sex,address) values (#{username}, #{birthday}, #{sex}, #{address})
</insert>
</mapper>
namespace 是名稱空間,這個真正的妙用後面體現出來,這裡暫時只需要知道用於區分同名作用域即可
id 可以認為是該條sql語句的識別符號
parameterType 傳遞進來的引數型別,可以看到基本資料型別可以省略,而其他的就要完整類名了
resultType 返回值型別,同上一樣
select 標籤用於書寫select查詢語句
insert 標籤用於書寫insert插入語句 update delete一樣
嘗試了一下 select 書寫insert好像也可以,但不推薦
關於模糊查詢的補充
在Mybatis中使用 #{}
作為佔位符,佔位符的作用在於會在識別符號取得結果後前後加單引號。
如果直接使用:
select * from mb_user where username like "%#{username}%"
就不對了,因為假如username=admin 結果變為:
select * from mb_user where username like "%'admin'%"
就語法錯誤或沒有結果
可以使用 ${}
這種字串拼接的方式
select * from mb_user where username like "%${username}%"
不過這樣只是沒有了佔位符引數的效果了而已。
書寫Dao類
接下來就到來書寫真正的Dao類了,這裡還是按照規範實現介面
public interface UserDao {
void save(User user);
void update(User user);
void delete(Integer id);
User find(Integer id);
// List<User> getAll();
// Integer getCount();
List<User> getByUsername(String username);
// List<User> getByCond(User user);
}
看下實現類:
public class UserDaoImpl implements UserDao {
private SqlSessionFactory sqlSessionFactory;
// 沒有spring幫助下,就需要外部手動注入!
public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void save(User user) {
SqlSession session = sqlSessionFactory.openSession();
session.insert("user.save", user);
session.commit();
}
@Override
public User find(Integer id) {
SqlSession session = sqlSessionFactory.openSession();
return session.selectOne("user.findUser", id);
}
@Override
public List<User> getByUsername(String username) {
SqlSession session = sqlSessionFactory.openSession();
return session.selectList("user.getByUsername", username);
}
}
測試類
最後即可編寫測試了
public class MybatisTest {
private SqlSessionFactory sqlSessionFactory;
private UserDao userDao;
@Before
public void before() throws Exception{
String resource = "sqlMap.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
userDao = new UserDaoImpl(sqlSessionFactory);
}
@Test
public void testFind() {
userDao.find(1);
}
@Test
public void testGetByUsername() {
userDao.getByUsername("王");
}
@Test
public void testSave() {
userDao.save(new User("未知使用者", "2", new Date(), "中國"));
}
}
返回id 的save
現在對實現增加使用者新增需求,就是在新增使用者的同時根據該使用者ID查詢該訂單資訊
這時候我們首先要獲取新增使用者後的id,但是新增使用者是資料庫給我們返回的是受影響條數,如何快速獲得最新插入資料的id呢
sql語法提供了 select LAST_INSERT_ID()
供我們獲取最新插入資料的ID,我們可以將該sql嵌入到sql配置檔案中去
這裡簡單講解配置語法:
- selectKey 標籤實現主鍵返回
- keyColumn:主鍵對應的表中的哪一列
- keyProperty:主鍵對應的pojo(Bean)中的哪一個屬性
- order:設定在執行insert語句前執行查詢id的sql,還是在執行insert語句之後執行查詢id的sql
使用主鍵自增策略的id:
<!-- 生成主鍵的順序Mysql是之後,oracle是之前 -->
<selectKey keyProperty="id" resultType="Integer" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
使用uuid的String
<selectKey keyColumn="id" keyProperty="id" order="BEFORE" resultType="string">
select LAST_INSERT_ID()
</selectKey>
這裡可以修改上述插入SQL語法配置:
<insert id="save" parameterType="edu.scnu.bean.User">
<!-- 生成主鍵的順序Mysql是之後,oracle是之前 -->
<selectKey keyProperty="id" resultType="Integer" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
insert into mb_user (username,birthday,sex,address) values (#{username}, #{birthday}, #{sex}, #{address})
</insert>
原始Dao開發方式存在的問題
- Dao方法體存在重複程式碼:通過SqlSessionFactory建立SqlSession,呼叫SqlSession的資料庫操作方法
- 呼叫sqlSession的資料庫操作方法需要指定statement的id,這裡存在硬編碼,不得於開發維護。
上述問題主要關鍵還是在於同類型的枯燥性重複性程式碼,只有少數引數傳遞部分要變動,其它基本都是同樣的套路,因此可不可以有個辦法一些重複性工作呢?Mybatis貼心為我們服務了這一點,它省去的不僅僅是一兩條語句,它把整個實現類都為我們省去了。來看看它是怎麼工作的吧!
Mapper 動態代理方式
Mapper介面開發方法只需要程式設計師編寫Mapper介面(相當於Dao介面),由Mybatis框架根據介面定義建立介面的動態代理物件,代理物件的方法體同上邊Dao介面實現類方法。
Mapper介面開發需要遵循以下規範:
1、 Mapper.xml檔案中的namespace與mapper介面的類路徑相同。
2、 Mapper介面方法名和Mapper.xml中定義的每個statement的id相同
3、 Mapper介面方法的輸入引數型別和mapper.xml中定義的每個sql 的parameterType的型別相同
4、 Mapper介面方法的輸出引數型別和mapper.xml中定義的每個sql的resultType的型別相同
也就是說,我們要完成的只是介面,實現類框架幫我們搞定了...
那麼首先來修改優化一下配置檔案
<!-- 使用動態代理開發DAO,1. namespace必須和Mapper介面類路徑一致 -->
<mapper namespace="edu.scnu.dao.UserDao">
<!-- 根據使用者id查詢使用者 -->
<!-- 2. id必須和Mapper介面方法名一致 -->
<!-- 3. parameterType必須和介面方法引數型別一致 -->
<!-- 4. resultType必須和介面方法返回值型別一致 -->
<select id="find" resultType="edu.scnu.bean.User" parameterType="Integer">
select * from mb_user where id = #{id}
</select>
<select id="getByUsername" resultType="edu.scnu.bean.User" parameterType="String">
select * from mb_user where username like "%"#{username}"%"
</select>
<insert id="save" parameterType="edu.scnu.bean.User">
<!-- 生成主鍵的順序Mysql是之後,oracle是之前 -->
<selectKey keyProperty="id" resultType="Integer" order="AFTER">
select LAST_INSERT_ID()
</selectKey>
insert into mb_user (username,birthday,sex,address) values (#{username}, #{birthday}, #{sex}, #{address})
</insert>
</mapper>
接下來如果把實現類刪除了,我們發現測試類例項化就報錯了,這時候我們需要通過session的getMapper方法獲得Mybatis幫我們建立好的實現類
測試類:
@Before
public void before() throws Exception{
String resource = "sqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
userDao = session.getMapper(UserDao.class);
}
優化配置檔案
到目前為止,我們書寫了兩個配置檔案,這兩個配置檔案開始出現不僅繁雜不好維護的問題,還有挺多不規範的地方:
- 資料庫連線引數不應該直接在xml配置當中,應該提取出來:
使用property屬性
<!-- 是用resource屬性載入外部配置檔案 -->
<properties resource="db.properties">
<!-- 在properties內部用property定義屬性 -->
<!-- 如果外部配置檔案有該屬性,則內部定義屬性被外部屬性覆蓋 -->
<property name="jdbc.password" value="root" />
</properties>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
- 類名過長重複
使用typeAliases屬性自定義別名
mybatis已經為我們提供了很多基本資料型別以及包裝類的別名,同樣,我們也可以自己自定義類名別名
支援單個別名的定義
<typeAliases>
<!-- 單個別名定義 -->
<typeAlias alias="user" type="edu.scnu.bean.User" />
</typeAliases>
還有批量定義,這裡選用批量定義的方式
<typeAliases>
<!-- 批量別名定義,掃描整個包下的類,別名為類名(大小寫不敏感) -->
<package name="edu.scnu.bean" />
</typeAliases>
這樣後續的所有ResultType 和 parameterType只需要直接寫User或user即可
詭異的是,也是需要注意的是順序問題
typeAliases 必須緊跟 property 後
關於Mappers對映器屬性
resource
- 使用相對於類路徑的資源(現在的使用方式)
- 如:
class
- 使用mapper介面類路徑
- 如:
- 注意:此種方法要求mapper介面名稱和mapper對映檔名稱相同,且放在同一個目錄中。
- 註冊指定包下的所有mapper介面
- 如:
- 注意:此種方法要求mapper介面名稱和mapper對映檔名稱相同,且放在同一個目錄中。
- 用於批量引入
輸入型別和輸出型別
接下來我們來完善其他業務功能,這裡為了演示以下各類幾種輸入和輸出型別的情況,我們增加一個實體型別包裝實體
輸入型別:
- 基本型別
- 實體型別
- 實體型別包裝類
說白了傳遞引用型別,會自動把所有引用屬性轉化為map物件,這點和struts的OGNL表示式類似
輸出型別:
- 基本型別
- 實體
- 實體列表
- 只需要傳遞泛型型別(集合元素的型別即可)
然後來完善我們其他功能
parameterType為實體包裝類的情況
<update id="update" parameterType="UserWrapper">
update mb_user set username=#{user.username}, birthday=#{user.birthday}, sex=#{user.sex}, address=#{user.address} where id=#{user.id}
</update>
resultType為基本型別的情況
<select id="getCount" resultType="int">
select count(*) from mb_user
</select>
動態sql完成多條件查詢
使用where標籤和if標籤
<select id="getByCond" parameterType="User" resultType="User">
select * from mb_user
<where>
<if test="sex != null and sex != ''">
and sex = #{sex}
</if>
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="address != null and address != ''">
and address = #{address}
</if>
</where>
</select>
使用sql片段簡化
sql標籤宣告 include引入
<sql id="selectSql">
select * from ub_user
</sql>
<select id="find" resultType="User" parameterType="Integer">
<include refid="selectSql" /> where id = #{id}
</select>
使用foreach標籤實現多id查詢
原理
select * from mb_user where id in (1,2,3);
以java陣列傳遞引數
<select id="getByIds" parameterType="Integer" resultType="User">
<include refid="selectSql" />
<where>
<foreach collection="array" item ="id" separator="," open="id in (" close=")">
#{id}
</foreach>
</where>
</select>
以list集合傳遞引數
<select id="getByIds" parameterType="Integer" resultType="User">
<include refid="selectSql" />
<where>
<foreach collection="list" item ="id" separator="," open="id in (" close=")">
#{id}
</foreach>
</where>
</select>
resultMap 輸出對映,完成訂單資訊
之前我們說到,訂單Dao類是以駝峰命名的,而資料庫欄位是下劃線命名的
在之前同名的情況都可以自動對映,那麼針對這種不同名的情況又如何呢?
我們先簡單完成以下訂單的其中一個業務
public interface OrdersDao {
List<Orders> getAll();
}
使用者訂單關聯查詢
一對一
方式一、使用新建實體類繼承關係
新建一個OrdersUser 的javaBean類
public class OrdersUser extends Orders {
private String username;
private String address;
// ...
}
配置OrdersUser.xml
<mapper namespace="edu.scnu.dao.OrdersUserDao">
<select id="queryAll" resultType="OrdersUser">
SELECT
o.id,
o.user_id
userId,
o.number,
o.createtime,
o.note,
u.username,
u.address
FROM
`order` o
LEFT JOIN `user` u ON o.user_id = u.id
</select>
</mapper>
方式二、直接多對一關係對映
直接在Orders實體類下增加User欄位,並在配置檔案裡作如下對映:
<resultMap type="Orders" id="orderResultMap">
<!-- 定義主鍵 ,非常重要。如果是多個欄位,則定義多個id -->
<id property="id" column="id"></id>
<result property="userId" column="user_id"/>
<!-- 關聯,屬性全寫上 -->
<result property="number" column="number" />
<result property="createtime" column="createtime" />
<result property="note" column="note" />
<association property="user" javaType="User">
<!-- id:宣告主鍵,表示user_id是關聯查詢物件的唯一標識-->
<id property="id" column="user_id" />
<result property="username" column="username" />
<result property="address" column="address" />
</association>
</resultMap>
一對多關係
一對多關係只能使用resultMap的collection對映
同樣在User Bean類中新增List<Orders>欄位
private List<Orders> orders;
<resultMap type="User" id="UserOrdersResultMap">
<id property="id" column="id" />
<result property="username" column="username"/>
<result property="address" column="address"/>
<collection property="orders" javaType="List" ofType="Orders">
<result property="id" column="oid"/>
<result property="createtime" column="createtime"/>
<result property="number" column="number"/>
</collection>
</resultMap>
<select id="getWithOrders" resultMap="UserOrdersResultMap">
SELECT
u.id,
u.username,
u.address,
o.id oid,
o.number,
o.createtime
FROM
`mb_user` u
LEFT JOIN `mb_orders` o ON u.id = o.user_id
</select>
結束
好了,到這裡已經介紹了單獨使用Mybatis操縱的所有功能,整個流程到這裡就算完成了,整個流程的詳細專案程式碼可以踩我的github結合觀光:https://github.com/Autom-liu/MybatisLearn ,整個流程涉及到諸多知識細節需要慢慢琢磨,這裡只給大家展示整個概貌,具體細節會在後續的文章中慢慢解釋,還會給大家展示Mybatis和Spring整合後的精彩內容哦,期待大家的支援和star~~~