2021-1-02----ssm之第2章 SQL對映檔案
第2章 SQL對映檔案
❖ 掌握通過SQL對映檔案進行增、刪、改、查的方法
❖ 掌握引數的使用方法
❖ 掌握resultMap
❖ 瞭解Cache的使用方法
任務1:實現條件查詢任務
2:實現增刪改操作任務
3:實現高階結果對映任務
4:配置resultMap自動對映級別和MyBatis快取
任務1 實現條件查詢
關鍵步驟如下。
➢ 使用select元素實現根據使用者名稱模糊查詢使用者列表資訊。
➢ 使用select元素實現多條件查詢使用者列表資訊。
➢ 使用resultMap元素對映自定義查詢結果。
2.1.1 SQL對映檔案
➢ mapper:對映檔案的根元素節點,只有一個屬性namespace(名稱空間),其作用如下。♦ 用於區分不同的mapper,全域性唯一。♦ 繫結DAO介面,即面向介面程式設計。當namespace繫結某一介面之後,可以不用寫該介面的實現類,MyBatis會通過介面的完整限定名查詢到對應的mapper配置來執行SQL語句。因此namespace的命名必須跟介面同名。
➢ cache:配置給定名稱空間的快取。
➢ cache-ref:從其他名稱空間引用快取配置。
➢ resultMap:用來描述資料庫結果集和物件的對應關係。
➢ sql:可以重用的SQL塊,也可以被其他語句引用。
➢ insert:對映插入語句。
➢ update:對映更新語句。
➢ delete:對映刪除語句。
➢ select:對映查詢語句。
注意
MyBatis的SQL對映檔案中mapper元素的namespace屬性有如下要求:(1)namespace的命名必須跟某個DAO介面同名,同屬於DAO層,故在程式碼結構上,對映檔案與該DAO介面應放置在同一package下(如cn.smbms.dao. user),並且習慣上都是以Mapper結尾(如UserMapper.java、UserMapper.xml)。
(2)在不同的mapper檔案中,子元素的id可以相同,MyBatis通過namespace和子元素的id聯合區分。介面中的方法與對映檔案中SQL語句id應一一對應。
2.1.2 單條件查詢
與查詢對應的select元素是使用MyBatis時最常用的。在上一章,我們實現了對使用者表的簡單查詢,現在升級需求,增加查詢條件,那麼如何實現帶引數和返回複雜型別的查詢?這就需要先詳細瞭解select元素的屬性。以根據使用者名稱模糊查詢來獲取使用者列表資訊為例,
➢ id:名稱空間中唯一的識別符號,可以被用來引用這條語句。
➢ parameterType:表示查詢語句傳入引數的型別的完全限定名或別名。它支援基礎資料型別和複雜資料型別。在示例1中使用的是基礎資料型別“string”,這是一個別名,代表String,屬於一個內建的型別別名。對於普通的Java型別,有許多內建的型別別名,並且它們對大小寫不敏感。
除了內建的型別別名外,還可以為自定義的類設定別名。
別名(typeAliases)在mybatis-config.xml中的設定,在對映檔案中可直接使用別名,以減少配置檔案的程式碼。
➢ resultType:查詢語句返回結果型別的完全限定名或別名。別名的使用方式與parameterType相同。
<select id="getUserListByUserName" resultType="User" parameterType="String">
select * from smbms_user where userName like CONCAT ('%',#{userName},'%')
</select>
這是一個id為getUserListByUserName的對映語句,引數型別為string,返回結果的型別是User。為了使資料庫查詢的結果和返回型別中的屬效能夠自動匹配、以便於開發,對於MySQL資料庫和JavaBean都會採用同一套命名規則,即Java命名駝峰規則,這樣就不需要再做對映(注:如果資料庫表的欄位名和屬性名不一致則需要手動對映)。引數的傳遞使用#{引數名},它告訴MyBatis生成PreparedStatement引數。
@Test
public void testGetUserListByUserName(){
SqlSession sqlSession = null;
List<User> userList = new ArrayList<User>();
try {
sqlSession = MyBatisUtil.createSqlSession();
//第一種方式:呼叫selectList方法執行查詢操作
//userList = sqlSession.selectList("cn.smbms.dao.user.UserMapper.getUserListByUserName","趙");
//第二種方式:呼叫getMapper(Mapper.class)執行dao介面方法來實現對資料庫的查詢操作
userList = sqlSession.getMapper(UserMapper.class).getUserListByUserName("趙");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
for(User user: userList){
logger.debug("testGetUserListByUserName userCode: " + user.getUserCode() + " and userName: " + user.getUserName());
}
}
2.1.3 多條件查詢
通過一個條件對使用者表進行查詢操作,但在實際應用中,資料查詢會有多種條件,結果也會有各種型別
查詢條件包括:使用者名稱(模糊查詢)、使用者角色。那對於多條件查詢,該如何實現?我們可以考慮將查詢條件封裝成物件進行入參,改造UserMapper.java
public List<User> getUserList(User user);
<!-- 查詢使用者列表(引數:物件入參) -->
<select id="getUserList" resultType="User" parameterType="User">
select * from smbms_user
where userName like CONCAT ('%',#{userName},'%') and userRole = #{userRole}
</select>
@Test
public void testGetUserList(){
SqlSession sqlSession = null;
List<User> userList = new ArrayList<User>();
try {
sqlSession = MyBatisUtil.createSqlSession();
User user = new User();
user.setUserName("趙");
user.setUserRole(3);
//第一種方式:呼叫selectList方法執行查詢操作
//userList = sqlSession.selectList("cn.smbms.dao.user.UserMapper.getUserList",user);
//第二種方式:呼叫getMapper(Mapper.class)執行dao介面方法來實現對資料庫的查詢操作
userList = sqlSession.getMapper(UserMapper.class).getUserList(user);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
for(User user: userList){
logger.debug("testGetUserList userCode: " + user.getUserCode() + " and userName: " + user.getUserName());
}
}
parameterType使用了複雜資料型別,把條件引數封裝成User物件進行入參。對User物件中userName和userRole兩個屬性分別進行賦值,在對映的查詢語句中設定parameterType為User型別,傳入引數分別使用#{userName}和#{userRole}來表示,即#{屬性名}(引數物件中的屬性名)。
parameterType支援的複雜資料型別除了JavaBean之外,還有Map型別,改造上一示例,把使用者名稱和使用者角色封裝成Map物件進行入參,測試類UserMapperTest.java的關鍵程式碼如示例5所示。
public List<User> getUserListByMap(Map<String, String> userMap);
改造UserMapper.xml,將parameterType設定為Map, SQL語句中的引數值使用#{uName}和#{uRole}來表示,即#{Map的key}
<select id="getUserListByMap" resultType="User" parameterType="Map">
select * from smbms_user
where userName like CONCAT ('%',#{uName},'%') and userRole = #{uRole}
</select>
這種做法更加靈活,不管是什麼型別的引數、有多少個引數,我們都可以把它們封裝成Map資料結構進行入參,通過Map的key即可獲取傳入的值。
@Test
public void testGetUserListByMap(){
SqlSession sqlSession = null;
List<User> userList = new ArrayList<User>();
try {
sqlSession = MyBatisUtil.createSqlSession();
Map<String, String> userMap = new HashMap<String, String>();
userMap.put("uName", "趙");
userMap.put("uRole", "3");
//第一種方式:呼叫selectList方法執行查詢操作
//userList = sqlSession.selectList("cn.smbms.dao.user.UserMapper.getUserListByMap",userMap);
//第二種方式:呼叫getMapper(Mapper.class)執行dao介面方法來實現對資料庫的查詢操作
userList = sqlSession.getMapper(UserMapper.class).getUserListByMap(userMap);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
for(User user: userList){
logger.debug("testGetUserListByMap userCode: " + user.getUserCode() + " and userName: " + user.getUserName());
}
}
注意
MyBatis傳入引數型別可以是Java基礎資料型別,但是隻適用於一個引數的情況,通過#{引數名}即可獲取傳入的值。若是多引數入參,則需要複雜資料型別來支援,包括Java實體類、Map,通過#{屬性名}或#{Map的key}來獲取傳入的引數值。
2.1.4 自定義查詢結果對映
(1)修改POJO:User.java增加userRoleName屬性,並修改查詢使用者列表的SQL語句,對使用者表(smbms_user)和角色表(smbms_role)進行聯表查詢,使用resultType自動對映。
(2)通過resultMap對映自定義結果。
注意
MyBatis中使用resultType做自動對映時,要注意欄位名和POJO的屬性名必須一致。若不一致,則需要給欄位起別名,保證別名與屬性名一致。
查詢出使用者資訊列表所必需的顯示欄位(包括使用者編碼、使用者名稱稱、性別、年齡、電話、使用者角色等欄位資訊),注意使用者角色要顯示角色名稱而不是角色id。首先需要在User類中加入userRoleName屬性:private StringuserRoleName,及其相應的getter和setter方法。然後修改UserMapper介面中的查詢使用者列表的getUserList()方法,在UserMapper. xml中,修改getUserList的SQL對映語句,並修改select的resultType屬性為resultMap,其屬性值為userList,
public List<User> getUserListByMap(Map<String, String> userMap);
通過getUserList的SQL語句,進行聯表查詢,可得到使用者對應角色的中文名稱。接下來在UserMapper.xml中增加id為userList的resultMap元素節點
<resultMap type="User" id="userList">
<result property="id" column="id"/>
<result property="userCode" column="userCode"/>
<result property="userName" column="userName"/>
<result property="phone" column="phone"/>
<result property="birthday" column="birthday"/>
<result property="gender" column="gender"/>
<result property="userRole" column="userRole"/>
<result property="userRoleName" column="roleName"/>
</resultMap>
<!-- 查詢使用者列表(引數:物件入參) -->
<select id="getUserList" resultMap="userList" parameterType="User">
select u.*,r.roleName from smbms_user u,smbms_role r
where u.userName like CONCAT ('%',#{userName},'%')
and u.userRole = #{userRole} and u.userRole = r.id
</select>
resultMap元素用來描述如何將結果集對映到Java物件,此處使用resultMap對列表展示所需的必要欄位進行自由對映,特別是當資料庫的欄位名和POJO中的屬性名不一致的情況下,比如角色名稱,欄位名column是roleName,而User物件的屬性名為userRoleName,此時就需要做對映。
resultMap元素的屬性值和子節點:
➢ id屬性:唯一標識,此id值用於對select元素resultMap屬性的引用。
➢ type屬性:表示該resultMap的對映結果型別。
➢ result子節點:用於標識一些簡單屬性,其中column屬性表示從資料庫中查詢的欄位名,property則表示查詢出來的欄位對應的值賦給實體物件的哪個屬性。
最後在測試類中進行相關欄位的輸出,展示列表(使用者編碼、使用者名稱稱、性別、年齡、電話、使用者角色)。注意:使用者角色不再是角色id,輸出的是角色名稱。MyBatis中在對查詢進行select對映的時候,返回型別可以用resultType,也可以用resultMap。
1.resultType
resultType直接表示返回型別,包括基礎資料型別和複雜資料型別。
2.resultMap
resultMap則是對外部resultMap定義的引用,對應外部resultMap的id,表示返回結果對映到哪一個resultMap上。它的應用場景一般是:資料庫欄位資訊與物件屬性不一致或者需要做複雜的聯合查詢,以便自由控制對映結果。
3.resultType和resultMap的關聯
在MyBatis進行查詢對映的時候,查詢出來的每個欄位值都放在一個對應的Map裡面,其中鍵是欄位名,值則是其對應的值。當select元素提供的返回型別屬性是resultType的時候,MyBatis會將Map裡面的鍵值對取出賦給resultType所指定的物件對應的屬性(即呼叫對應的物件裡的屬性的setter方法進行填充)。正因為如此,當使用resultType的時候,直接在後臺就能接收到其相應的物件屬性值。由此可看出,其實MyBatis的每個查詢對映的返回型別都是resultMap,只是當我們提供的返回型別屬性是resultType的時候,MyBatis會自動把對應的值賦給resultType所指定物件的屬性;而當我們提供的返回型別屬性是resultMap的時候,因為Map不能很好地表示領域模型,就需要通過進一步的定義把它轉化為對應的實體物件。
注意
在MyBatis的select元素中,resultType和resultMap本質上是一樣的,都是Map資料結構。但需要明確一點:resultType屬性和resultMap屬性絕對不能同時存在,只能二者選其一使用。
4.resultMap的自動對映級別
在上面的示例中,選擇部分欄位進行resultMap對映,我們希望沒有對映的欄位不能在後臺查詢並輸出,即使SQL語句中是查詢所有欄位(select * from……)。因為我們使用resultMap也是為了自由靈活地控制對映結果,達到只對關心的屬性進行賦值填充的目的。修改測試類(UserMapperTest.java)的輸出項
@Test
public void testGetUserList(){
SqlSession sqlSession = null;
List<User> userList = new ArrayList<User>();
try {
sqlSession = MyBatisUtil.createSqlSession();
User user = new User();
user.setUserName("趙");
user.setUserRole(3);
userList = sqlSession.getMapper(UserMapper.class).getUserList(user);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
/**
* 若設定resultMap的自動對映級別為NONE,
* 那麼沒有進行對映匹配的屬性(比如:address等)則輸出為null
* 若不設定resultMap的自動對映級別,則不管是否進行了對映,所有的屬性值均可輸出
*/
for(User user: userList){
logger.debug("testGetUserList userCode: " + user.getUserCode() +
" and userName: " + user.getUserName() +
" and userRole: " + user.getUserRole() +
" and userRoleName: " + user.getUserRoleName() +
" and age: " + user.getAge() +
" and address: " + user.getAddress());
}
}
在該示例程式碼中,對比之前設定的resultMap對映的屬性,增加了address和age兩個屬性值的輸出。觀察輸出結果,發現address和age的值均可正常輸出
為何沒有在resultMap中做對映關聯的age和address卻能正常輸出結果?若需求為沒有在resultMap內對映的欄位不能獲取,那麼又該如何實現?
這跟resultMap的自動對映級別有關,預設的對映級別為PARTIAL。要滿足新需求,則需要設定MyBatis對於resultMap的自動對映級別(autoMappingBehavior)為NONE,即禁止自動匹配。修改mybatis-config.xml
<settings>
<!-- 配置mybatis的log實現為LOG4J -->
<setting name="logImpl" value="LOG4J" />
<!-- 設定resultMap的自動對映級別為NONE(禁止自動匹配) -->
<setting name="autoMappingBehavior" value="NONE" />
</settings>
增加以上的設定之後,再進行結果的輸出,發現address屬性值為null,即該屬性沒有進行自動setter賦值,但是age的屬性值仍為30,並非為空。這是因為age屬性值並非直接取自資料表,而是在getAge()方法中通過birthday屬性計算得出,只要載入了birthday就可以計算出age
注意
在MyBatis中,使用resultMap能夠進行自動對映匹配的前提是欄位名和屬性名必須一致,在預設對映級別(PARTIAL)情況下,若欄位名和屬性名一致,即使沒有做屬性名和欄位名的匹配,也可以在後臺獲取到未匹配過的屬性值;若欄位名和屬性名不一致,且在resultMap裡沒有做對映,那麼就無法在後臺獲取並輸出。
任務2 實現增刪改操作
關鍵步驟如下。
➢ 使用insert元素實現使用者表的增加。
➢ 使用update元素實現根據使用者id修改使用者資訊。
➢ 使用@Param註解實現多引數入參。
➢ 使用delete元素實現根據使用者id刪除使用者。
2.2.1 增加操作
MyBatis實現增加操作,使用的是insert元素來對映插入語句。具體的用法很簡單,下面通過使用者表的增加操作示例來演示具體用法,首先在UserMapper介面裡增加add()方法。
public int add(User user);
要插入的User物件作為入參,返回值為int型別,即返回執行SQL語句影響的行數。修改UserMapper.xml,增加插入語句
<insert id="add" parameterType="User">
insert into smbms_user (userCode,userName,userPassword,gender,birthday,phone,
address,userRole,createdBy,creationDate)
values (#{userCode},#{userName},#{userPassword},#{gender},#{birthday},#{phone},
#{address},#{userRole},#{createdBy},#{creationDate})
</insert>
insert元素的屬性:
➢ id:與select元素的id一樣,是名稱空間中唯一的識別符號,可以被用來引用該條語句。
➢ parameterType:與select元素的parameterType一樣,是傳入引數的型別的完全限定名或別名。別名的含義和用法見select元素中的解釋。
public int add(User user);
注意
對於增刪改(insert、update、delete)這類資料庫更新操作,需要注意兩點:(1)該型別的操作本身預設返回執行SQL語句影響的行數,所以DAO層的介面方法的返回值一般設定為int型別。最好不要返回boolean型別。
(2)insert、update、delete元素中均沒有resultType屬性,只有查詢操作需要對返回結果的型別(resultType/resultMap)進行相應的指定。
接下來修改測試類UserMapperTest.java,增加testAdd()方法,進行插入資料測試,並開啟事務控制,模擬異常,若發生異常則回滾以測試事務。
@Test
public void testAdd(){
logger.debug("testAdd !===================");
SqlSession sqlSession = null;
int count = 0;
try {
sqlSession = MyBatisUtil.createSqlSession();
User user = new User();
user.setUserCode("test001");
user.setUserName("測試使用者001");
user.setUserPassword("1234567");
Date birthday = new SimpleDateFormat("yyyy-MM-dd").parse("1984-12-12");
user.setBirthday(birthday);
user.setCreationDate(new Date());
user.setAddress("地址測試");
user.setGender(1);
user.setPhone("13688783697");
user.setUserRole(1);
user.setCreatedBy(1);
user.setCreationDate(new Date());
count = sqlSession.getMapper(UserMapper.class).add(user);
//模擬異常,進行回滾
//int i = 2/0;
sqlSession.commit();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
sqlSession.rollback();
count = 0;
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
logger.debug("testAdd count: " + count);
}
那麼在此處測試中,當sqlSession執行add()方法之後需要進行commit,完成資料的插入操作。若在執行的過程中丟擲異常,那麼就必須在catch中進行回滾,以此來保證資料的一致性,同時設定count為0。
2.2.2 修改操作
MyBatis實現修改操作,使用的是update元素來對映修改語句。具體的用法與insert類似,下面通過根據使用者id修改使用者資訊的操作例項來演示具體用法。首先在UserMapper介面裡增加modify()方法。
public int modify(User user);
要修改的User物件作為入參,返回值為int型別,
即返回執行SQL語句影響的行數。修改UserMapper.xml,增加修改語句,
<update id="modify" parameterType="User">
update smbms_user set userCode=#{userCode},userName=#{userName},userPassword=#{userPassword},
gender=#{gender},birthday=#{birthday},phone=#{phone},address=#{address},
userRole=#{userRole},modifyBy=#{modifyBy},modifyDate=#{modifyDate}
where id = #{id}
</update>
update元素的屬性id和parameterType的含義和用法等同於insert元素中的屬性用法,此處不再贅述。另外,由於是修改操作,因此更新的欄位中需更新modifyBy和modifyDate,而不需要更新createBy和creationDate。
接下來修改測試類UserMapperTest.java,增加testModify方法進行修改資料測試,並開啟事務控制,模擬異常,若發生異常則回滾以測試事務。
@Test
public void testModify(){
logger.debug("testModify !===================");
SqlSession sqlSession = null;
int count = 0;
try {
User user = new User();
user.setId(25);
user.setUserCode("testmodify");
user.setUserName("測試使用者修改");
user.setUserPassword("0000000");
Date birthday = new SimpleDateFormat("yyyy-MM-dd").parse("1980-10-10");
user.setBirthday(birthday);
user.setCreationDate(new Date());
user.setAddress("地址測試修改");
user.setGender(2);
user.setPhone("13600002222");
user.setUserRole(2);
user.setModifyBy(1);
user.setModifyDate(new Date());
sqlSession = MyBatisUtil.createSqlSession();
count = sqlSession.getMapper(UserMapper.class).modify(user);
//模擬異常,進行回滾
//int i = 2/0;
sqlSession.commit();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
sqlSession.rollback();
count = 0;
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
logger.debug("testModify count: " + count);
}
2.2.3 多引數入參
實現的是根據使用者id修改使用者資訊操作,超市訂單管理系統還有一個需求:修改個人密碼。此需求也是修改操作,但是傳入引數有兩個:使用者id和新密碼。若按照之前封裝成User物件的方式進行引數傳遞,並不是很合適,這裡可以用更靈活的方式處理,即直接進行多引數入參,程式碼可讀性高,可清晰地看出這個介面方法所需的引數是什麼。
具體示例程式碼如下,修改UserMapper介面,增加修改個人密碼的方法,當方法引數有多個時,每個引數前都需增加@Param註解:
public int updatePwd(@Param("id")Integer id, @Param("userPassword")String pwd);
使用註解@Param來傳入多個引數,如@Param(“userPassword”)String pwd,相當於將該引數pwd重新命名為userPassword,在對映的SQL中需要使用#{註解名稱},如#{userPassword}。
下面繼續修改UserMapper.xml,增加id為updatePwd的SQL對映
<!-- 修改當前使用者密碼 -->
<update id="updatePwd">
update smbms_user set userPassword=#{userPassword} where id=#{id}
</update>
若不使用@Param註解,則會報錯,報錯資訊類似於Parameter ’引數名’ not found。探究原因,需要深入MyBatis原始碼,MyBatis的引數型別為Map,如果使用@Param註解引數,那麼就會記錄指定的引數名為key;如果引數前沒有加@Param,那麼就會使用“param”+它的序號作為Map的key。所以在進行多引數入參時,如果沒有使用@Param指定引數,那麼在對映的SQL語句中將獲取不到#{引數名},從而報錯。
最後修改測試類UserMapperTest.java,增加testUpdatePwd方法進行個人密碼修改測試
@Test
public void testUpdatePwd() {
logger.debug("testUpdatePwd !===================");
SqlSession sqlSession = null;
String pwd = "8888888";
Integer id = 1;
int count = 0;
try {
sqlSession = MyBatisUtil.createSqlSession();
count = sqlSession.getMapper(UserMapper.class).updatePwd(id, pwd);
sqlSession.commit();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
sqlSession.rollback();
count = 0;
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
logger.debug("testUpdatePwd count: " + count);
}
在該測試方法中,不需要再封裝User物件,直接進行兩個引數的入參即可,清晰明瞭。
經驗
在MyBatis中使用引數入參,何時需要封裝成物件入參,何時又需要使用多引數入參?
一般情況下,超過4個以上的引數最好封裝成物件入參(特別是在常規的增加和修改操作時,欄位較多,封裝成物件比較方便)。
對於引數固定的業務方法,最好使用多引數入參。因為這種方法比較靈活,程式碼的可讀性高,可以清晰地看出介面方法中所需的引數是什麼。並且對於固定的介面方法,引數一般是固定的,所以直接採用多引數入參,無需封裝物件。比如修改個人密碼、根據使用者id刪除使用者、根據使用者id檢視使用者明細,都可以採取這種入參方式。
需要注意的是,當引數為基礎資料型別時,不管是多引數入參,還是單獨的一個引數入參,都需要使用@Param註解來進行引數的傳遞。
2.2.4 刪除操作
MyBatis實現刪除操作是使用delete元素來對映刪除語句。具體的用法與insert、update類似,下面通過根據使用者id刪除使用者的操作示例來演示具體用法,首先在UserMapper介面中增加delete方法:
public int deleteUserById(@Param(“id”)Integer delId);
引數:delId(使用者id),使用@Param註解來指定引數名為id,返回值為int型別,即返回執行SQL語句影響的行數。
修改UserMapper.xml,增加刪除語句
<!-- 根據userId刪除使用者資訊 -->
<delete id="deleteUserById" parameterType="Integer">
delete from smbms_user where id=#{id}
</delete>
接下來修改測試類UserMapperTest.java,增加testDeleteUserById方法,進行刪除資料測試,並開啟事務控制,模擬異常,若發生異常則回滾以測試事務。
Test
public void testDeleteUserById() {
logger.debug("testDeleteUserById !===================");
SqlSession sqlSession = null;
Integer delId = 25;
int count = 0;
try {
sqlSession = MyBatisUtil.createSqlSession();
count = sqlSession.getMapper(UserMapper.class).deleteUserById(delId);
sqlSession.commit();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
sqlSession.rollback();
count = 0;
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
logger.debug("testDeleteUserById count: " + count);
}
任務3 實現高階結果對映
➢ 使用association實現根據使用者角色id獲取該角色下的使用者列表。
➢ 使用collection實現獲取指定使用者的相關資訊和地址列表。
2.3.1 resultMap的配置
1.屬性
➢ id:resultMap的唯一標識。
➢ type:表示該resultMap的對映結果型別(通常是Java實體類)。
2.子節點
➢ id:一般對應資料庫中該行的主鍵id,設定此項可以提升MyBatis效能。
➢ result:對映到JavaBean的某個“簡單型別”屬性,如基礎資料型別、包裝類等。
子節點id和result均可實現最基本的結果集對映,將列對映到簡單資料型別的屬性。這兩者的唯一不同是:在比較物件例項時id將作為結果集的標識屬性。這有助於提高總體效能,特別是在應用快取和巢狀結果對映的時候。而若要實現高階結果對映,就需要學習下面兩個配置項:association和collection。
2.3.2 使用association處理一對一關聯關係
association:對映到JavaBean的某個“複雜型別”屬性,比如JavaBean類,即JavaBean內部巢狀一個複雜資料型別(JavaBean)屬性,這種情況就屬於複雜型別的關聯。需要注意:association僅處理一對一的關聯關係。
下面通過一個示例來演示association的具體應用,示例需求:根據使用者角色id獲取該角色下的使用者列表。
首先修改User類,增加角色屬性(Role role),並增加其相應的getter和setter方法;註釋掉使用者角色名稱屬性(StringuserRoleName),並註釋掉其getter和setter方法
package cn.smbms.pojo;
import java.util.Date;
public class User {
private Integer id; //id
private String userCode; //使用者編碼
private String userName; //使用者名稱稱
private String userPassword; //使用者密碼
private Integer gender; //性別
private Date birthday; //出生日期
private String phone; //電話
private String address; //地址
private Integer userRole; //使用者角色ID
private Integer createdBy; //建立者
private Date creationDate; //建立時間
private Integer modifyBy; //更新者
private Date modifyDate; //更新時間
private Integer age;//年齡
//private String userRoleName; //使用者角色名稱
//association
private Role role; //使用者角色
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public Integer getAge() {
/*long time = System.currentTimeMillis()-birthday.getTime();
Integer age = Long.valueOf(time/365/24/60/60/1000).IntegerValue();*/
Date date = new Date();
Integer age = date.getYear()-birthday.getYear();
return age;
}
/* public String getUserRoleName() {
return userRoleName;
}
public void setUserRoleName(String userRoleName) {
this.userRoleName = userRoleName;
}*/
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserCode() {
return userCode;
}
public void setUserCode(String userCode) {
this.userCode = userCode;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPassword() {
return userPassword;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getUserRole() {
return userRole;
}
public void setUserRole(Integer userRole) {
this.userRole = userRole;
}
public Integer getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Integer createdBy) {
this.createdBy = createdBy;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public Integer getModifyBy() {
return modifyBy;
}
public void setModifyBy(Integer modifyBy) {
this.modifyBy = modifyBy;
}
public Date getModifyDate() {
return modifyDate;
}
public void setModifyDate(Date modifyDate) {
this.modifyDate = modifyDate;
}
}
通過以上改造,我們的JavaBean:User物件內部巢狀了一個複雜資料型別的屬性:role。接下來在UserMapper介面裡增加根據角色id獲取使用者列表的方法,
public List<User> getUserListByRoleId(@Param("userRole")Integer roleId);
修改對應UserMapper.xml,增加getUserListByRoleId,該select查詢語句返回型別為resultMap,外部引用的resultMap的型別為User。由於User物件內嵌JavaBean物件(Role),因此需要使用association來實現結果對映。
<!-- 根據roleId獲取使用者列表 association start-->
<resultMap type="User" id="userRoleResult">
<id property="id" column="id"/>
<result property="userCode" column="userCode" />
<result property="userName" column="userName" />
<result property="userRole" column="userRole" />
<association property="role" javaType="Role" >
<id property="id" column="r_id"/>
<result property="roleCode" column="roleCode"/>
<result property="roleName" column="roleName"/>
</association>
</resultMap>
<select id="getUserListByRoleId" parameterType="Integer" resultMap="userRoleResult">
select u.*,r.id as r_id,r.roleCode,r.roleName from smbms_user u,smbms_role r
where u.userRole = #{userRole} and u.userRole = r.id
</select>
從上述程式碼來簡單分析association的屬性。
➢ javaType:完整Java類名或者別名。若對映到一個JavaBean,則MyBatis會自行檢測到其型別;若對映到一個HashMap,則應該明確指定javaType,來確保所需行為。此處為Role。
➢ property:對映資料庫列的實體物件的屬性。此處為在User裡定義的屬性:role。association的子元素如下。
➢ result
♦ property:對映資料庫列的實體物件的屬性。此處為Role的屬性。
♦ column:資料庫列名或別名。在做結果對映的過程中,要確保所有的列名都是唯一且無歧義的。
注意
id子元素在巢狀結果對映中扮演了非常重要的角色,應該指定一個或者多個屬性來唯一標識這個結果集。實際上,即便沒有指定id, MyBatis也會工作,但是會導致嚴重的效能開銷,所以選擇儘量少的屬性來唯一標識結果,使用主鍵或者聯合主鍵均可。
最後修改測試類UserMapperTest.java,增加測試方法
@Test
public void getUserListByRoleIdTest(){
SqlSession sqlSession = null;
List<User> userList = new ArrayList<User>();
Integer roleId = 3;
try {
sqlSession = MyBatisUtil.createSqlSession();
userList = sqlSession.getMapper(UserMapper.class).getUserListByRoleId(roleId);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
logger.debug("getUserListByRoleIdTest userList.size : " + userList.size());
for(User user:userList){
logger.debug("userList =====> userName: " + user.getUserName() + ", Role: "
+ user.getRole().getId() + " --- " + user.getRole().getRoleCode()
+" --- " + user.getRole().getRoleName());
}
}
在測試方法中呼叫getUserListByRoleId()方法獲取userList,並進行結果輸出,關鍵是對映的使用者角色相關資訊。
通過上面的示例,我們瞭解了association的基本用法以及適用場景,現在再思考一個問題:上一個例子中使用“userRoleResult”聯合一個association的結果對映來載入User例項,那麼association的role結果對映是否可複用?
答案是肯定的,association提供了另一個屬性:resultMap。通過這個屬性可以擴充套件一個resultMap來進行聯合對映,這樣就可以使role結果對映重複使用。當然,若不需要複用,也可按照之前的寫法,直接巢狀這個聯合結果對映,如何使用要根據具體業務而定。下面就來改造剛才的示例,使用resultMap完成association的role對映結果的複用。
修改UserMapper.xml,增加resultMap來完成role的結果對映,association增加屬性resultMap來引用外部的“roleResult”
<!-- 根據roleId獲取使用者列表 association start-->
<resultMap type="User" id="userRoleResult">
<id property="id" column="id"/>
<result property="userCode" column="userCode" />
<result property="userName" column="userName" />
<result property="userRole" column="userRole" />
<association property="role" javaType="Role" resultMap="roleResult"/>
</resultMap>
<resultMap type="Role" id="roleResult">
<id property="id" column="r_id"/>
<result property="roleCode" column="roleCode"/>
<result property="roleName" column="roleName"/>
</resultMap>
<select id="getUserListByRoleId" parameterType="Integer" resultMap="userRoleResult">
select u.*,r.id as r_id,r.roleCode,r.roleName from smbms_user u,smbms_role r
where u.userRole = #{userRole} and u.userRole = r.id
</select>
在上述程式碼中,把之前的角色結果對映程式碼抽取出來放在一個resultMap中,然後設定了association的resultMap屬性來引用外部的“roleResult”。這樣做就可以達到複用的效果,並且整體的結構較為清晰明瞭,特別適合association的結果對映比較多的情況。
association用於處理一對一的關聯關係,對於一對多的關聯關係的處理,則需要collection元素來實現。
2.3.3 使用collection處理一對多關聯關係
collection元素的作用和association元素的作用差不多。事實上,它們非常類似,collection也是對映到JavaBean的某個“複雜型別”屬性,只不過這個屬性是一個集合列表,即JavaBean內部巢狀一個複雜資料型別(集合)屬性。和association元素一樣,我們使用巢狀查詢,或者從連線中巢狀結果集。
下面通過一個示例來演示collection的具體應用,示例需求:獲取指定使用者的相關資訊和地址列表。
首先需要建立POJO:Address.java,根據資料庫表(smbms_address)設計相應的屬性,並增加getter和setter方法
package cn.smbms.pojo;
import java.util.Date;
import java.util.List;
public class User {
private Integer id; //id
private String userCode; //使用者編碼
private String userName; //使用者名稱稱
private String userPassword; //使用者密碼
private Integer gender; //性別
private Date birthday; //出生日期
private String phone; //電話
private String address; //地址
private Integer userRole; //使用者角色ID
private Integer createdBy; //建立者
private Date creationDate; //建立時間
private Integer modifyBy; //更新者
private Date modifyDate; //更新時間
private Integer age;//年齡
//private String userRoleName; //使用者角色名稱
//association
private Role role; //使用者角色
//然後修改User類,增加地址列表屬性(List<Address>addressList),並增加相應的getter和setter方法,
//collection
private List<Address> addressList;//使用者地址列表
public List<Address> getAddressList() {
return addressList;
}
public void setAddressList(List<Address> addressList) {
this.addressList = addressList;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public Integer getAge() {
/*long time = System.currentTimeMillis()-birthday.getTime();
Integer age = Long.valueOf(time/365/24/60/60/1000).IntegerValue();*/
Date date = new Date();
Integer age = date.getYear()-birthday.getYear();
return age;
}
/* public String getUserRoleName() {
return userRoleName;
}
public void setUserRoleName(String userRoleName) {
this.userRoleName = userRoleName;
}*/
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserCode() {
return userCode;
}
public void setUserCode(String userCode) {
this.userCode = userCode;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserPassword() {
return userPassword;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getUserRole() {
return userRole;
}
public void setUserRole(Integer userRole) {
this.userRole = userRole;
}
public Integer getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Integer createdBy) {
this.createdBy = createdBy;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public Integer getModifyBy() {
return modifyBy;
}
public void setModifyBy(Integer modifyBy) {
this.modifyBy = modifyBy;
}
public Date getModifyDate() {
return modifyDate;
}
public void setModifyDate(Date modifyDate) {
this.modifyDate = modifyDate;
}
}
我們的JavaBean:User物件內部巢狀了一個複雜資料型別的屬性addressList。接下來在UserMapper介面中增加根據使用者id獲取使用者資訊以及地址列表的方法
public List<User> getAddressListByUserId(@Param("id")Integer userId);
對應修改UserMapper.xml,增加getAddressListByUserId,該select查詢語句返回型別為resultMap,並且引用外部的resultMap型別為User。由於User物件內嵌集合物件(addressList),因此需要使用collection來實現結果對映。
<!-- 獲取指定使用者的地址列表(user表-address表:1對多關係) collection start-->
<resultMap type="User" id="userAddressResult">
<id property="id" column="id"/>
<result property="userCode" column="userCode"/>
<result property="userName" column="userName"/>
//可以理解為:一個名為addressList、元素型別為Address的ArrayList集合。
<collection property="addressList" ofType="Address">
<id property="id" column="a_id"/>
<result property="postCode" column="postCode"/>
<result property="tel" column="tel"/>
<result property="contact" column="contact"/>
<result property="addressDesc" column="addressDesc"/>
</collection>
</resultMap>
<select id="getAddressListByUserId" parameterType="Integer" resultMap="userAddressResult">
select u.*,a.id as a_id,a.contact,a.addressDesc,a.postCode,a.tel
from smbms_user u,smbms_address a where u.id = a.userId and u.id=#{id}
</select>
簡單分析collection的屬性。
➢ ofType:完整Java類名或者別名,即集合所包含的型別。此處為Address。
➢ property:對映資料庫列的實體物件的屬性。此處為User裡定義的屬性:addressList。
最後修改測試類UserMapperTest.java,增加測試方法
@Test
public void getAddressListByUserIdTest(){
SqlSession sqlSession = null;
List<User> userList = new ArrayList<User>();
Integer userId = 1;
try {
sqlSession = MyBatisUtil.createSqlSession();
userList = sqlSession.getMapper(UserMapper.class).getAddressListByUserId(userId);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
for(User user:userList){
logger.debug("userList(include:addresslist) =====> userCode: " + user.getUserCode() + ", userName: " + user.getUserName());
for(Address address : user.getAddressList()){
logger.debug("address ----> id: " + address.getId() + ", contact: " + address.getContact()
+ ", addressDesc: " + address.getAddressDesc() + ", tel: " + address.getTel()
+ ", postCode: " + address.getPostCode());
}
}
}
在測試方法中呼叫getAddressListByUserId()方法獲取userList,並進行結果輸出,關鍵是對映的使用者地址列表的相關資訊,需要進一步迴圈addressList輸出。當然,通過之前學習association,我們就可以想到例子中的collection結果對映可以複用。提取相應程式碼到一個resultMap中,給collection增加resultMap屬性進行外部引用
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-hUNTR4j6-1609551381902)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201216213129163.png)]
任務4 配置resultMap自動對映級別和MyBatis快取
關鍵步驟如下。
➢ 配置resultMap的自動對映級別。
➢ 瞭解MyBatis快取的配置方法。
2.4.1 resultMap自動對映級別
<resultMap type="User" id="userAddressResult">
<id property="id" column="id"/>
<result property="userCode" column="userCode"/>
<result property="userName" column="userName"/>
<collection property="addressList" ofType="Address" resultMap="addressResult"/>
</resultMap>
<resultMap type="Address" id="addressResult">
<id property="id" column="a_id"/>
<result property="postCode" column="postCode"/>
<result property="tel" column="tel"/>
<result property="contact" column="contact"/>
<result property="addressDesc" column="addressDesc"/>
</resultMap>
<select id="getAddressListByUserId" parameterType="Integer" resultMap="userAddressResult">
select u.*,a.id as a_id,a.contact,a.addressDesc,a.postCode,a.tel
from smbms_user u LEFT JOIN smbms_address a ON u.id = a.userId where u.id=#{id}
</select>
User類中的userPassword屬性和Address類中的userId屬性均未在resultMap中進行匹配對映。現修改測試方法getAddressListByUserIdTest的程式碼,輸出未做匹配對映的屬性值
@Test
public void getAddressListByUserIdTest(){
SqlSession sqlSession = null;
User user = null;
Integer userId = 1;
try {
sqlSession = MyBatisUtil.createSqlSession();
user = sqlSession.getMapper(UserMapper.class).getAddressListByUserId(userId);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally{
MyBatisUtil.closeSqlSession(sqlSession);
}
if(null != user){
logger.debug("userList(include:addresslist) =====> userCode: " + user.getUserCode() + ", userName: " + user.getUserName());
if(user.getAddressList().size() > 0){
for(Address address : user.getAddressList()){
logger.debug("address ----> id: " + address.getId() + ", contact: " + address.getContact()
+ ", addressDesc: " + address.getAddressDesc() + ", tel: " + address.getTel()
+ ", postCode: " + address.getPostCode());
}
}else{
logger.debug("該使用者下無地址列表!");
}
}else{
logger.debug("查無此使用者!");
}
}
觀察輸出結果,發現當沒有設定autoMappingBehavior的時候,也就是預設情況下(PARTIAL),若是普通資料型別的屬性,會自動匹配,這在示例10中已經提到過。但若是有內部巢狀(association或者collection),那麼輸出結果就是null(如圖中的userPassword和userId),也就是說它不會自動匹配,除非手工設定autoMappingBehavior的value為FULL(自動匹配所有)。修改mybatis-config.xml程式碼如下:
<settings>
<!-- 配置mybatis的log實現為LOG4J -->
<setting name="logImpl" value="LOG4J" />
<!-- 設定resultMap的自動對映級別為NONE(禁止自動匹配) -->
<!-- <setting name="autoMappingBehavior" value="NONE" /> -->
</settings>
觀察輸出結果,發現autoMappingBehavior的value設定為FULL(自動匹配所有屬性)之後,未作對映的欄位userPassword和userId均有值輸出。
可以認識到MyBatis對resultMap自動對映的三個匹配級別:
➢ NONE:禁止自動匹配。
➢ PARTIAL(預設):自動匹配所有屬性,有內部巢狀(association、collection)的除外。
➢ FULL:自動匹配所有屬性。
2.4.2 MyBatis快取
1.一級快取一級快取是基於PerpetualCache(MyBatis自帶)的HashMap本地快取,作用範圍為session域內,當session flush或者close之後,該session中所有的cache就會被清空。
2.二級快取二級快取就是global caching,它超出session範圍之外,可以被所有SqlSession共享,開啟它只需要在MyBatis的核心配置檔案(mybatis-config.xml)的settings中設定即可。
一級快取快取的是SQL語句,二級快取快取的是結果物件。
3.二級快取的配置
(1)MyBatis的全域性cache配置,需要在mybatis-config.xml的settings中設定
<settings>
<setting name="cacheEnable" value="true"/>
<!-- 配置mybatis的log實現為LOG4J -->
<setting name="logImpl" value="LOG4J" />
<!-- 設定resultMap的自動對映級別為NONE(禁止自動匹配) -->
<!-- <setting name="autoMappingBehavior" value="NONE" /> -->
</settings>
(2)在mapper檔案(如UserMapper.xml)中設定快取,預設情況下是未開啟快取的。需要注意的是,global caching的作用域是針對mapper的namespace而言的,即只有在此namespace內(cn.smbms.dao.user.UserMapper)的查詢才能共享這個cache,程式碼如下:
<mapper namespace="cn.smbms.dao.user.UserMapper">
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>
(3)在mapper檔案配置支援cache後,如果需要對個別查詢進行調整,可以單獨設定cache,程式碼如下:
<select id="getAddressListByUserId" parameterType="Integer" resultMap="userAddressResult" useCache="true">
select u.*,a.id as a_id,a.contact,a.addressDesc,a.postCode,a.tel
from smbms_user u LEFT JOIN smbms_address a ON u.id = a.userId where u.id=#{id}
</select>
本章總結
對於MyBatis快取的內容僅做了解即可,因為面對一定規模的資料量,內建的Cache方式就派不上用場了;況且對查詢結果集做快取並不是MyBatis框架擅長的,它擅長做的應該是SQL對映。所以採用OSCache、Memcached等專門的快取伺服器來處理快取更為合理。
➢ MyBatis的SQL對映檔案提供select、insert、update、delete等元素來實現SQL語句的對映。
➢ SQL對映檔案的根節點是mapper元素,需要指定namespace來區別於其他的mapper,保證全域性唯一;並且其名稱必須跟介面同名,作用是繫結DAO介面,即面向介面程式設計。
➢ SQL對映檔案的select返回結果型別的對映可以使用resultMap或resultType,但不能同時使用。
➢ MyBatis的SQL語句引數入參,對於基礎資料型別的引數資料,使用@Param註解實現引數入參;對於複雜資料型別的引數資料,直接入參即可。
➢ resultMap的association和collection可以實現高階結果對映。
相關文章
- MyBatis系列之對映檔案之sql、Parameters、cache和cache-refMyBatisSQL
- MyBatis學習 之 二、SQL語句對映檔案(1)resultMapMyBatisSQL
- MyBaits | 對映檔案之引數處理AI
- MyBatis系列之對映檔案中的ResultMapsMyBatis
- MyBatis框架之SQL對映和動態SQLMyBatis框架SQL
- Mybatis對映檔案簡介MyBatis
- PostgreSQL 物理檔案對映解析SQL
- 載入Mapper對映檔案APP
- MyBatis學習 之 二、SQL語句對映檔案(2)增刪改查、引數、快取MyBatisSQL快取
- nginx容器卷對映檔案不生效Nginx
- 使用記憶體對映檔案(mmap)記憶體
- mybatis原始碼配置檔案解析之五:解析mappers標籤(解析XML對映檔案)MyBatis原始碼APPXML
- 溫故之.NET程式間通訊——記憶體對映檔案記憶體
- Hibernate對映檔案一對多關係薦
- 裸裝置表空間對映檔案
- MyBatis3:SQL對映MyBatisS3SQL
- 記憶體對映檔案詳解-----C++實現(即一塊記憶體和一個檔案相對映對應)記憶體C++
- Ubuntu下修改hosts檔案中的域名對映Ubuntu
- Hibernate配置檔案中對映元素詳解
- VC++中用記憶體對映檔案 (轉)C++記憶體
- 二進位制檔案記憶體對映記憶體
- windows核心程式設計--記憶體對映檔案Windows程式設計記憶體
- Python mmap的使用-檔案記憶體對映Python記憶體
- 居然這就是C++記憶體對映檔案?!C++記憶體
- Android入門教程 | mmap 檔案對映介紹Android
- Middlegen生成Hibernate對映檔案時出錯
- TypeScript 之對映型別TypeScript型別
- 非同步日誌 vs. 記憶體對映檔案非同步記憶體
- 思考工具之概念對映 | Untools
- Java Web之tomcat修改80埠和專案對映JavaWebTomcat
- Mybatis 裡對映檔案的動態 SQL 語句,實現if,where,foreache的SQL語句動態拼接查詢MyBatisSQL
- Mybatis配置檔案resultMap對映啥時候可寫可不寫?MyBatis
- 一個記憶體檔案對映使用者類 (轉)記憶體
- Mybatis的Mapper對映檔案中常用標籤及作用MyBatisAPP
- Mybatis基礎:Mybatis對映配置檔案,Mybatis核心配置檔案,Mybatis傳統方式開發MyBatis
- Three.js進階篇之9 - 紋理對映和UV對映JS
- 第10章 對檔案的輸入輸出
- DM7使用DMRMAN對資料庫執行指定對映檔案還原資料庫