resultMap算是mybatis映射器中最復雜的一個節點了,能夠配置的屬性較多,我們在mybatis映射器配置細則這篇博客中已經簡單介紹過resultMap的配置了,當時我們介紹了resultMap中的id和result節點,那么在resultMap中除了這兩個之外,還有其他節點,今天我們就來詳細說說resultMap中的這些節點。
如果小伙伴對mybatis尚不了解,建議先翻看博主前面幾篇博客了解一下,否則本文你可能難以理解,老司機請略過。
#概覽
先來看看resultMap中都有那些屬性:
<resultMap> <constructor> <idArg/> <arg/> </constructor> <id/> <result/> <association property=""/> <collection property=""/> <discriminator javaType=""> <case value=""></case> </discriminator> </resultMap>
我們看到,resultMap中一共有六種不同的節點,除了id和result我們在mybatis映射器配置細則這篇博客中已經介紹過了之外,還剩三種,剩下的四個本文我們就來一個一個看一下。
#constructor
constructor主要是用來配置構造方法,默認情況下mybatis會調用實體類的無參構造方法創建一個實體類,然后再給各個屬性賦值,但是有的時候我們可能為實體類生成了有參的構造方法,並且也沒有給該實體類生成無參的構造方法,這個時候如果不做特殊配置,resultMap在生成實體類的時候就會報錯,因為它沒有找到無參構造方法。這個時候mybatis會報如下錯誤:

那么解決方式很簡單,就是在constructor節點中進行簡單配置,假設我現在User實體類的構造方法如下:
public User(Long id, String username, String password, String address) { this.id = id; this.username = username; this.password = password; this.address = address; }
那么我在resultMap中配置constructor節點,如下:
<resultMap id="userResultMap" type="org.sang.bean.User"> <constructor> <idArg column="id" javaType="long"/> <arg column="username" javaType="string"/> <arg column="password" javaType="string"/> <arg column="address" javaType="string"/> </constructor> </resultMap>
在constructor中指定相應的參數,這樣resultMap在構造實體類的時候就會按照這里的指定的參數尋找相應的構造方法去完成了。
#association
association是mybatis支持級聯的一部分,我們知道在級聯中有一對一、一對多、多對多等關系,association主要是用來解決一對一關系的,假設我現在有兩張表,一張表示省份,一張表示省份的別名,假設一個省只有一個別名(實際上有的省份有兩個別名),我們來看一下如下兩張表:
1.省份表:

說明一下最后一個area字段表示該省是屬於南方還是北方。
2.別名表:

