先說下問題產生的背景:
最近在做一個用到MyBatis的項目,其中有個業務涉及到關聯查詢,我是將兩個查詢分開來寫的,即嵌套查詢,個人感覺這樣更方便重用;
關聯的查詢使用到了動態sql,在執行查詢時就出現了如下錯誤:Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'id' in 'class java.lang.Integer'
因為出現了這個問題,在解決的時候一連串又發現了一些其它新的問題,比如關聯查詢時的效率問題,以及映射。通過百度,看文檔基本解決,做個筆記。
現在簡單的用學生和年級兩個對象來做實驗,學生有一個年級屬性,年級有一個學生集合屬性。
先把一些基礎的代碼貼出來:
學生實體:
1 package com.lizhou.entity; 2
3 import java.io.Serializable; 4
5 /**
6 * 學生 7 * @author bojiangzhou 8 * @date 2016年3月30日 9 */
10 public class Student implements Serializable { 11
12 private static final long serialVersionUID = -4165939686905301187L; 13
14 private Integer id; 15 //學生姓名
16 private String name; 17 //年齡
18 private int age; 19 //學生所屬年級
20 private Grade grade; 21
22 //getter/setter...
23
24 }
年級實體:
1 package com.lizhou.entity; 2
3 import java.io.Serializable; 4 import java.util.ArrayList; 5 import java.util.List; 6
7 /**
8 * 年級 9 * @author bojiangzhou 10 * @date 2016年3月30日 11 */
12 public class Grade implements Serializable { 13
14 private static final long serialVersionUID = -3469035301959740223L; 15
16 private Integer id; 17 //年級名稱
18 private String name; 19 //年級下的學生集合
20 private List<Student> studentList = new ArrayList<Student>(); 21
22 //getter/setter...
23
24 }
學生數據接口:
1 package com.lizhou.dao; 2
3 import java.util.List; 4
5 import com.lizhou.entity.Student; 6
7 /**
8 * 數據層 9 * @author bojiangzhou 10 * @date 2016年3月30日 11 */
12 public interface StudentDao { 13
14 /**
15 * 獲取學生信息 16 * @param student 17 * @return
18 */
19 Student getStudent(Student student); 20
21 /**
22 * 獲取學生集合 23 * @param student 24 * @return
25 */
26 List<Student> getStudentList(Student student); 27 }
年級數據接口:
1 package com.lizhou.dao; 2
3 import com.lizhou.entity.Grade; 4
5 /**
6 * 數據層 7 * @author bojiangzhou 8 * @date 2016年3月30日 9 */
10 public interface GradeDao { 11
12 /**
13 * 獲取某個年級 14 * @param grade 15 * @return
16 */
17 Grade getGrade(Grade grade); 18
19 /**
20 * 獲取年級集合 21 * @param grade 22 * @return
23 */
24 Grade getGradeList(Grade grade); 25
26 }
加載配置文件獲取SqlSession工具類:
1 package com.lizhou.tools; 2
3 import java.io.IOException; 4 import java.io.InputStream; 5
6 import org.apache.ibatis.io.Resources; 7 import org.apache.ibatis.session.SqlSession; 8 import org.apache.ibatis.session.SqlSessionFactory; 9 import org.apache.ibatis.session.SqlSessionFactoryBuilder; 10
11 /**
12 * 加載配置文件獲取SqlSession工具類 13 * @author bojiangzhou 14 * @date 2016年3月30日 15 */
16 public class SqlSessionFactoryTool { 17
18 private static SqlSessionFactory sqlSessionFactory; 19
20 public static SqlSessionFactory getSqlSessionFactory(){ 21 if(sqlSessionFactory == null){ 22 try { 23 InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); 24 sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); 25 } catch (IOException e) { 26 e.printStackTrace(); 27 } 28 } 29 return sqlSessionFactory; 30 } 31
32 public static SqlSession openSession(){ 33 return getSqlSessionFactory().openSession(); 34 } 35
36 }
測試類:
1 package com.lizhou.service; 2
3 import java.util.LinkedHashMap; 4 import java.util.List; 5 import java.util.Map; 6
7 import org.apache.ibatis.session.SqlSession; 8 import org.junit.After; 9 import org.junit.Before; 10 import org.junit.Test; 11
12 import com.lizhou.entity.Grade; 13 import com.lizhou.entity.Student; 14 import com.lizhou.dao.GradeDao; 15 import com.lizhou.dao.StudentDao; 16 import com.lizhou.tools.SqlSessionFactoryTool; 17
18 /**
19 * 測試類 20 * @author bojiangzhou 21 * @date 2016年3月30日 22 */
23 public class TestService { 24
25 private SqlSession sqlSession = null; 26
27 private StudentDao studentDao = null; 28 private GradeDao gradeDao = null; 29
30 /**
31 * 測試方法前調用 32 * @throws Exception 33 */
34 @Before 35 public void setUp() throws Exception { 36 sqlSession = SqlSessionFactoryTool.openSession(); 37 studentDao = sqlSession.getMapper(StudentDao.class); 38 gradeDao = sqlSession.getMapper(GradeDao.class); 39 } 40
41 /**
42 * 測試方法后調用 43 * @throws Exception 44 */
45 @After 46 public void tearDown() throws Exception { 47 sqlSession.close(); 48 } 49
50 /**
51 * 測試方法 52 */
53 @Test 54 public void test() { 55
56 } 57
58 }
**************************************************************************************************************************
一、mybatis嵌套查詢(即SQL語句分離的)時報類似於There is no getter for property named 'id' in 'class java.lang.Integer' 的錯誤
先看看產生問題的與學生和年級對應的映射配置:
StudentMapper.xml:這里是通過association查詢年級的(第10行代碼)。
1 <?xml version="1.0" encoding="UTF-8" ?>
2 <!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="com.lizhou.dao.StudentDao">
6
7 <resultMap type="Student" id="studentResult">
8 <id property="id" column="id"/>
9
10 <association property="grade" column="gradeId" select="com.lizhou.dao.GradeDao.getGrade"></association>
11 </resultMap>
12
13 <select id="getStudent" resultMap="studentResult">
14 SELECT * FROM student 15 <where>
16 <if test="id != null">
17 AND id=#{id} 18 </if>
19 </where>
20 </select>
21
22 </mapper>
GradeMapper.xml:動態Sql,本意是想如果有一個查詢所有年級和根據id來查詢年級的業務時就可以使用同一個查詢了。
注意這里年級還有一個學生集合沒有映射到gradeResult里面,如果映射了會循環查詢最終導致棧溢出的,后面再說。
1 <?xml version="1.0" encoding="UTF-8" ?>
2 <!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="com.lizhou.dao.GradeDao">
6
7 <resultMap type="Grade" id="gradeResult">
8 <id property="id" column="id"/>
9
10 </resultMap>
11
12 <select id="getGrade" parameterType="Grade" resultMap="gradeResult">
13 SELECT * FROM grade 14 <where>
15 <if test="id != null">
16 AND id=#{id} 17 </if>
18 </where>
19 </select>
20
21 </mapper>
測試代碼:
1 @Test 2 public void test() { 3 Student student = new Student(); 4 student.setId(1); 5 //根據id查詢學生
6 student = studentDao.getStudent(student); 7
8 System.out.println(student); 9 }
我的本意是各自的業務分開來寫,我要查詢學生時就調用getStudent,要查詢年級時就調用getGrade,而我要查詢學生的同時還要查詢其關聯的年級,這樣我也可以調用getGrade來完成,這樣能很好的實現重用。
但結果並不是這樣的,報錯:Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'id' in 'class java.lang.Integer'
剛開始真的沒明白怎么回事,在Integer中找不到id屬性??
我將GradeMapper.xml中的select語句改成如下時,就可以:
1 <mapper namespace="com.lizhou.dao.GradeDao">
2
3 <resultMap type="Grade" id="gradeResult">
4 <id property="id" column="id"/>
5
6 </resultMap>
7
8 <select id="getGrade" parameterType="Grade" resultMap="gradeResult">
9 SELECT * FROM grade WHERE id=#{id} 10 </select>
11
12 </mapper>
百度了很久,原因是在查詢到一條記錄的時候就會將id注入,而且不管#{id}的名稱,就是說你隨便寫也能查出結果來,第9行代碼:
1 <mapper namespace="com.lizhou.dao.GradeDao">
2
3 <resultMap type="Grade" id="gradeResult">
4 <id property="id" column="id"/>
5
6 </resultMap>
7
8 <select id="getGrade" parameterType="Grade" resultMap="gradeResult">
9 SELECT * FROM grade WHERE id=#{id-xxxxxx} 10 </select>
11
12 </mapper>
結論》》聯合查詢時:
不管輸入參數名稱是什么,mybatis最終會執行:
效果為:select * from grade where id =resultSet.getInt("id");
所以我之前用動態sql時去判斷id,其實是不存在id的,所以導致那樣的錯誤!!
這里參考原文解釋:MyBatis中Association聯合select使用
------------------------------------------------------------------------------------------------------------------------------------------------------------------
二、嵌套結果查詢導致的循環查詢棧溢出
如果這樣使用聯合查詢,其實還存在另外一個問題:學生下有年級,年級下有學生集合,這樣查詢下去只有棧溢出了....
看代碼:
StudentMapper.xml:年級也有映射
1 <mapper namespace="com.lizhou.dao.StudentDao">
2
3 <resultMap type="Student" id="studentResult">
4 <id property="id" column="id"/>
5
6 <association property="grade" column="gradeId" select="com.lizhou.dao.GradeDao.getGrade"></association>
7 </resultMap>
8
9 <select id="getStudent" resultMap="studentResult">
10 SELECT * FROM student WHERE gradeId=#{gradeId} 11 </select>
12
13 </mapper>
GradeMapper.xml:注意此時映射了學生集合的,第6行代碼
1 <mapper namespace="com.lizhou.dao.GradeDao">
2
3 <resultMap type="Grade" id="gradeResult">
4 <id property="id" column="id"/>
5
6 <collection property="studentList" column="id" ofType="Student" select="com.lizhou.dao.StudentDao.getStudent"></collection>
7 </resultMap>
8
9 <select id="getGrade" parameterType="Grade" resultMap="gradeResult">
10 SELECT * FROM grade WHERE id=#{id} 11 </select>
12
13 </mapper>
不管是查詢學生還是年級,結果報錯:java.lang.StackOverflowError
這樣看來,最終都還是要分開寫,就是說我想查詢年級和查詢學生時聯合查詢年級要寫兩個select才行,並沒能達到重用的目的。
但是相信程序員都是有強迫症的,這樣有點不爽,我就又百度,看能不能很好的重用,結果是沒有百度到。
但是在看文檔的時候,發現了另一個問題:聯合查詢分開寫查詢語句是很消耗性能的,明明可以只用執行一次查詢,卻執行了兩次甚至更多次。
文檔是這樣說的:這種方式很簡單,但是對於大型數據集合和列表將不會表現很好。問題就是我們熟知的“N+1 查詢問題”。
N+1 查詢問題可以是這樣引起的:
1. 你執行了一個單獨的 SQL 語句來獲取結果列表(就是“+1”)
2.對返回的每條記錄,你執行了一個查詢語句來為每個加載細節(就是“N”)
這個問題會導致成百上千的 SQL 語句被執行。這通常不是期望的。
------------------------------------------------------------------------------------------------------------------------------------------------------------------
三、關聯的嵌套結果
上面的叫做[關聯的嵌套查詢]
所以自然有另一種方法----[關聯的嵌套結果](文檔上這樣說的):聯合兩張表在一起,代替了執行一個分離的語句。
看代碼:
1 <mapper namespace="com.lizhou.dao.StudentDao">
2
3 <resultMap type="Student" id="studentResult">
4 <id property="id" column="id"/>
5 <result property="name" column="name"/>
6 <result property="age" column="age"/>
7 <association property="grade" column="gradeId" javaType="Grade">
8 <id property="id" column="id"/>
9 <result property="name" column="name"/>
10 </association>
11 </resultMap>
12
13 <select id="getStudent" resultMap="studentResult">
14 SELECT * FROM student LEFT OUTER JOIN grade ON grade.id=student.gradeId WHERE student.id=#{id} 15 </select>
16
17 </mapper>
映射的時候是將年級和學生一起映射的,這個就叫做嵌套結果映射。然后是sql語句,使用的是聯合查詢。
------------------------------------------------------------------------------------------------------------------------------------------------------------------
四、映射、效率
這里又遇到了幾個問題:
1.在之前寫resultMap映射時,我是一般不愛將屬性映射寫出來的,因為mybatis會自動映射的,但是這里不行了。
在嵌套結果映射時,哪個屬性沒有映射那么這個值就是空的,查詢出來的結果不會注入到對象中。所以必須將所有屬性都顯示映射出來。
2.先看看映射都OK時輸出的結果:Student [id=2, name=羅若亞卓落, age=22, grade=Grade [id=2, name=羅若亞卓落, studentList=[]]]
輸出結果中,grade中的數據和student中的一樣了....因為Student的id和name跟Grade的id和name屬性名一樣。
mybatis其實就是使用resultSet.getString("name")這樣的方式來獲取值的,所以聯合查詢時兩張表的字段名不能重復。
或者可以在配置文件中配置好映射和sql語句。如下:注意第8、9行代碼,重新配置column;以及sql語句。但是如果屬性過多,sql代碼可能就有點長了,所以最好還是保持字段名不一樣吧。
1 <mapper namespace="com.lizhou.dao.StudentDao">
2
3 <resultMap type="Student" id="studentResult">
4 <id property="id" column="id"/>
5 <result property="name" column="name"/>
6 <result property="age" column="age"/>
7 <association property="grade" column="gradeId" javaType="Grade">
8 <id property="id" column="grade_id"/>
9 <result property="name" column="grade_name"/>
10 </association>
11 </resultMap>
12
13 <select id="getStudent" resultMap="studentResult">
14 SELECT 15 s.id, 16 s.name, 17 s.age, 18 g.id as grade_id, 19 g.name as grade_name 20 FROM student s LEFT OUTER JOIN grade g ON g.id=s.gradeId 21 WHERE s.id=#{id} 22 </select>
23
24 </mapper>
3.最后是在文檔中看到的一個比較重要的問題,最好是要顯示映射出id。直接復制原話:
非常重要:在嵌套結果映射中 id 元素扮演了非常重要的角色。應該通常指定一個或多個屬性,它們可以用來唯一標識結果。
實際上就是如果你不使用它(id 元素) ,但是會產生一個嚴重的性能問題,不過 MyBatis 仍然可以正常工作。
選擇的屬性越少越好,它們可以唯一地標識結果。主鍵就是一個顯而易見的選擇(即便是聯合主鍵)。
============================================================================================
剛學完mybatis不久,現在正在做的這個項目(視頻教程,當練手)本來是struts+spring+ibatis的,我想着ibatis也算是過去式了,恰好前陣剛學了mybatis還沒實戰過,就把ibatis換成了mybatis。
不過只是知道怎么使用,很多細節性的東西還沒深入了解。所以這篇博客也只是開發中遇到的一些問題解決后做個筆記,大部分都屬於個人理解,有些可能不全面,希望有不同見解的朋友一起交流~~O(∩_∩)O~~
