MyBatis框架的學習(三)——Dao層開發方法
使用MyBatis開發Dao層,通常有兩個方法,即原始Dao開發方法和Mapper介面開發方法。本文案例程式碼的編寫是建立在前文MyBatis框架的學習(二)——MyBatis架構與入門案例基礎之上的!
需求
明確開發需求,在實際開發中,我們總歸是要開發Dao層的,所以在本文中我使用MyBatis這個框架技術開發Dao層來將以下功能一一實現:
- 根據使用者id查詢一個使用者資訊
- 根據使用者名稱稱模糊查詢使用者資訊列表
- 新增使用者資訊
MyBatis常用API的使用範圍
在講解使用MyBatis這個框架技術開發Dao層之前,首先來說一下MyBatis常用的API。
SqlSessionFactoryBuilder的使用範圍
SqlSessionFactoryBuilder用於建立SqlSessionFacoty,SqlSessionFacoty一旦建立完成就不需要SqlSessionFactoryBuilder了,因為SqlSession是通過SqlSessionFactory生產,所以可以將SqlSessionFactoryBuilder當成一個工具類使用,最佳使用範圍是方法範圍(即方法體內區域性變數)。
SqlSessionFactory的使用範圍
SqlSessionFactory是一個介面,介面中定義了openSession的不同過載方法,SqlSessionFactory的最佳使用範圍是整個應用執行期間,一旦建立後可以重複使用,通常以單例模式管理SqlSessionFactory。
SqlSession的使用範圍
SqlSession中封裝了對資料庫的操作,如查詢、插入、更新、刪除等。通過SqlSessionFactory建立SqlSession,而SqlSessionFactory是通過SqlSessionFactoryBuilder進行建立的。
SqlSession是一個面向程式設計師的介面,SqlSession中定義了資料庫操作方法,所以SqlSession作用是操作資料庫,並且SqlSession物件要儲存資料庫連線、事務和一級快取結構等。
每個執行緒都應該有它自己的SqlSession例項。SqlSession的例項不能共享使用,它是執行緒不安全的(多執行緒訪問系統,當多執行緒同時使用一個SqlSession物件時會造成資料衝突問題)。由於SqlSession物件是執行緒不安全的,因此它的最佳使用範圍是請求或方法範圍(也可說為SqlSession的最佳使用場合是在方法體內作為區域性變數來使用),絕對不能將SqlSession例項的引用放在一個類的靜態欄位或例項欄位中。
開啟一個SqlSession,使用完畢就要關閉它。通常把這個關閉操作放到finally塊中以確保每次都能執行關閉。如下:
SqlSession session = sqlSessionFactory.openSession();
try {
// do work
} finally {
session.close();
}
原始Dao開發方式
原始Dao開發方法需要程式設計師自己編寫Dao介面和Dao實現類。
在工程的src目錄下建立一個cn.itheima.mybatis.dao包,並在該包下編寫一個UserDao介面:
public interface UserDao {
User getUserById(int id);
List<User> getUserByName(String username);
void insertUser(User user);
}
接著再在src目錄下建立一個cn.itheima.mybatis.dao.impl包,在該包下編寫UserDao介面的實現類——UserDaoImpl.java:
public class UserDaoImpl implements UserDao {
private SqlSessionFactory sqlSessionFactory;
public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public User getUserById(int id) {
SqlSession sqlSession = sqlSessionFactory.openSession();
// 根據id查詢使用者資訊
User user = sqlSession.selectOne("getUserById", id);
// 關閉sqlSession
sqlSession.close();
return user;
}
@Override
public List<User> getUserByName(String username) {
// 建立一個SQLSession物件
SqlSession sqlSession = sqlSessionFactory.openSession();
// 執行查詢
List<User> list = sqlSession.selectList("getUserByName", username);
// 釋放資源
sqlSession.close();
return list;
}
@Override
public void insertUser(User user) {
// 建立一個SQLSession物件
SqlSession sqlSession = sqlSessionFactory.openSession();
// 插入使用者
sqlSession.insert("insertUser", user);
// 提交事務
sqlSession.commit();
// 釋放資源
sqlSession.close();
}
}
最後建立一個JUnit的測試類——UserDaoTest.java,對UserDao介面進行測試。步驟如下:
- 在工程下新建一個原始碼目錄,專門用於存放JUnit的單元測試類。
- 右鍵【UserDao.java】→【New】→【JUnit Test Case】
- 在彈出的對話方塊中,修改單元測試類的存放目錄為test原始碼目錄
- 點選【Next】按鈕,在彈出的對話方塊中勾選全部測試方法,然後點選【Finish】完成
這樣就會產生如下效果:
最後將JUnit的單元測試類——UserDaoTest.java修改為:
public class UserDaoTest {
private SqlSessionFactory sqlSessionFactory = null; // 工廠物件一般在我們的系統中是單例的
@Before
public void init() throws IOException {
// 第一步,建立SqlSessionFactoryBuilder物件
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 第二步,載入配置檔案
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 第三步,建立SqlSessionFactory物件
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
}
@Test
public void testGetUserById() {
UserDao userDao = new UserDaoImpl(sqlSessionFactory);
User user = userDao.getUserById(10);
System.out.println(user);
}
@Test
public void testGetUserByName() {
UserDao userDao = new UserDaoImpl(sqlSessionFactory);
List<User> list = userDao.getUserByName("張");
for (User user : list) {
System.out.println(user);
}
}
@Test
public void testInsertUser() {
UserDao userDao = new UserDaoImpl(sqlSessionFactory);
User user = new User();
user.setUsername("趙雲");
user.setAddress("正定");
user.setBirthday(new Date());
user.setSex("1");
userDao.insertUser(user);
}
}
讀者可自行測試,在此不做過多贅述。
原始Dao開發方式所帶來的問題
從以上UserDaoImpl類的程式碼可看出原始Dao開發存在以下問題:
dao介面實現類方法中存在大量的重複程式碼,這些重複的程式碼就是模板程式碼。
模板程式碼為:- 先建立sqlsession
- 再呼叫sqlsession的方法
- 再提交sqlsession
- 再關閉sqlsession
設想能否將這些程式碼提取出來,這可大大減輕程式設計師的工作量。
- 呼叫sqlSession的資料庫操作方法需要指定statement的id,這裡存在硬編碼,不得於開發維護。
- 呼叫sqlsession方法時傳入的變數,由於sqlsession方法使用泛型,即使變數型別傳入錯誤,在編譯階段也不報錯,不利於程式設計師開發。
下面我來使用mapper代理方法來開發Dao層,來解決上面我們所發現的問題。
Mapper動態代理開發方式
開發規範
Mapper介面開發方法只需要程式設計師編寫Mapper介面(相當於Dao介面),由Mybatis框架根據介面定義建立介面的動態代理物件,代理物件的方法體同上邊Dao介面實現類方法。
Mapper介面開發需要遵循以下規範:
- Mapper.xml檔案中的namespace與mapper介面的類路徑相同,即namespace必須是介面的全限定名。
- Mapper介面方法名和Mapper.xml中定義的每個statement的id相同。
- Mapper介面方法的輸入引數型別和mapper.xml中定義的每個sql的parameterType的型別相同。
- Mapper介面方法的輸出引數型別和mapper.xml中定義的每個sql的resultType的型別相同。
接下來我就來編碼使用mapper動態代理方式來開發Dao層。
編寫Mapper.xml(對映檔案)
我們可在config原始碼目錄下新建一個mapper的普通資料夾,該資料夾專門用於存放對映檔案。然後在該資料夾下建立一個名為mapper.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="cn.itheima.mybatis.mapper.UserMapper">
<select id="getUserById" parameterType="int" resultType="USer">
select * from user where id=#{id};
</select>
<select id="getUserByName" parameterType="string" resultType="cn.itheima.mybatis.po.User">
SELECT * FROM `user` WHERE username LIKE '%${value}%'
</select>
<insert id="insertUser" parameterType="cn.itheima.mybatis.po.User">
<selectKey keyProperty="id" resultType="int" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO `user` (username,birthday,sex,address) VALUES (#{username},#{birthday},#{sex},#{address})
</insert>
</mapper>
編寫Mapper介面
在工程的src目錄下新建一個cn.itheima.mybatis.mapper包,並在該包下建立一個Mapper介面——UserMapper.java:
public interface UserMapper {
User getUserById(int id);
List<User> getUserByName(String username);
void insertUser(User user);
}
介面定義有如下特點:
- mapper介面方法名和mapper.xml中定義的statement的id相同。
- mapper介面方法的輸入引數型別和mapper.xml中定義的statement的parameterType的型別相同。
- mapper介面方法的輸出引數型別和mapper.xml中定義的statement的resultType的型別相同。
載入mapper.xml對映檔案
在SqlMapConfig.xml檔案新增如下配置:
<mapper resource="mapper/mapper.xml"/>
如此一來,SqlMapConfig.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>
<!-- 和spring整合後 environments配置將廢除 -->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事務管理 -->
<transactionManager type="JDBC" />
<!-- 資料庫連線池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" />
<property name="username" value="root" />
<property name="password" value="yezi" />
</dataSource>
</environment>
</environments>
<!-- 載入mapper檔案 -->
<mappers>
<!-- resource是基於classpath來查詢的 -->
<mapper resource="sqlmap/user.xml"/>
<mapper resource="mapper/mapper.xml"/>
</mappers>
</configuration>
編寫測試程式
最後編寫UserMapper介面的一個單元測試類,具體步驟我已在本文中詳細說明了。修改UserMapperTest這個單元測試類的內容為:
public class UserMapperTest {
private SqlSessionFactory sqlSessionFactory = null; // 工廠物件一般在我們的系統中是單例的
@Before
public void init() throws IOException {
// 第一步,建立SqlSessionFactoryBuilder物件
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 第二步,載入配置檔案
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 第三步,建立SqlSessionFactory物件
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
}
@Test
public void testGetUserById() {
// 和Spring整合後就省略了
SqlSession sqlSession = sqlSessionFactory.openSession();
// 獲得代理物件(到時候就只需要通過Spring容器拿到UserMapper介面的代理物件就可以了)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(10);
System.out.println(user);
// 和Spring整合後就省略了
sqlSession.close();
}
@Test
public void testGetUserByName() {
// 和Spring整合後就省略了
SqlSession sqlSession = sqlSessionFactory.openSession();
// 獲得代理物件(到時候就只需要通過Spring容器拿到UserMapper介面的代理物件就可以了)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> list = userMapper.getUserByName("張");
for (User user : list) {
System.out.println(user);
}
// 和Spring整合後就省略了
sqlSession.close();
}
@Test
public void testInsertUser() {
// 讀者自己編寫程式碼測試......
}
}
以上方法測試均沒問題,讀者如若不信可親測。
小結
selectOne和selectList
動態代理物件呼叫sqlSession.selectOne()和sqlSession.selectList()是根據mapper介面方法的返回值決定,如果返回list則呼叫selectList方法,如果返回單個物件則呼叫selectOne方法。
namespace
mybatis官方推薦使用mapper代理方法開發mapper介面,程式設計師不用編寫mapper介面實現類,使用mapper代理方法時,輸入引數可以使用pojo包裝物件或map物件,保證dao的通用性。
SqlMapConfig.xml配置檔案
配置內容
SqlMapConfig.xml檔案中配置的內容和順序如下:
- properties(屬性)
- settings(全域性配置引數)
- typeAliases(類型別名)
- typeHandlers(型別處理器)
- objectFactory(物件工廠)
- plugins(外掛)
- environments(環境集合屬性物件)
- environment(環境子屬性物件)
- transactionManager(事務管理)
- dataSource(資料來源)
- environment(環境子屬性物件)
- mappers(對映器)
properties(屬性)
在SqlMapConfig.xml配置檔案中,我們可把資料庫連線資訊配置到properties標籤當中,類似如下:
<!-- 配置屬性 -->
<properties>
<property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
</properties>
接下來把以上配置資訊新增到SqlMapConfig.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>
<property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
</properties>
<!-- 和spring整合後 environments配置將廢除 -->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事務管理 -->
<transactionManager type="JDBC" />
<!-- 資料庫連線池 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="root" />
<property name="password" value="yezi" />
</dataSource>
</environment>
</environments>
<!-- 載入mapper檔案 -->
<mappers>
<!-- resource是基於classpath來查詢的 -->
<mapper resource="sqlmap/user.xml"/>
<mapper resource="mapper/mapper.xml"/>
</mappers>
</configuration>
SqlMapConfig.xml檔案向上面這樣配置,雖然沒問題,但是我們不覺得這樣很不爽嗎?我們一般都是將資料庫連線資訊配置到一個java屬性檔案中,然後再來引用其中的配置資訊。我按照這種指導思想在classpath下定義一個db.properties檔案,SqlMapConfig.xml檔案引用該屬性檔案中的配置資訊如下:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8
jdbc.username=root
jdbc.password=yezi
如此一來,SqlMapConfig.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 resource="db.properties">
<property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
</properties>
<!-- 和spring整合後 environments配置將廢除 -->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事務管理 -->
<transactionManager type="JDBC" />
<!-- 資料庫連線池 -->
<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>
</environment>
</environments>
<!-- 載入mapper檔案 -->
<mappers>
<!-- resource是基於classpath來查詢的 -->
<mapper resource="sqlmap/user.xml"/>
<mapper resource="mapper/mapper.xml"/>
</mappers>
</configuration>
注意:MyBatis將按照下面的順序來載入屬性:
- 在properties元素體內定義的屬性首先被讀取
- 然後會讀取properties元素中resource或url載入的屬性,它會覆蓋已讀取的同名屬性
按照我自己的話說就是:先載入property元素內部的屬性,然後再載入db.properties檔案外部的屬性,如果有同名屬性則會覆蓋。
如若讀者不信,可以將properties元素的內容修改為:
<properties resource="db.properties">
<property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
<property name="jdbc.username" value="hello"/>
</properties>
以上故意將資料庫連線的使用者名稱給整錯,結果發現仍然好使,這足以說明問題了。
typeAliases(類型別名)
mybatis支援別名
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 |
map | Map |
自定義別名
如果我們在SqlMapConfig.xml配置檔案新增如下配置資訊:
<!-- 配置pojo的別名 -->
<typeAliases>
<!-- 單個別名定義 -->
<typeAlias type="cn.itheima.mybatis.po.User" alias="user"/>
</typeAliases>
以上就為User類定義了一個別名(user)。
那麼我們就可以在mapper.xml對映檔案中使用這個別名了,如下:
<mapper namespace="cn.itheima.mybatis.mapper.UserMapper">
<!-- 別名不區分大小寫 -->
<select id="getUserById" parameterType="int" resultType="USer">
select * from user where id=#{id};
</select>
......
</mapper>
注意:resultType屬性的值就是User類的別名,且別名是不區分大小寫的。
聰明的小夥伴們肯定發現如果像這樣為每一個pojo定義一個別名,並不是明智之舉,萬一一個專案裡面有很多pojo類呢?所以我們可以批量定義別名,如下:
<!-- 配置pojo的別名 -->
<typeAliases>
<!-- 批量別名定義,掃描包的形式建立別名,別名就是類名,且不區分大小寫 -->
<package name="cn.itheima.mybatis.po"/>
</typeAliases>
SqlMapConfig.xml檔案載入mapper.xml檔案
Mapper(對映器)配置的幾種方法:
<mapper resource=" " />
使用相對於類路徑的資源,如<mapper resource="sqlmap/user.xml"/>
<mapper class=" " />
使用mapper介面類路徑,如:<mapper class="cn.itheima.mybatis.mapper.UserMapper"/>
注意:此種方法要求mapper介面名稱和mapper對映檔名稱相同,且放在同一個目錄中。<package name=""/>
註冊指定包下的所有mapper介面,如:<package name="cn.itheima.mybatis.mapper"/>
注意:此種方法要求mapper介面名稱和mapper對映檔名稱相同,且放在同一個目錄中。
雖然Mapper(對映器)配置有以上三種方法,但是實際開發中就用第三種方法。至此,使用MyBatis開發Dao層我就總結到這裡。