1. 程式人生 > >mybatis關聯查詢問題(一對多、多對一)

mybatis關聯查詢問題(一對多、多對一)

mybatis 提供了高階的關聯查詢功能,可以很方便地將資料庫獲取的結果集對映到定義的Java Bean 中。下面通過一個例項,來展示一下Mybatis對於常見的一對多和多對一關係複雜對映是怎樣處理的。

設計一個簡單的部落格系統,一個使用者可以開多個部落格,在部落格中可以發表文章,允許發表評論,可以為文章加標籤。部落格系統主要有以下幾張表構成:

Author表作者資訊表,記錄作者的資訊,使用者名稱和密碼,郵箱等。

Blog表     部落格表,一個作者可以開多個部落格,即Author和Blog的關係是一對多。

Post表   文章記錄表,記錄文章發表時間,標題,正文等資訊;一個部落格下可以有很多篇文章,Blog 和Post的關係是一對多。

Comments表文章評論表,記錄文章的評論,一篇文章可以有很多個評論:Post和Comments的對應關係是一對多。

Tag表:標籤表,表示文章的標籤分類,一篇文章可以有多個標籤,而一個標籤可以應用到不同的文章上,所以Tag和Post的關係是多對多的關係;(Tag和Post的多對多關係通過Post_Tag表體現)

Post_Tag表: 記錄 文章和標籤的對應關係。

一般情況下,我們會根據每一張表的結構 建立與此相對應的JavaBean(或者Pojo),來完成對錶的基本CRUD操作。

上述對單個表的JavaBean定義有時候不能滿足業務上的需求。在業務上,一個Blog物件應該有其作者的資訊和一個文章列表,如下圖所示:


如果想得到這樣的類的例項,則最起碼要有一下幾步:

1. 通過Blog 的id 到Blog表裡查詢Blog資訊,將查詢到的blogId 和title 賦到Blog物件內;

2. 根據查詢到到blog資訊中的authorId 去 Author表獲取對應的author資訊,獲取Author物件,然後賦到Blog物件內;

3. 根據 blogId 去 Post表裡查詢 對應的 Post文章列表,將List<Post>物件賦到Blog物件中;

這樣的話,在底層最起碼呼叫三次查詢語句,請看下列的程式碼:

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. /* 
  2.  * 通過blogId獲取BlogInfo物件
     
  3.  */
  4. publicstatic BlogInfo ordinaryQueryOnTest(String blogId)  
  5. {  
  6.     BigDecimal id = new BigDecimal(blogId);  
  7.     SqlSession session = sqlSessionFactory.openSession();  
  8.     BlogInfo blogInfo = new BlogInfo();  
  9.     //1.根據blogid 查詢Blog物件,將值設定到blogInfo中
  10.     Blog blog = (Blog)session.selectOne("com.foo.bean.BlogMapper.selectByPrimaryKey",id);  
  11.     blogInfo.setBlogId(blog.getBlogId());  
  12.     blogInfo.setTitle(blog.getTitle());  
  13.     //2.根據Blog中的authorId,進入資料庫查詢Author資訊,將結果設定到blogInfo物件中
  14.     Author author = (Author)session.selectOne("com.foo.bean.AuthorMapper.selectByPrimaryKey",blog.getAuthorId());  
  15.     blogInfo.setAuthor(author);  
  16.     //3.查詢posts物件,設定進blogInfo中
  17.     List posts = session.selectList("com.foo.bean.PostMapper.selectByBlogId",blog.getBlogId());  
  18.     blogInfo.setPosts(posts);  
  19.     //以JSON字串的形式將物件打印出來
  20.     JSONObject object = new JSONObject(blogInfo);  
  21.     System.out.println(object.toString());  
  22.     return blogInfo;  
  23. }  