別名表中pid表示省份的id,假設我現在有一個實體類,Province,該類有兩個屬性,一個叫做name表示省份的名字,一個叫做alias表示省份的別名,那么我在查詢的時候可以通過association來實現這種一對一級聯,實現方式如下:
##創建Alias實體類
public class Alias { private Long id; private String name; //省略getter/setter }
##創建Province實體類
public class Province { private Long id; private String name; private Alias alias; //省略getter/setter }
##創建AliasMapper
public interface AliasMapper { Alias findAliasByPid(Long id); }
這里就提供一個方法,根據省份的id找到省份的別名。
##創建aliasMapper.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="org.sang.db.AliasMapper"> <select id="findAliasByPid" parameterType="long" resultType="org.sang.bean.Alias"> SELECT * FROM alias WHERE pid=#{id} </select> </mapper>
##創建ProvinceMapper
public interface ProvinceMapper { List<Province> getProvince(); }
##創建provinceMapper.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="org.sang.db.ProvinceMapper"> <resultMap id="provinceResultMapper" type="org.sang.bean.Province"> <id column="id" property="id"/> <association property="alias" column="id" select="org.sang.db.AliasMapper.findAliasByPid"/> </resultMap> <select id="getProvince" resultMap="provinceResultMapper"> SELECT * FROM province </select> </mapper>
小伙伴們注意這里的resultMap,我們在resultMap中指定了association節點,association節點中的select屬性表示要執行的方法,該方法實際上指向了一條SQL語句(就是我們在aliasMapper.xml中配置的那條SQL語句),column表示給方法傳入的參數的字段,我們這里要傳入省份的id,所以column為id,property表示select查詢的結果要賦值給誰,我們這里當然是賦值給Province的alias屬性。
##在mybatis-conf.xml中配置mapper
<mappers> <mapper resource="provinceMapper.xml"/> <mapper resource="aliasMapper.xml"/> </mappers>
##測試
@Test public void test7() { SqlSession sqlSession = null; try { sqlSession = DBUtils.openSqlSession(); ProvinceMapper pm = sqlSession.getMapper(ProvinceMapper.class); List<Province> list = pm.getProvince(); for (Province province : list) { System.out.println(province); } sqlSession.commit(); } catch (Exception e) { e.printStackTrace(); sqlSession.rollback(); } finally { if (sqlSession != null) { sqlSession.close(); } } }
測試結果:

OK,這就是簡單的一對一級聯的使用。
#collection
collection是用來解決一對多級聯的,還是上面那個例子,每個省份下面都會有很多城市,於是,我來創建一張城市表,如下:

城市表中有一個pid字段,該字段表示這個城市是屬於哪個省份的,OK,假設我現在Province實體類中多了一個屬性叫做cities,這個cities屬性的數據類型是一個List集合,這個集合中放的所有的數據就是這個省份的,我希望查詢結束之后這個屬性的值就會被自動填充,OK,那么在上面那個案例的基礎上,我們來看看這個要怎么實現。
##為Province類添加屬性
新的Province類變成下面這個樣子:
public class Province { private Long id; private String name; private Alias alias; private List<City> cities; //省略getter/setter }
##創建City實體類
public class City { private Long id; private Long pid; private String name; //省略getter/setter }
##創建CityMapper
public interface CityMapper { List<City> findCityByPid(Long id); }
CityMapper中就提供一個方法,那就是根據省份的id來查找到相應的城市。
##創建cityMapper.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="org.sang.db.CityMapper"> <select id="findCityByPid" parameterType="long" resultType="org.sang.bean.City"> SELECT * FROM city WHERE pid=#{id} </select> </mapper>
##在mybatis-conf.xml中配置mapper
<mappers> <mapper resource="provinceMapper.xml"/> <mapper resource="aliasMapper.xml"/> <mapper resource="cityMapper.xml"/> </mappers>
##修改provinceMapper.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="org.sang.db.ProvinceMapper"> <resultMap id="provinceResultMapper" type="org.sang.bean.Province"> <id column="id" property="id"/> <association property="alias" column="id" select="org.sang.db.AliasMapper.findAliasByPid"/> <collection property="cities" column="id" select="org.sang.db.CityMapper.findCityByPid"/> </resultMap> <select id="getProvince" resultMap="provinceResultMapper"> SELECT * FROM province </select> </mapper>
多了一個collection節點,節點中屬性的含義和association都是一樣的,我這里不再贅述。OK,如此之后,我們就可以來測試了。
##測試
測試代碼(和上面association的測試代碼是一樣的):
@Test public void test7() { SqlSession sqlSession = null; try { sqlSession = DBUtils.openSqlSession(); ProvinceMapper pm = sqlSession.getMapper(ProvinceMapper.class); List<Province> list = pm.getProvince(); for (Province province : list) { System.out.println(province); } sqlSession.commit(); } catch (Exception e) { e.printStackTrace(); sqlSession.rollback(); } finally { if (sqlSession != null) { sqlSession.close(); } } }
測試結果:

OK,Province的cities屬性已經被順利賦上值了。
#discriminator
discriminator既不是一對多也不是一對一,這個我們稱之為鑒別器級聯,使用它我們可以在不同的條件下執行不同的查詢匹配不同的實體類,還是以上文的例子為例,不同的省份分別屬於南北方,南北方的人有不同的飲食習慣,北方人吃面、南方人吃米飯,據此,我來新創建三個類,分別是Food、Rice、Noodle三個類,其中Food是Rice和Noodle的父類,將兩者之間的一些共性抽取出來,這三個類如下:
public class Food { protected Long id; protected String name; //省略getter/setter } public class Noodle extends Food{ //每天吃幾次 private int price; //省略getter/setter } public class Rice extends Food { //烹飪方法 private String way; //省略getter/setter }
然后我在數據庫中再分別創建兩張表,分別是rice表和noodle表,如下:

然后我現在現在再修改我的Province實體類,如下:
public class Province { private Long id; private String name; private Alias alias; private List<City> cities; private List<Food> foods; //省略getter/setter }
這次多了一個foods屬性,這個屬性是這樣,當我在數據庫中查詢的時候,如果查到這個省份是北方省份,那么就自動去查詢noodle表,將查到的結果賦值給foods屬性,如果這個省份是南方省份,那么就自動去查詢rice表,將查到的結果賦值給foods屬性,這種要根據查詢結果動態匹配查詢語句的需求,我們就可以通過discriminator來實現。OK,接下來我們為Rice和Noodle分別創建Mapper,並在mybatis-conf.xml中注冊mapper,結果如下:
public interface RiceMapper { List<Rice> findRiceByArea(); } public interface NoodleMapper { List<Noodle> findNoodleByArea(); }
noodleMapper.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="org.sang.db.NoodleMapper"> <select id="findNoodleByArea" resultType="org.sang.bean.Noodle"> SELECT * FROM noodle </select> </mapper>
riceMapper.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="org.sang.db.RiceMapper"> <select id="findRiceByArea" resultType="org.sang.bean.Rice"> SELECT * FROM rice </select> </mapper>
mybaits-conf.xml
<mappers> <mapper resource="provinceMapper.xml"/> <mapper resource="aliasMapper.xml"/> <mapper resource="cityMapper.xml"/> <mapper resource="riceMapper.xml"/> <mapper resource="noodleMapper.xml"/> </mappers>
OK ,做完這些之后接下來我們就可以來稍微的完善下provinceMapper.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="org.sang.db.ProvinceMapper"> <resultMap id="provinceResultMapper" type="org.sang.bean.Province"> <id column="id" property="id"/> <association property="alias" column="id" select="org.sang.db.AliasMapper.findAliasByPid"/> <collection property="cities" column="id" select="org.sang.db.CityMapper.findCityByPid"/> <discriminator javaType="int" column="area"> <case value="1" resultMap="noodleResultMap"></case> <case value="2" resultMap="riceResultMap"></case> </discriminator> </resultMap> <resultMap id="noodleResultMap" type="org.sang.bean.Province" extends="provinceResultMapper"> <collection property="foods" column="area" select="org.sang.db.NoodleMapper.findNoodleByArea"/> </resultMap> <resultMap id="riceResultMap" type="org.sang.bean.Province" extends="provinceResultMapper"> <collection property="foods" column="area" select="org.sang.db.RiceMapper.findRiceByArea"/> </resultMap> <select id="getProvince" resultMap="provinceResultMapper"> SELECT * FROM province </select> </mapper>
小伙伴們注意,我們在這里添加了discriminator節點,該節點有點類似於switch語句,column表示用哪個值參與比較,我們這里使用area字段進行比較,當area為0時(即北方省份)我們使用的resultMap為noodleResultMap,當area為1時(即南方省份)我們使用的resultMap為riceResultMap,然后我們在下面再分別定義riceResultMap和noodleResultMap,但是注意這兩個里邊返回值類型都是Province,也都是繼承自provinceResultMapper,這里的繼承和我們Java中面向對象的繼承差不多,父類有的子類繼承之后也都自動具備了。這樣做時候,我們再來運行剛才的測試代碼,結果如下:

和我們想的基本一致。
#延遲加載問題
按照上文我們介紹的方式,每次查詢省份的時候都會去查詢別名食物等表,有的時候我們可能並不需要這些數據但是卻無可避免的要調用這個方法,那么在mybatis中,針對這個問題也提出了相應的解決方案,那就是延遲加載,延遲加載就是當我需要調用這條數據的時候mybatis再去數據庫中查詢這條數據,比如Province的foods屬性,當我調用Province的getFoods()方法來獲取這條數據的時候系統再去執行相應的查詢操作。OK,針對這個需求mybatis給我們提供了兩種不同的方式,一種是在mybatis的配置文件中進行配置,還有一種是針對不同的查詢進行單獨配置,我們接下來就來看一下這兩種不同的配置方式。
##在mybatis的配置文件中進行配置
這種配置有點類似於全局配置,配置成功之后,所有的查詢操作都開啟了延遲加載。配置方式如下:
<settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>
OK,直接在mybatis-conf.xml中添加如上配置即可。那么這里涉及到兩個屬性,含義不同,我們來分別看一下:
1.lazyLoadingEnabled表示是否開啟延遲加載,默認為false表示沒有開啟,true表示開啟延遲加載。
2.aggressiveLazyLoading表示延遲加載的時候內容是按照層級來延遲加載還是按照需求來延遲加載,默認為true表示按照層級來延遲加載,false表示按照需求來延遲加載。以我們上文查詢食物的需求為例,去查詢rice表或者noodle表是屬於同一級的,但是在我查詢到陝西省的時候,這個時候只需要去查詢noodle表就可以了,當我查詢到廣東省的時候再去查詢rice表,但是如果aggressiveLazyLoading為true的話,即使我只查詢到陝西省,系統也會去把rice和noodle都查一遍,因為它倆屬於同一級,而如果aggressiveLazyLoading為false的話,那么當我查詢到陝西省的時候,系統就只查詢noodle表,當我查詢到廣東省的時候系統才去查詢rice表。
OK,這樣配置之后,我們再來看看查詢日志:

和我們想的一致。
##在針對不同的查詢進行配置
OK,上面這種配置算是一種全局配置,如果我們想針對某一條查詢開啟延遲加載該怎么做呢?比如針對省份別名的查詢我想即時加載,而針對城市的查詢我想延遲加載該怎么辦呢?很簡單,在association和collection中配置fetchType屬性就可以啦。如上需求,如下配置:
<?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="org.sang.db.ProvinceMapper"> <resultMap id="provinceResultMapper" type="org.sang.bean.Province"> <id column="id" property="id"/> <association property="alias" column="id" select="org.sang.db.AliasMapper.findAliasByPid" fetchType="eager"/> <collection property="cities" column="id" select="org.sang.db.CityMapper.findCityByPid" fetchType="lazy"/> <discriminator javaType="int" column="area"> <case value="1" resultMap="noodleResultMap"></case> <case value="2" resultMap="riceResultMap"></case> </discriminator> </resultMap> <resultMap id="noodleResultMap" type="org.sang.bean.Province" extends="provinceResultMapper"> <collection property="foods" column="area" select="org.sang.db.NoodleMapper.findNoodleByArea"/> </resultMap> <resultMap id="riceResultMap" type="org.sang.bean.Province" extends="provinceResultMapper"> <collection property="foods" column="area" select="org.sang.db.RiceMapper.findRiceByArea"/> </resultMap> <select id="getProvince" resultMap="provinceResultMapper"> SELECT * FROM province </select> </mapper>
eager表示即時加載,lazy表示延遲加載。OK,做了如上配置之后,我們再來看看查詢日志:

和我們想的基本一致。
#小插曲
關於resultMap我們就說上面那么多。最后我們再來稍微說一下mapper中的sql元素吧。sql元素有點像變量的定義,如果一個表的字段特別多,我們總是寫select XXX,XXX,XXX from X總是很麻煩,我們可能希望將一些通用的東西提取成變量然后單獨引用,那么這個提取方式也很簡單,那就是sql變量,如下:
<sql id="selectAll"> SELECT * FROM user </sql>
這里是將整個查詢語句封裝,然后在select中引用即可,如下:
<select id="getUser2" resultType="user"> <include refid="selectAll"/> </select>
也可以只封裝一部分查詢語句,如下:
<sql id="selectAll3"> id,username,address,password </sql>
引用方式如下:
<select id="getUser3" resultType="user"> SELECT <include refid="selectAll3"/> FROM user </select>
還可以在封裝的時候使用一些變量,如下:
<sql id="selectAll4"> ${prefix}.id,${prefix}.username,${prefix}.address </sql>
注意變量引用方式是$符號哦,不是#,引用方式如下:
<select id="getUser4" resultType="user" parameterType="string"> SELECT <include refid="selectAll4"> <property name="prefix" value="u"/> </include> FROM user u </select>
在property中設置prefix的值。
OK,以上。
本文案例下載:
本文案例GitHub地址https://github.com/lenve/JavaEETest/tree/master/Test27-mybatis7。
參考資料:
《深入淺出MyBatis 技術原理與實戰》第四章
