一、前言
本篇內容以理解MyBatis的基本用法和快速在項目中實踐為目的,遵循Make it work,better and excellent原則。 技術棧為MyBatis3.2.7+log4j1.2.17+sqlite3+jdk1.7。
二、示例
示例代碼功能:
學校人員管理系統,對象分別為學生、教師和班級,學生與班級為多對一關系,班級與教師為一對多關系。示例代碼主要為操作學生對象。
1. 表結構
學生表(TStudentML)
字段 | 數據類型 | 備注 |
ID | Integer | 主鍵 |
Name | Text | 姓名 |
ClassID | Integer | 班級ID |
班級表(TClassML)
字段 | 數據類型 | 備注 |
ID | Integer | 主鍵 |
Name | Text | 姓名 |
教師表(TTeacherML)
字段 | 數據類型 | 備注 |
ID | Integer | 主鍵 |
Name | Text | 姓名 |
ClassID | Integer | 班級ID |
2. 項目目錄結構
src |-- com | |-- entity | | |-- ETeacher | | |-- EStudent | | |-- EClass | |-- dao | |-- studentMapper.xml | |-- StudentMapper.java | |-- DBase.java | |-- DStudent.java |-- mybatis-config.xml |-- db.db |-- log4j.properties |-- config.properties
3. 數據庫配置文件(config.properties)
driver=org.sqlite.JDBC
url=jdbc:sqlite:src/db.db
4. log4j.properties
log4j.rootLogger=TRACE, studout log4j.appender.studout=org.apache.log4j.ConsoleAppender log4j.appender.studout.layout=org.apache.log4j.PatternLayout log4j.appender.studout.layout.ConversionPattern=[%5p]%t -%m%n
5. mybatis-config.xml
<?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>
<!--
引入外部properties配置文件,后面內容則通過${屬性名}來引用屬性值
在使用實例化SqlSessionFactory時,還可以通過new SqlSessionFactoryBuilder.build(config.xml的InputStream實例, Properties實例)來設置屬性值。
優先級從高到低是:
1. 通過build方法入參設置
2. 通過resource引入的屬性
3. 通過property標簽設置的屬性
--> <properties resource="config.properties"></properties>
<!--強制指定MyBatis使用log4j作為日志日志框架,若不指定那么當部署到如Tomcat等應用容器時,會被容器設置為使用common-logging來記錄日志--> <settings> <setting name="logImpl" value="LOG4J"/> </settings>
<!--設置自定義JAVA類型的別名,否則在映射文件中的resultType、parameterType等特性值就需要填寫全限定類名才有效--> <typeAliases>
<!--
這時對於包下的類,在映射文件中的resultType、parameterType等特性值,我們只需寫類名或首字母小寫的類名
當自定義JAVA類配合@Aliase("別名")使用時,只需寫別名即可,且不區分大小寫
MyBatis對JAVA原生類型定義了內置別名:
`int`,`long`等就是`_int`,`_long`
`Integer`,`String`就是`int`,`string`
--> <package name="com.entity"/> </typeAliases> <environments default="dev">
<!--運行環境配置--> <environment id="dev">
<!--
type屬性用於指定事務管理器類型
JDBC:使用JDBC的提交和回滾設置,依賴從數據源獲取的連接來管理事務范圍。
MANAGED:讓容器(如Spring)來管理事務的生命周期。默認情況會關閉連接,
若不想關閉連接則需要如下配置:
<transactionManager type="MANAGED">
<property name="closeConnection" value="false"/>
</transactionManager>
--> <transactionManager type="JDBC"></transactionManager>
<!--
type屬性用於指定連接池類型
UNPOOLED:連接用完就關閉,不放到連接池
POOLED:連接用完則放在連接池
--> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <!--雖然sqlite不用填寫username和password,但這兩個節點必須保留,否則將報錯--> <property name="username" value=""/> <property name="password" value=""/> </dataSource> </environment> </environments>
<!--向MyBatis注冊映射信息--> <mappers> <mapper resource="com/dao/studentMapper.xml"></mapper> </mappers> </configuration>
6. 實體類
// 教師實體類 public class ETeacher{ private int id; private String name; // 省略各種setter和getter....... } // 班級實體類 public class EClass{ private int id; private String name; // 省略各種setter和getter....... } // 學生實體類 public class EStudent{ private int id; private String name; private EClass myClass; private List<ETeacher> teachers; // 省略各種setter和getter....... }
7. 映射接口
public interface StudentMapper{ /** * 通過姓名獲取學生信息(不含教師、班級信息) * @param name 學生姓名 * @return */ EStudent getJustStudentInfo(String name); /** * 通過姓名模糊查詢學生信息(不含教師、班級信息) * @param name 學生姓名 * @return */ List<EStudent> getJustStudentInfos(String name); /** * 通過姓名和班級名稱獲取學生信息(含教師、班級信息) * @param studentName 學生姓名 * @param className 班級名稱 * @return */ EStudent getStudentInfo(String studentName, String className); /** * 新增學生信息 * @param student * @return */ void addStudent(EStudent student); /** * 刪除學生信息 * @param id * @return */ void delStudent(int id); }
8. 映射文件
<?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="main.dao.StudentMapper"> <select id="getJustStudentInfo" resultType="EStudent"> select ID,Name from TStudentML where Name = #{0} </select> <select id="getJustStudentInfos" resultType="EStudent"> select * from TStudentML <if test="#{0} != null"> where name like '%'+#{0}+'%' </if> </select> <select id="getStudentInfo" resultMap="getStudentInfoResultMap"> select a.ID a_id, a.Name a_name, b.ID b_id, b.Name b_name, c.ID c_id, c.Name c_name, c.ClassID c_classId from
TStudentML a left outer join TClassML b on(a.ClassID=b.ID) left outer join TTeacherML c on(b.ID=c.ClassID) <trim prefix="where" prefixOverrides="and"> <if test="#{0} != null"> a.Name like '%'+#{0}+'%' </if> <if test="#{1} != null"> and b.Name like '%'+#{1}+'%' /if> </trim> </select> <resultMap id="getStudentInfoResultMap" type="EStudent"> <id property="id" column="a_id"/> <result property="name" column="a_name"/>
<!-- 一對一關系 --> <association property="myClass" javaType="EClass"> <id property="id" column="b_id"/> <result property="name" column="b_name"/> </association>
<!-- 一對多關系 --> <collection property="teachers" ofType="ETeacher"> <id property="id" column="c_id"/> <result property="name" column="c_name"/> <result property="classId" column="c_classId"/> </collection> </resultMap> <insert id="addStudent" parameterType="EStudent"> insert into TStudentML(Name, ClassID) values(#{name}, #{myClass.id}) </insert> <delete id="delStudent"> delete from TStudentML where ID = #{0} </delete> </mapper>
9. 數據庫操作基類
public class DBase{ private static SqlSessionFactory factory = null; public DBase(){ if (factory != null) return; InputStream is = this.getClass().getClassLoader().getResourceAsStream("mybatis-config.xml"); factory = new SqlSessionFactoryBuilder().build(is); } /** * 獲取SqlSession實例,使用后需調用實例的close()函數釋放資源 * @return */ protected SqlSession openSession(){ return factory.openSession(); } }
10. 數據庫操作類
public class DStudent extends DBase{ /** * 通過姓名獲取學生信息(不含教師、班級信息) * @param name 學生姓名 * @return */ public EStudent getJustStudentInfo(String name){ EStudent ret = null; SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); ret = sm.getJustStudentInfo(name); } finally{ session.close(); } return ret; } /** * 通過姓名模糊查詢學生信息(不含教師、班級信息) * @param name 學生姓名 * @return */ public List<EStudent> getJustStudentInfos(String name){ List<EStudent> ret = null; SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); ret = sm.getJustStudentInfos(name); } finally{ session.close(); } return ret; } /** * 通過姓名和班級名稱獲取學生信息(含教師、班級信息) * @param name 學生姓名 * @param className 班級名稱 * @return */ public EStudent getStudentInfo(String name, String className){ EStudent ret = null; SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); ret = sm.getStudentInfo(name, className); } finally{ session.close(); } return ret; } /** * 新增學生信息 * @param student */ public void addStudent(EStudent student){ SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); sm.addStudent(student); session.commit(); } finally{ session.close(); } } /** * 刪除學生信息 * @param id */ public void delStudent(int id){ SqlSession session = openSession(); try{ StudentMapper sm = session.getMapper(StudentMapper.class); sm.delStudent(id); session.commit(); } finally{ session.close(); } } }
三、基礎知識
作為一個ORM框架,可以知道其至少由對象模型轉換為關系模型、關系模型轉換為對象模型和緩存管理這三個模塊組成。
MyBatis在對象模型轉換為關系模型模塊的實現方式是對象模型實例屬性+自定義SQL語句,好處是對SQL語句的可操作性高,同時簡化SQL入參的處理;壞處是對於簡單的單表操作,依舊要寫SQL語句,無法由對象模型自動生成SQL,最明顯的問題是在開發初期數據表結構不穩定,一旦表結構改了,代碼上不僅要改對象模型還要改SQL語句(不過MyBatis也考慮到這點,通過<sql>標簽實現SQL語句復用,緩解這樣問題)。
關系模型轉換為對象模型則采用關系模型結果集字段映射到對象模型實體字段的方式處理。
緩存模塊則分為SQL語句緩存和查詢數據緩存兩種,由於MyBatis需要開發者自定義SQL語句,因此SQL語句緩存不用考慮;而查詢數據緩存則被分為一級和二級緩存,一級緩存以事務為作用域,二級緩存以同一個映射集為作用域,而且二級緩存采用直寫的方式處理緩存數據被修改和刪除的情況。
(本人不才,曾開發輕量級ORM框架LessSQL.Net,由於設計為SQL語句必須由對象模塊實例映射生成,而關系模型數據集合無法自動填充任意的對象模型實體中,無法支撐復雜的查詢語句,而緩存方面僅實現了SQL語句緩存性能優化有限,因此框架僅適用於小型工具軟件。因為踩過這些坑,所以對ORM框架有一點淺薄的認識和看法)
言歸正轉,我們一起了解MyBatis的基礎知識吧。
1. MyBatis框架配置文件
實際上就是MyBatis會話工廠的配置文件,用於配置如緩存、日志框架、數據庫鏈接信息、兩種模型間轉換的處理器和注冊映射集等。通過上文大家應該知道如何Make it work了。而Make it better也是從這里出發。
2. 映射集
映射集是由多個“標識”——“SQL語句”組成,映射記錄上還有如入參類型、返回類型等信息,為對象關系模型轉換引擎提供元數據。
設置映射集的方式有兩種,一種是通過接口,一種通過xml文檔。但上文示例采用兩者相結合的方式,綜合兩者優點。
[a]. 映射接口方式
public interface StudentMapper{ @Select("select * from TStudentML where Name=#{0}") EStudent getJustStudent(String name); @Insert("insert into TStudentML(Name, ClassID) values(#{name}, #{myClass.id})") void addStudent(EStudent student); @Delete("delete from TStudentML where ID=#{0}") void delStudent(int id); @Update("update TStudentML set Name=#{name},ClassID=#{myClass.id} where ID=#{id}") void updateStudent(EStudent student); }
很明顯通過接口方式定義映射集是無法實現上文中復雜的查詢操作,而好處就是代碼量銳減,且由於使用了接口,所以IDE的智能提示和編譯時語法、依賴關系檢查會降低編碼錯誤的風險。
使用接口方式需要將mybatis-config.xml中的mapper改為如下內容
<mappers> <mapper class="com.dao.StudentMapper"></mapper> </mappers>
<!--
或注冊某包下的所有映射接口
<mappers>
<package name="com.dao"/>
</mappers>
-->
[b]. 映射XML文件
上文示例已經展示了映射XML文件的用法,下面我們逐個細節理解。
1. select、update、delete和insert標簽用於填寫對應動作的SQL語句。
2. parameterType屬性就是入參類型具體法則如下(parameterMap已被deprecated,所以不理也罷):
a. parameterType為POJO類型時,可通過 #{屬性名} 填入屬性值(該屬性值經過防SQL注入處理),也可通過 ${name} 填入屬性raw值(未經過防SQL注入處理的屬性值),更爽的是 #{} 支持短路徑操作如上文中的 #{myClass.id} 。
b. parameterType為int、long等值類型時,當僅有一個入參時,可以使用 #{任意字符} 填入屬性值,但無法通過 ${任意字符串} 填入屬性raw值(報找不到改實例屬性),還可以通過 #{0} 和 #{param0} 來填入屬性值;而入參為多個時,則只能使用 #{0}到#{n} 和 #{param0}到#{paramn} 來填入屬性值了;但由於動態SQL下的標簽僅識別 #{0} 等格式的占位符,因此建議通過使用 #{0} 格式的占位符,保持代碼一致性。
3. resultType屬性就是返回值類型。
4. sql標簽則用於重用SQL片段,示例如下:
<sql id="studentCols"> Name, ClassID </sql> <select id="qryStudent" resultType="EStudent"> select <include id="studentCols"/> from TStudentML </select>
5. 模糊查詢
網上有很多做法,但試過不是效果不好,就是報錯,誤打誤撞發現最原始的做法 '%'+#{0}+'%'就OK了!
6. 一對一關系
一對一關系MyBatis為我們提供 嵌套結果、嵌套查詢 兩種查詢方式。由於嵌套查詢需要向數據庫執行兩次查詢操作,因此推薦使用嵌套結果方式。
嵌套結果示例:
<!-- resultMap屬性值為配置返回值映射信息的resultMap標簽的id值 -->
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class c inner join teacher t on(c.tid = t.id) where t.id = #{0} </select>
<!-- type屬性值為返回值的JAVA數據類型 --> <resultMap id="ClassResultMap" type="Cls">
<!--
id標簽表示對象屬性對應的是表主鍵
result標簽表示對象屬性對應的是普通表字段
注意:必須用id或result標出需要返回的字段/屬性映射,否則在查詢多條記錄時,僅會返回最后一條記錄
--> <id property="id" column="c_id"/> <result property="name" column="c_name"/>
<!--
一對一關系采用association標簽配置關聯信息
javaType為JAVA數據類型
--> <association property="teacher" javaType="Teacher"> <id property="id" column="t_id"/> <result property="name" column="t_name"/> </association> </resultMap>
嵌套查詢示例:
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class where tid = #{id} </select> <select id="getTeacher" parameterType="int" resultType="Teacher"> select t_id id, t_name name from teacher where t_id = #{0} </select> <resultMap id="ClassResultMap" type="Cls"> <id property="id" column="c_id"/> <result property="name" column="c_name"/>
<!--
select屬性值為第二執行SQL語句id
而column屬性值為傳遞給第二執行SQL語句的入參,而且入參為第一次SQL語句的查詢結果集字段值
注意:若嵌套查詢的條件不只一個,那么就需要將column屬性設置為column="{prop1: fie;d1, prop2: field2}",然后嵌套查詢的SQL中通過#{prop1},#{prop2}獲取查詢條件值
--> <association prorperty="teacher" column="tid" select="getTeacher"> </association> </resultMap>
7. 一對多關系
一對多關系同樣分為 嵌套結果 和嵌套查詢兩種,由於嵌套查詢會由於N+1次查詢導致性能下降,一般推薦使用嵌套結果的做法,但有些查詢操作必須使用嵌套查詢才能完成。
嵌套結果示例:
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class c inner join student s on(c.id = s.cid) where c.id = #{id} </select> <resultMap id="ClassResultMap" type="Cls">
<!--
注意:必須用id或result標出需要返回的字段/屬性映射,否則在查詢多條記錄時,僅會返回最后一條記錄
--> <id property="id" column="c_id"/> <result property="name" column="c_name"/>
<!--
一對多關系采用collection標簽來配置映射信息
ofType屬性值為返回值的JAVA數據類型
--> <collection prorperty="students" ofType="Student"> <id property="id" column="s_id"/> <result property="name" column="s_name"/> </collection> </resultMap>
嵌套查詢示例:
<select id="getClass" parameterType="int" resultMap="ClassResultMap"> select * from class where tid = #{id} </select> <select id="getStudents" parameterType="int" resultType="Student"> select s_id id, s_name name from student where cid = #{id} </select> <resultMap id="ClassResultMap" type="Cls">
<!-- 注意:若不寫這句,那么c_id字段將作為嵌套查詢的條件,而不會賦值到id屬性了 --> <id property="id" column="c_id"/> <result property="name" column="c_name"/> <collection prorperty="students" column="c_id" select="getStudents"> </collection> </resultMap>
8. 動態SQL
MyBatis的動態SQL與數據庫中通過exec、sp_executesql()等執行的動態SQL目的是一致的,只是操作形式的不同而已。MyBatis的動態SQL定義方式上與文本模板定義無異,定義后均為經過類似於模板引擎的模塊進行解析得到最終的數據。下面我們來了解具體的標簽吧。
[a]. <if test="邏輯條件判斷"></if>
如果test內返回true,則標簽體的內容將被添加到最終結果中。示例:
<if test="name != null and job != null"> and Name like '%'+#{name}+'%' and Description like '%'+#{job}+'%' </if>
注意:test語句中的邏輯條件判斷必須使用入參的屬性名或鍵名,而不能使用#{0}或#{1}等形式的入參,否則條件判斷一律視為true。
[b]. <choose></choose>
相當於Java的switch語句。示例:
<choose> <when test="#{title} !=null"> and Title = #{title} </when> <when test="#{name} !=null"> and Name = #{name} </when> <otherwise> and Age > 10 </otherwise> </choose>
[c]. <where></where>
用於處理動態條件時,where留存與否的尷尬。具體就是
select * from tbl where <if test="#{name}!=null"> Name = #{name} </if> <if test="#{title}!=null"> and Title = #{title} </if>
當兩個條件都不符合時,sql語句就變成 select * from tbl where ,報錯是必然的。而 where標簽 會根據其標簽體是否有值來決定是否插入where關鍵字,並會自動去除無用的 or 和 and 關鍵字。示例:
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG <where> <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </where> </select>
[d]. <set></set>
用於在update語句中,動態設置更新的列。示例:
<update id="updateAuthorIfNecessary"> update Author <set> <if test="username != null">username=#{username},</if> <if test="password != null">password=#{password},</if> <if test="email != null">email=#{email},</if> <if test="bio != null">bio=#{bio}</if> </set> where id=#{id} </update>
[e].<trim></trim>
當標簽體有內容時則為內容添加前綴、后綴,而且可以除去內容前后部分內容。與 where標簽 功能相同的示例:
<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG
<!--
prefix屬性值為添加到內容的前綴信息
prefixOverrides屬性值為除去內容前的內容,當需要除去多個內容時,使用管道符|分割,注意:空格也將被除去
--> <trim prefix="where" prefixOverrides="AND |OR "> <if test="state != null"> state = #{state} </if> <if test="title != null"> AND title like #{title} </if> <if test="author != null and author.name != null"> AND author_name like #{author.name} </if> </trim> </select>
與 set標簽 功能相同的示例:
<update id="updateAuthorIfNecessary"> update Author <trim prefix="set" suffixOverrides=","> <if test="username != null">username=#{username},</if> <if test="password != null">password=#{password},</if> <if test="email != null">email=#{email},</if> <if test="bio != null">bio=#{bio}</if> </trim> where id=#{id} </update>
[f]. <foreach></foreach>
主要用於IN關鍵字,示例:
<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </select>
入參為List或Array類型時,MyBatis會自動將其添加到內部Map對象中,並且List類型對象的鍵名為list,Array類型對象的鍵名為array,並通過 foreach標簽 的collection屬性指定入參的鍵名。而item屬性用於指定 foreach標簽 內集合元素的占位符名稱,index屬性則指定 foreach標簽 內當前元素索引的占位符名稱,而open、close和separator屬性則分別指定動態SQL的開頭、結束文本和集合元素項間的分隔符。
3. 操作數據
如上文示例那樣,通過 SqlSessionFactoryBuilder實例 生成 SqlSessionFactory實例 ,然后在生成操作數據庫的 SqlSession實例 。
需要注意的是:
a. 每次使用完 SqlSession實例 必須調用其 close() 方法釋放鏈接;
b. 由於mybatis-config.xml中 <transactionManager type="JDBC"></transactionManager> 而且通過 SqlSessionFactory實例.openSession() 獲取鏈接對象,因此鏈接對象默認時不會自動提交增、刪和改操作的,因此需要調用 commit() 方法手動提交操作。
4. 生命周期
[a]. SqlSessionFactoryBuilder
由於`SqlSessionFactoryBuilder`實例用於生成`SqlSessionFactory`實例而已,因此並沒有必要以應用程序全局作為作用域,並且無必要多線程共享。因此作為函數的局 部變量使用即可。
[b]. SqlSessionFactory
作為數據庫連接池和連接池管理器使用,為達到數據庫連接復用的效果,`SqlSessionFactory`實例應當以程序全局作為作用域,並且多線程共享。采用單例或靜態單例模式較好
[c]. SqlSession
由於`SqlSession`實例非線程安全,因此作為函數的局部變量使用。而且由於數據庫連接為共享資源,因此必須遵循晚調用早釋放原則, 確保調用`close()`函數釋放連接。
四、總結
初嘗MyBatis時會覺得麻煩,尤其是使用過Hibernate或其他可將對象模型實例自動轉成SQL語句的框架的朋友來說,這確實太不方便了,而且容易出錯。其實我們可以將很多工作交給相關的工具去解決。以后慢慢說吧!
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4014819.html ^_^肥仔John