save_snippets.png
	/*
	 * 通過blogId獲取BlogInfo物件
	 */
	public static BlogInfo ordinaryQueryOnTest(String blogId)
	{
		BigDecimal id = new BigDecimal(blogId);
		SqlSession session = sqlSessionFactory.openSession();
		BlogInfo blogInfo = new BlogInfo();
		//1.根據blogid 查詢Blog物件,將值設定到blogInfo中
		Blog blog = (Blog)session.selectOne("com.foo.bean.BlogMapper.selectByPrimaryKey",id);
		blogInfo.setBlogId(blog.getBlogId());
		blogInfo.setTitle(blog.getTitle());
		
		//2.根據Blog中的authorId,進入資料庫查詢Author資訊,將結果設定到blogInfo物件中
		Author author = (Author)session.selectOne("com.foo.bean.AuthorMapper.selectByPrimaryKey",blog.getAuthorId());
		blogInfo.setAuthor(author);
		
		//3.查詢posts物件,設定進blogInfo中
		List posts = session.selectList("com.foo.bean.PostMapper.selectByBlogId",blog.getBlogId());
		blogInfo.setPosts(posts);
		//以JSON字串的形式將物件打印出來
		JSONObject object = new JSONObject(blogInfo);
		System.out.println(object.toString());
		return blogInfo;
	}
點選我下載原始碼

從上面的程式碼可以看出,想獲取一個BlogInfo物件比較麻煩,總共要呼叫三次資料庫查詢,得到需要的資訊,然後再組裝BlogInfo物件。

巢狀語句查詢

mybatis提供了一種機制,叫做巢狀語句查詢,可以大大簡化上述的操作,加入配置及程式碼如下:

[html] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. <resultMaptype="com.foo.bean.BlogInfo"id="BlogInfo">
  2.     <idcolumn="blog_id"property="blogId"/>
  3.     <resultcolumn="title"property="title"/>
  4.     <associationproperty="author"column="blog_author_id"
  5.         javaType="com.foo.bean.Author"select="com.foo.bean.AuthorMapper.selectByPrimaryKey">
  6.     </association>
  7.     <collectionproperty="posts"column="blog_id"ofType="com.foo.bean.Post"
  8.         select="com.foo.bean.PostMapper.selectByBlogId">
  9.     </collection>
  10. </resultMap>
  11. <selectid="queryBlogInfoById"resultMap="BlogInfo"parameterType="java.math.BigDecimal">
  12.     SELECT  
  13.     B.BLOG_ID,  
  14.     B.TITLE,  
  15.     B.AUTHOR_ID AS BLOG_AUTHOR_ID  
  16.     FROM LOULUAN.BLOG B  
  17.     where B.BLOG_ID = #{blogId,jdbcType=DECIMAL}  
  18. </select>
save_snippets.png
	<resultMap type="com.foo.bean.BlogInfo" id="BlogInfo">
		<id column="blog_id" property="blogId" />
		<result column="title" property="title" />
		<association property="author" column="blog_author_id"
			javaType="com.foo.bean.Author" select="com.foo.bean.AuthorMapper.selectByPrimaryKey">
		</association>
		<collection property="posts" column="blog_id" ofType="com.foo.bean.Post"
			select="com.foo.bean.PostMapper.selectByBlogId">
		</collection>
	</resultMap>

	<select id="queryBlogInfoById" resultMap="BlogInfo" parameterType="java.math.BigDecimal">
		SELECT
		B.BLOG_ID,
		B.TITLE,
		B.AUTHOR_ID AS BLOG_AUTHOR_ID
		FROM LOULUAN.BLOG B
		where B.BLOG_ID = #{blogId,jdbcType=DECIMAL}
	</select>
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. /* 
  2.  * 通過blogId獲取BlogInfo物件 
  3.  */
  4. publicstatic BlogInfo nestedQueryOnTest(String blogId)  
  5. {  
  6.     BigDecimal id = new BigDecimal(blogId);  
  7.     SqlSession session = sqlSessionFactory.openSession();  
  8.     BlogInfo blogInfo = new BlogInfo();  
  9.     blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id);  
  10.     JSONObject object = new JSONObject(blogInfo);  
  11.     System.out.println(object.toString());  
  12.     return blogInfo;  
  13. }  
save_snippets.png
	/*
	 * 通過blogId獲取BlogInfo物件
	 */
	public static BlogInfo nestedQueryOnTest(String blogId)
	{
		BigDecimal id = new BigDecimal(blogId);
		SqlSession session = sqlSessionFactory.openSession();
		BlogInfo blogInfo = new BlogInfo();
		blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id);
		JSONObject object = new JSONObject(blogInfo);
		System.out.println(object.toString());
		return blogInfo;
	}
