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?- /*
- * 通過blogId獲取BlogInfo物件
- */
- publicstatic 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;
- }
/*
* 通過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?- <resultMaptype="com.foo.bean.BlogInfo"id="BlogInfo">
- <idcolumn="blog_id"property="blogId"/>
- <resultcolumn="title"property="title"/>
- <associationproperty="author"column="blog_author_id"
- javaType="com.foo.bean.Author"select="com.foo.bean.AuthorMapper.selectByPrimaryKey">
- </association>
- <collectionproperty="posts"column="blog_id"ofType="com.foo.bean.Post"
- select="com.foo.bean.PostMapper.selectByBlogId">
- </collection>
- </resultMap>
- <selectid="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>
<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?
- /*
- * 通過blogId獲取BlogInfo物件
- */
- publicstatic 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;
- }
/*
* 通過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
- <resultMaptype="com.foo.bean.BlogInfo"id="BlogInfo">
- <idcolumn="blog_id"property="blogId"/>
- <resultcolumn="title"property="title"/>
- <associationproperty="author"column="blog_author_id"javaType="com.foo.bean.Author">
- <idcolumn="author_id"property="authorId"/>
- <resultcolumn="user_name"property="userName"/>
- <resultcolumn="password"property="password"/>
- <resultcolumn="email"property="email"/>
- <resultcolumn="biography"property="biography"/>
- </association>
- <collectionproperty="posts"column="blog_post_id"ofType="com.foo.bean.Post">
- <idcolumn="post_id"property="postId"/>
- <resultcolumn="blog_id"property="blogId"/>
- <resultcolumn="create_time"property="createTime"/>
- <resultcolumn="subject"property="subject"/>
- <resultcolumn="body"property="body"/>
- <resultcolumn="draft"property="draft"/>
- </collection>
- </resultMap>
<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?
- <selectid="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>
<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?
- /*
- * 獲取所有Blog的所有資訊
- */
- publicstatic 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;
- }
/*
* 獲取所有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所有資訊來演示了一對多和多對一的對映物件處理。