MyBatis 中的映射文件,告诉这 MyBatis 如何进行增删改查数据库,其中至关重要的作用。MyBatis 的想法就行想把 sql 语句和 Java 代码解耦,实现真正的只关心 sql 语句的编写,而无需担心的连接数据库的各种细节。
下面的例子中的 Emp 实体类和 数据库字段如下
Emp.java
public class Emp {
private Integer empId;
private String empName;
private String empMail;
private Integer empGender;
private Integer deptId;
private Dept dept;
//getter、setter 方法省略
......
}
Dept.java
public class Dept {
private Integer deptId;
private String deptName;
private List<Emp> emps;
//getter、setter 方法省略
...
}
emp 表
create table emp(
-- 职工号
emp_id int primary key auto_increment,
-- 职工姓名
emp_name varchar(20) not null,
-- 职工邮箱
emp_mail varchar(20) not null,
-- 职工性别
emp_gender int not null,
-- 部门号
dept_id int not null
)
元素
-
<mapper namespace></mapper>
标签是一级标签,用来把映射文件和 Dao 接口绑定起来。也就意味这一个映射文件,一个接口<mapper namespace="com.nhky.dao.EmpDao"></mapper>
-
二级标签
- cache:配置二级缓存
- cache-ref:引用其他命名空间的二级换成配置
- resultMap:自定义映射结果集
- sql:定义可复用的 sql 语句
- select:映射查询语句
- insert:映射插入语句
- delete:映射删除语句
- update:映射更新语句
-
三级标签
- Where
- If
- Foreach
- Set
- trim
增删改查
EmpDao.java 接口类
public interface EmpDao {
Emp selectEmpByEmpId(Integer EmpId);
void insertEmp(Emp emp);
void deleteEmpByEmpId(Integer empId);
void updateEmpNameByEmpId(String empName,Integer empId);
}
Select 标签
EmpDao.xml 配置文件
<select id="selectEmp" resultType="com.nhky.entitys.Emp">
select * from emp where emp_id #{empId}
</select>
对比上面的 selectEmpByEmp 方法,这个标签内容意思
- id 属性:指向方法名为 selectEmpByEmp 的方法
- resultType 属性:返回结果集的类型为 Emp 实体类的全限定名(或别名),和 selectEmpByEmp 返回值一致。
- #{empId} :这里表示一个占位符,当方法只有一个参数的时候,里面的名称可以任意值,通过 empId 找到 emp 的信息。
Insert 、update、delete 标签
<insert id="insertEmp">
insert into emp(emp_name,emp_mail,emp_gender,dept_id)
values(#{empName},#{empMail},#{empGender},#{deptId})
</insert>
<delete id="deleteEmpByEmpId">
delete from emp where emp_id=#{empId}
</delete>
<update id="updateEmpNameByEmpId">
update emp set emp_name =#{param1} where emp_id =#{param2}
</update>
属性和 select 标签意思相同。不同的是:
- insert 标签传入的是一个 Emp 实体类对象,sql 语句的占位符指定的值必须是 #{Java Bean 属性名}
- update 标签中传入是多个参数,必须得以 #{param1} 、#{param2}、#{param3}… 或 #{0}、 #{1}、 #{2}… 来跟方法的参数顺序一一对应。其中 #{键} ,键必须是 param1…(myBatis 3.5 的版本是 arg0、arg1…)或 0… 的。
结果返回值
如何判断增删改查是否成功?MyBatis 内部封装好了 Integer 、 int 、Long 、long、Boolean、boolean 类型参数来作为增删改查方法所影响的行数。可以把我们接口的方法的返回值改成上述对应的类型,就可以获得所影响的行数(boolean 类型的返回值是以如果返回 0 ,则 false,反之 true)。并且映射文件无需修改。
参数处理
单个参数
对于单个参数,MyBatis 不会进行特殊的处理,在 SQL 映射文件中使用任意的参数名都可以取得该参数。
沿用最开始解释标签的例子,把 Select 标签修改如下,照样能够查询到值
<select id="selectEmp" resultType="com.nhky.entitys.Emp">
select * from emp where emp_id #{eeee}
</select>
多个参数
对于获取多个参数,例如上面的 update 标签的例子,我们可以测试一下,如果修改如下
<update id="updateEmpNameByEmpId">
update emp set emp_name =#{empName} where emp_id =#{empId}
</update>
程序会报如下异常:
Cause: org.apache.ibatis.binding.BindingException: Parameter 'empName' not found. Available parameters are [1, 0, param1, param2]
只有参数 0,1,param1,param2 (MyBatis 3. 5 则是 arg1, arg0, param1, param2)可用。为什么?因为传递多个参数的情况下,MyBatis 会将参数自动封装成 Map 键值对。而每一个参数对应两个键、两个值,两个键不同,但是值相同。封装结果可以想象如下:
Key | value |
---|---|
0 | 第一个参数的值 |
Param1 | 第一个参数的值 |
1 | 第二个参数的值 |
Param2 | 第二个参数的值 |
2 | 第三个参数的值 |
param3 | 第三个参数的值 |
对应的在映射文件中,#{键} MyBatis 则会对应顺序传入相应的参数。也就是上面的 update 标签,如下:
<update id="updateEmpNameByEmpId">
update emp set emp_name =#{param1} where emp_id =#{param2}
</update>
但是如果执意要指定参数名?可以使用 @Param 注解,给参数指定在封装成 Map 时候的 key,修改接口方法
void updateEmpNameByEmpId(@Param("empName")String empName,@Param("empId")Integer empId);
在映射文件也就可以使用指定参数名字了
<update id="updateEmpNameByEmpId">
update emp set emp_name =#{empName} where emp_id =#{empId}
</update>
注意:
- 使用 @Param 注解,会代替对应顺序的 0 , 1 …数字 ‘键’,0,1… 对应的顺序将不能使用,而不会代替 param1 ,param2 … 对应的顺序 ‘键’
传递对象
传递的参数是一个实体类对象:会将参数封装成一个Map键值对,键对应的是实体类对象的属性名,值对应的是该实体类属性的值。比如,传递对象为Emp,则封装的Map键值对为:
Key | value |
---|---|
empName | empName属性值 |
empMail | empMail属性值 |
empId | empId属性值 |
empGender | empGender属性值 |
deptId | deptId属性值 |
那么对应的,也就是开头的 insert 标签例子
<insert id="insertEmp">
insert into emp(emp_name,emp_mail,emp_gender,dept_id)
values(#{empName},#{empMail},#{empGender},#{deptId})
</insert>
传递 Map 参数
传入参数为 Map 的情况下,直接在 sql 映射文件中使用 #{map 中对应的键} 就可以传递相应的值
添加接口方法
List<Emp> selectEmpByEmpIdAndEmpName(Map<String, Object> map);
映射文件
<select id="selectEmpByEmpIdAndEmpName" resultType="com.nhky.entitys.Emp">
select * from emp limit #{start},#{pageSize}
</select>
调用
@Test
public void testArg02() throws Exception {
InputStream is = Resources.getResourceAsStream("myBatis-conf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao empDao = sqlSession.getMapper(EmpDao.class);
Map<String, Object> map = new HashMap<>();
map.put("start", 0);
map.put("pageSize",5);
List<Emp> emps = empDao.selectEmpByEmpIdAndEmpName(map);
System.out.println(emps);
sqlSession.commit();
sqlSession.close();
}
传递 List、Set、Array 参数
一个 list 参数是很好处理的,集合会被封装成键值对,键为 list 和 collection,值为 List 集合的值,所以在 sql 映射文件中想要取得集合的某个位置的具体的值:#{list[索引位置]},#{collection[索引位置]},同理Set集合或者Array数据处理相似。
- List 处理
List<Emp> selectEmpLimit(List<Object> list);
映射文件
<select id="selectEmpLimit" resultType="com.nhky.entitys.Emp">
select * from emp limit #{list[0]},#{list[1]}
</select>
调用
@Test
public void testArg03() throws Exception {
InputStream is = Resources.getResourceAsStream("myBatis-conf.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao empDao = sqlSession.getMapper(EmpDao.class);
List list = new ArrayList<Integer>();
list.add(0);
list.add(5);
List<Emp> emps = empDao.selectEmpLimit(list);
System.out.println(emps);
sqlSession.commit();
sqlSession.close();
}
- Array 数组的话,修改映射文件,其他都差不多
<select id="selectEmpLimit" resultType="com.wanbangee.entities.Emp">
select * from emp limit #{array[0]},#{array[1]}
</select>
如果 List 的用在多个参数上,也要符合多参的要求,也就是 0,1,2… 或 param1,param2…如下
映射文件
<select id="selectEmpLimit" resultType="com.nhky.entitys.Emp">
select * from emp limit #{param1[0]},#{param1[1]}
</select>
# 和 $ 区别
默认情况下,使用 #{}
参数语法时,MyBatis 会创建 PreparedStatement
参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中动态修改表名或库名,就可以使用 ${}
试想有一个订单表 order_202001,order_202002,order_202003…有一个业务,要求按照用户输入的月份查询对应月份的订单信息,参数值为202001,那么 sql 语句如何写?
Select * from order_#{year_month}
Select * from order_${year_month}
上面的语句,第一句采用了预编译,我们指定,预编译是先把 sql 语句编译了,在填值,编译完的话,第一句就变成了 Select * from order_?
,那就没有这个表,也就出了问题,而第二局代码采用了字符串拼接,意思就懂了吧。
总结
- #:使用占位符的形式传递参数
- $ : 直接通过字符串拼接的形式传递参数
- $ : 直接通过字符串拼接的形式传递参数
- 存在 sql 注入的安全问题
取值设置参数类型
jdbcType 通常在数据为 null 的情况下,有些数据库不支持 MyBatis 中定义的 null 类型,比如 Oracle 就不支持。MyBatis 在默认情况下,会将 NUll 自动的定义为 JdbcType.OTHER
,MYSQL 中支持 Jdbc.OTHER
类型,但是 Oracle中,并不支持 Jdbc.OTHER
类型,所以在ORACLE 环境下,我们不能使用 JdbcType.OTHER
,而是使用JdbcType.NULL
,这样在传入值为 null 的情况下,Oracle不会报错。
<select id="selectEmpByID2" resultType="com.wanbangee.entities.Emp">
insert into emp_721(emp_id,emp_name,emp_mail,emp_gender,dept_id) values(seq_2020721.nextval,#{empName,jdbcType=NULL},#{empMail,jdbcType=NULL},#{empGender,jdbcType=NULL},#{deptId,jdbcType=NULL})
</select>
返回值
查询记录返回 List
如果查询的结果集为多笔数据,每一笔数据都对应实体类的一个对象,那么我们可以使用 List 接收,那么每一条数据都会被分装为实体类的一个对象,而 List 中就是存放了查询结果集的多个对象。
EmpDao.java
public List<Emp> selectEmp();
映射文件
<select id="selectEmp" resultType="com.nhky.entities.Emp" >
select * from emp
</select>
注意:
- resultType 要设置为实体类的全类名,不是 List 全类名
查询记录返回 Map
如果查询的结果集是多个实体类的情况下,不能使用 Map 接收返回结果。只有查询结果为单行的时候,才能使用 Map 接收结果。
Map 接收的结果为,表的列名为键,而表的值为 Map 的值。
EmpDao.xml
public Map<String,Object> selectEmpByEmpId(Integer empId);
映射文件
<select id="selectEmpByEmpId" resultType="map">
select * from emp where emp_id = #{empId}
</select>
查询记录返回单行单列
比如查询数据的笔数,那么怎会获取?直接把 resultType 定义为结果集类型即可
EmpDao.xml
public Integer selectEmpByCount();
映射文件
<select id="selectEmpByCount" resultType="int">
select count(*) from emp
</select>
这里也可以使用 Map 接收。
查询记录返回 resultMap
自定义结果集的映射关系,可以利用它把我们的返回结果和实体类关系映射起来
<resultMap id="empMap" type="com.nhky.entitys.Emp">
<id column="emp_id" property="empId" />
<result column="emp_name" property="empName" />
<result column="emp_mail" property="empMail" />
<result column="emp_gender" property="empGender" />
<result column="dept_id" property="deptId" />
</resultMap>
如生,把实体类 Emp 和数据库表字段联系起来
- 属性
- type:封装后结果集的类型全类名或别名
- id:表示自定义结果集的唯一标识
id 标签和 result 标签
用于将一个列的值映射到实体类的一个属性或简单的数据类型上。
两者唯一不同的是,id 元素标签对应的属性会被标记为对象的标识符,在比较对象实例的时候使用,有着提高整体性能的效果。尤其是缓存和连接映射。
- id 标签:表示自定义主键封装映射规则
- result 标签:表示自定义非主键封装的映射规则
- 属性
- column:查询结果集中数据库的列名
- property:实力类的对应属性名或简单类型
注意:
-
在 Select 标签中要使用自定义结果集,要使用 resultMap 属性进行指定,并且此时 resultType 不能使用
<select id="selectEmp" resultMap="empEmp"> select * from emp </select>
级联属性
如果在 Emp 实体类包含了 Dept 实体类,并且在查询 Emp 的情况下,查询 Dept 的值,该如何封装上?
映射文件:
<resultMap id="empMap" type="com.nhky.entitys.Emp">
<id column="emp_id" property="empId" />
<result column="emp_name" property="empName" />
<result column="emp_mail" property="empMail" />
<result column="emp_gender" property="empGender" />
<result column="dept_id" property="deptId" />
<!-- 使用属性名 dept ,并且直接调用 dept 实体类中的属性名即可 -->
<result column="dept_id" property="dept.deptId"/>
<result column="dept_name" property="dept.deptName" />
</resultMap>
<select id="selectEmpById" resultMap="myEmp" >
select a.emp_id,a.emp_name,a.emp_mail,a.emp_gender,a.dept_id,b.dept_name from emp a
left join dept b on a.dept_id = b.dept_id where emp_id = #{id}
</select>
通过上述代码
关联(association)
Association 是 MyBatis 提供的一个多对一的关联映射标签。使用在 resultMap 标签中,表示多对一的关系。在多的一端引用一的一端。比如一个雇员可以对应一个部门。毕竟雇员对应的部门只有一个。
更改上面的 resultMap
<resultMap id="empMap" type="com.nhky.entitys.Emp">
<id column="emp_id" property="empId" />
<result column="emp_name" property="empName" />
<result column="emp_mail" property="empMail" />
<result column="emp_gender" property="empGender" />
<result column="dept_id" property="deptId" />
<association javaType="com.nhky.Dept" property="dept">
<id column="dept_id" property="deptId" />
<result column="dept_name" property="deptName" />
</association>
</resultMap>
<select id="selectEmpById" resultMap="empMap" >
select a.emp_id,a.emp_name,a.emp_mail,a.emp_gender,a.dept_id,b.dept_name from emp a
left join dept b on a.dept_id = b.dept_id where emp_id = #{id}
</select>
以上查询的结果正确,但是数据量庞大的时候,效率低,因为上面使用了关联查询,可能造成笛卡尔乘积。所以采用分步查询更多一点:
association 分步查询
Association是支持进行分布查询的,第一步先查询雇员信息,第二步查询对应 部门信息。
EmpDao.xml 映射文件
<resultMap type="com.nhky.entities.Emp" id="myEmp">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="emp_mail" property="empMail"/>
<result column="emp_gender" property="empGender"/>
<result column="dept_id" property="deptId"/>
<association property="dept" select="com.nhky.dao.DeptDao.selectDeptByDeptId" column="dept_id">
</association>
</resultMap>
<select id="selectEmpById" resultMap="myEmp" >
select emp_id,emp_name,emp_mail,emp_gender,dept_id from emp a
where emp_id = #{id}
</select>
DeptDao.xml 映射文件
<select id="selectDeptByDeptId" resultType="com.nhky.entitys.Dept">
select * from dept where dept_id = #{deptId}
</select>
上面的代码意思是,由于我们的 Emp 实体类包含了 Dept 实体类,所以在 association 标签中,property 指的是 Emp 中 Dept 实体类的属性名,select 指的是调用其他的 Dao 映射文件的查询方法,column 表示的查询所需要传递的参数的数据库列名。
- association :使用之后,可以将应用的属性进行封装,比如上面的 Emp 中封装的 dept 属性。
- property:设置要封装的属性
- select:调用其他的 Dao 查询方法,需要使用全类名
- column:查询方法所需要传递的参数的数据库列名,上面需要的就是 dept_Id
延迟加载
上述的实体类 Emp 的所用属性在调用方法的时候,就能把所有的数据查出来,但是有时候我们只需要显示雇员的部分信息,不需要显示部门的怎么办?在需要显示部门信息的时候,在加载,该怎么办?那么可以使用延迟加载。
这种查询策略叫做延迟加载策略,又称按需加载,又称懒加载,比如 Emp 在调用 getDept() 方法的时候,才查询,实现懒加载,提示数据库性能。默认情况下,MyBatis 提供的分布查询策略就是即时加载,所以我们要通过配置开启延迟加载策略。
-
改变 MyBatis 运行时的行为,表示要开启延迟加载
<setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/>
上面会对所有的都采取懒加载的策略,如果我们有些需要即使加载呢?
-
可以在 association 标签中配置 fetchType 属性
<association property="xxx" select="xxx" column="xxx" fetchType="eager"> </association>
- eager 表示渴望,也就是即使加载
- lazy 表示懒得,也就是懒加载
collection 和 association 定义关联的封装规则
除了上述的 association 标签,还有一个 collection 标签和 association 标签的使用类似,只是用途不同。
-
多对一:在多的一端引用一的一端作为属性,比如在 Emp 中引用 Dept 作为属性,MyBatis 中使用 association
-
一对一:在任意的一的一端,引用另外的一的一端作为属性,MyBatis 中使用 association
-
一对多:在以的一的一端引用多的一端,且使用集合属性,比如在 Dept 中有一个 Emp 的集合作为属性,MyBatis 中使用 collection
-
多对多:以任意一端持有另外一端的集合,作为属性。比如学生和老师的关系,就是典型的多对多,MyBatis中使用 collection
collection 分布查询
集合元素和关联元素几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分。 所以让我们来关注它们的不同之处吧。
我们来继续上面的示例,一个雇员(Emp)只有一个部门(Dept)。但一个部门有很多雇员。 在部门类中,这可以用下面的写法来表示:
private List<Emp> emps;
DeptDao 的映射文件
<resultMap type="com.nhky.entities.Dept" id="myDept">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<collection property="emps" select="com.nhky.dao.EmpDao.selectEmpByDeptId" column="dept_id" javaType="java.util.List" >
</collection>
</resultMap>
<select id="selectDeptByDeptId" resultMap="myDept">
select * from dept where dept_id = #{deptId}
</select>
你可能会立刻注意到几个不同,但大部分都和我们上面学习过的关联元素非常相似。 首先,你会注意到我们使用的是集合元素。 其他的懒加载,其实都和 association 相同的。
分布查询传递多列值情况
不管是 association 还是 collection,上面的程序只传递了一列值作为分布查询的查询条件参数,在开发中,还存在另外一种情况,分布查询值传递两个参数,也就意味着传递两列值作为分布查询的查询条件参数
DeptDao.xml 映射文件
<resultMap type="com.nhky.entities.Dept" id="myDept">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<!--
在传递多列值的情况下使用{abc=dept_id,bcd=dept_name},根据我们参数规则,
多个参数在 MyBatis 中会封装了Map键值对,此时 Map 封装的效果如下:
key ======================value
abc dept_id数据列的值
bcd dept_name数据列的值
-->
<collection property="emps" select="com.nhky.dao.EmpDao.selectEmpByDeptId"
column="{abc=dept_id,bcd=dept_name}" javaType="java.util.List" >
</collection>
</resultMap>
EmpDao.xml 映射文件
<select id="selectEmpByDeptId" resultType="com.nhky.entitys.Emp" >
select * from emp where dept_id=#{abc} or 1 = #{bcd}
</select>
注意的是,如果传递多个参数,那么这里的 #{键} 必须符合传递过来对应的参数名,也就是 DeptDao.xml 指定的键(abc,和 bcd),和 EmpDao 接口方法的参数名无关 。(上面的 1 = #{bcd} 只是为了演示获取值而已)
鉴别器
有时候,一个数据库查询可能会返回多个不同的结果集(但总体上还是有一定的联系的)。 鉴别器(discriminator)元素就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。
一个鉴别器的定义需要指定 column 和 javaType 属性。column 指定了 MyBatis 查询被比较值的地方(属性)。 而 javaType 用来确保使用正确的相等测试
<resultMap type="com.nhky.entities.Dept" id="myDept">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<discriminator javaType="string" column="dept_name">
<case value="财务部" resultType="com.nhky.entities.Dept">
<collection property="emps" select="com.nhky.dao.EmpDao.selectEmpByDeptId"
column="{deptId=dept_id}" javaType="java.util.List" >
</collection>
</case>
<case value="行政部" resultType="com.nhky.entities.Dept">
<collection property="emps" select="com.nhky.dao.EmpDao.selectEmpByDeptId"
column="{deptId=dept_id}" javaType="java.util.List" >
</collection>
</case>
</discriminator>
</resultMap>
<select id="selectDeptByDeptId" resultMap="myDept">
select * from dept where dept_id = #{deptId}
</select>
case 标签
- value 用于指定要和属性比较的值,
- resultType 用于比较结果为 true 的时候,目前使用 resultMap=”myDept“ 的接口方法的返回值类型就是 com.nhky.entities.Dept。
- collection 标签可以参考上上面的解释。
上面的代码是,如果 dept_name 的值为财务部或行政部,那么就把财务部或行政部的 deptId 传递到 com.nhky.dao.EmpDao.selectEmpByDeptId 获取它的所有雇员基本信息。不是这两个部门的,都不用获取雇员基本信息。
这里的 case 如同 java 中的 switch ,互相排除,一旦执行了一个,其他的不会再执行。