通過上述的程式碼完全可以實現前面的那個查詢。這裡我們在程式碼裡只需要 blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id);一句即可獲取到複雜的blogInfo物件。

巢狀語句查詢的原理

在上面的程式碼中,Mybatis會執行以下流程:

1.先執行 queryBlogInfoById 對應的語句從Blog表裡獲取到ResultSet結果集;

2.取出ResultSet下一條有效記錄,然後根據resultMap定義的對映規格,通過這條記錄的資料來構建對應的一個BlogInfo 物件。

3. 當要對BlogInfo中的author屬性進行賦值的時候,發現有一個關聯的查詢,此時Mybatis會先執行這個select查詢語句,得到返回的結果,將結果設定到BlogInfo的author屬性上;

4. 對BlogInfo的posts進行賦值時,也有上述類似的過程。

5. 重複2步驟,直至ResultSet. next () == false;

以下是blogInfo物件構造賦值過程示意圖:


這種關聯的巢狀查詢,有一個非常好的作用就是:可以重用select語句,通過簡單的select語句之間的組合來構造複雜的物件。上面巢狀的兩個select語句com.foo.bean.AuthorMapper.selectByPrimaryKey和com.foo.bean.PostMapper.selectByBlogId完全可以獨立使用。

N+1問題

    它的弊端也比較明顯:即所謂的N+1問題。關聯的巢狀查詢顯示得到一個結果集,然後根據這個結果集的每一條記錄進行關聯查詢。

    現在假設巢狀查詢就一個(即resultMap 內部就一個association標籤),現查詢的結果集返回條數為N,那麼關聯查詢語句將會被執行N次,加上自身返回結果集查詢1次,共需要訪問資料庫N+1次。如果N比較大的話,這樣的資料庫訪問消耗是非常大的!所以使用這種巢狀語句查詢的使用者一定要考慮慎重考慮,確保N值不會很大。

     以上面的例子為例,select 語句本身會返回com.foo.bean.BlogMapper.queryBlogInfoById 條數為1 的結果集,由於它有兩條關聯的語句查詢,它需要共訪問資料庫 1*(1+1)=3次資料庫。

巢狀結果查詢

巢狀語句的查詢會導致資料庫訪問次數不定,進而有可能影響到效能。Mybatis還支援一種巢狀結果的查詢:即對於一對多,多對多,多對一的情況的查詢,Mybatis通過聯合查詢,將結果從資料庫內一次性查出來,然後根據其一對多,多對一,多對多的關係和ResultMap中的配置,進行結果的轉換,構建需要的物件。

重新定義BlogInfo的結果對映 resultMap

[html] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. <resultMaptype="com.foo.bean.BlogInfo"id="BlogInfo">
  2.     <idcolumn="blog_id"property="blogId"/>
  3.     <resultcolumn="title"property="title"/>
  4.     <associationproperty="author"column="blog_author_id"javaType="com.foo.bean.Author">
  5.         <idcolumn="author_id"property="authorId"/>
  6.         <resultcolumn="user_name"property="userName"/>
  7.         <resultcolumn="password"property="password"/>
  8.         <resultcolumn="email"property="email"/>
  9.         <resultcolumn="biography"property="biography"/>
  10.     </association>
  11.     <collectionproperty="posts"column="blog_post_id"ofType="com.foo.bean.Post">
  12.         <idcolumn="post_id"property="postId"/>
  13.         <resultcolumn="blog_id"property="blogId"/>
  14.         <resultcolumn="create_time"property="createTime"/>
  15.         <resultcolumn="subject"property="subject"/>
  16.         <resultcolumn="body"property="body"/>
  17.         <resultcolumn="draft"property="draft"/>
  18.     </collection>
  19. </resultMap>
save_snippets.png
  <resultMap type="com.foo.bean.BlogInfo" id="BlogInfo">
      <id column="blog_id" property="blogId"/>
      <result column="title" property="title"/>
      <association property="author" column="blog_author_id" javaType="com.foo.bean.Author">
          <id column="author_id" property="authorId"/>
          <result column="user_name" property="userName"/>
          <result column="password" property="password"/>
          <result column="email" property="email"/>
          <result column="biography" property="biography"/>
      </association>
      <collection property="posts" column="blog_post_id" ofType="com.foo.bean.Post">
          <id column="post_id" property="postId"/>
          <result column="blog_id" property="blogId"/>
          <result column="create_time" property="createTime"/>
          <result column="subject" property="subject"/>
          <result column="body" property="body"/>
          <result column="draft" property="draft"/>
      </collection>
      
  </resultMap>

對應的sql語句如下: [html] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. <selectid="queryAllBlogInfo"resultMap="BlogInfo">
  2.     SELECT   
  3.      B.BLOG_ID,  
  4.      B.TITLE,  
  5.      B.AUTHOR_ID AS BLOG_AUTHOR_ID,  
  6.      A.AUTHOR_ID,  
  7.      A.USER_NAME,  
  8.      A.PASSWORD,  
  9.      A.EMAIL,  
  10.      A.BIOGRAPHY,  
  11.      P.POST_ID,  
  12.      P.BLOG_ID   AS BLOG_POST_ID ,  
  13.   P.CREATE_TIME,  
  14.      P.SUBJECT,  
  15.      P.BODY,  
  16.      P.DRAFT  
  17. FROM BLOG B  
  18. LEFT OUTER JOIN AUTHOR A  
  19.   ON B.AUTHOR_ID = A.AUTHOR_ID  
  20. LEFT OUTER JOIN POST P  
  21.   ON P.BLOG_ID = B.BLOG_ID  
  22. </select>
save_snippets.png
  <select id="queryAllBlogInfo" resultMap="BlogInfo">
      SELECT 
       B.BLOG_ID,
       B.TITLE,
       B.AUTHOR_ID AS BLOG_AUTHOR_ID,
       A.AUTHOR_ID,
       A.USER_NAME,
       A.PASSWORD,
       A.EMAIL,
       A.BIOGRAPHY,
       P.POST_ID,
       P.BLOG_ID   AS BLOG_POST_ID ,
	   P.CREATE_TIME,
       P.SUBJECT,
       P.BODY,
       P.DRAFT
  FROM BLOG B
  LEFT OUTER JOIN AUTHOR A
    ON B.AUTHOR_ID = A.AUTHOR_ID
  LEFT OUTER JOIN POST P
    ON P.BLOG_ID = B.BLOG_ID
  </select>
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. /* 
  2.  * 獲取所有Blog的所有資訊 
  3.  */
  4. publicstatic BlogInfo nestedResultOnTest()  
  5. {  
  6.     SqlSession session = sqlSessionFactory.openSession();  
  7.     BlogInfo blogInfo = new BlogInfo();  
  8.     blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryAllBlogInfo");  
  9.     JSONObject object = new JSONObject(blogInfo);  
  10.     System.out.println(object.toString());  
  11.     return blogInfo;  
  12. }  
save_snippets.png
	/*
	 * 獲取所有Blog的所有資訊
	 */
	public static BlogInfo nestedResultOnTest()
	{
		SqlSession session = sqlSessionFactory.openSession();
		BlogInfo blogInfo = new BlogInfo();
		blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryAllBlogInfo");
		JSONObject object = new JSONObject(blogInfo);
		System.out.println(object.toString());
		return blogInfo;
	}

巢狀結果查詢的執行步驟:

1.根據表的對應關係,進行join操作,獲取到結果集;

2. 根據結果集的資訊和BlogInfo 的resultMap定義資訊,對返回的結果集在記憶體中進行組裝、賦值,構造BlogInfo;

3. 返回構造出來的結果List<BlogInfo> 結果。

對於關聯的結果查詢,如果是多對一的關係,則通過形如 <association property="author" column="blog_author_id" javaType="com.foo.bean.Author"> 進行配置,Mybatis會通過column屬性對應的author_id 值去從記憶體中取資料,並且封裝成Author物件;

如果是一對多的關係,就如Blog和Post之間的關係,通過形如 <collection property="posts" column="blog_post_id" ofType="com.foo.bean.Post">進行配置,MyBatis通過 blog_Id去記憶體中取Post物件,封裝成List<Post>;

對於關聯結果的查詢,只需要查詢資料庫一次,然後對結果的整合和組裝全部放在了記憶體中。

以上是通過查詢Blog所有資訊來演示了一對多和多對一的對映物件處理。