MyBatis进阶
Mapper代理
直接利用session+id来执行sql的方式存在一些问题
- session执行sql时都需要提供要执行sql的id,而这个id是字符串类型,意味着id是否正确在编译期间是无法获知的,必须等到运行时才能发现错误,
- sql需要的参数和返回值类都不明确,这也增加了出错的概率
理想的状况:像调用方法一样调用sql,既避免了直接写id的问题,也可以明确指定方法的参数类型和返回值类型
解决方案:
MyBatis通过动态代理来解决,简单的说动态代理(动态生成),就是在运行过程中自动产生一个对象,用它来代理原本已经存在的对象的方法
MyBatis中本来由Executor(被代理对象)来完成sql的执行,现在由代理对象(自动生成)来代理Executor完成,代理对象会将我们的操作转交给Executor
问题是:MyBatis怎么知道代理对象是什么样的对象呢?,这就需要为MyBatis提供Mapper接口,这个接口就是对mapper.xml中的sql语句的声明,与DAO层的接口一毛一样
使用步骤
-
创建接口类
package com.kkb.mapper; import com.kkb.pojo.Products; public interface ProductsMapper { //根据name查询一个Products public Products selectProductByName(String name); }
-
提供响应的sql映射
<mapper namespace="com.kkb.mapper.ProductsMapper"> <select id="selectProductByName" parameterType="string" resultType="com.kkb.pojo.Products"> select *from products where pname = #{name} </select> </mapper>
-
获取代理对象 执行方法完成操作
@Test public void proxyTest(){ SqlSession session = factory.openSession(); ProductsMapper mapper = session.getMapper(ProductsMapper.class); Products product = mapper.selectProductByName("泰国咖喱"); System.out.println(product); session.close(); }
注意事项:
- 必须保证mapper.xml中的namespace与接口的全限定名称一致
- 方法的名称必须与对应的sql statement的id一致
- 方法的参数必须与对应的sql statement的parameterType一致
- 方法的返回值必须与对应的sql statement的resultType一致
XML配置
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
-
environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
注意配置文件各个节点个层次是固定,需按照上述的顺序书写否则报错,
单独使用MyBatis的场景是很少的,后续都会将其与Spring进行整合,并由Spring来对MyBatis进行配置,加粗的为需要重点关注,其余的了解即可,若遇到特殊需求可查阅官方文档
属性(properties)
properties可从配置文件或是properties标签中读取需要的参数,使得配置文件各个部分更加独立
内部properties标签
外部配置文件
jdbc.properties位于resource下
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql:///mybatisDB?serverTimezone=Asia/Shanghai&characterEncoding=utf8
user = root
password = admin
引用方式
当内部和外部属性出现同名时,则优先使用外部的;
别名(typeAliases)
typeAliases用于为Java的类型取别名,从而简化mapper中类名的书写
为某个类定义别名
在mapper.xml中就可以直接使用别名 不区分大小写
当指定package时将批量为包下所有类指定别名为类名小写
<typeAliases>
<package name="com.kkb.pojo"/>
</typeAliases>
使用package批量设置时很容易出现别名冲突,这是就需要使用@Alias注解来为冲突的类单独设置别名
@Alias("products1")
public class Products {
private int pid;
private String pname;
private float price;
private Date pdate;
private String cid;
.....}
下面列出MyBatis已存在的别名
_byte | byte |
---|---|
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
object | Object |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
映射(Mappers)
在mapper.xml文件中定义sql语句后,就必须让MyBatis知道,到哪里去找这些定义好的sql,这就需要在配置文件中指出要加载的mapper的位置;MyBatis支持4种方式来加载mapper文件
<mappers>
<!-- 1.指定资源文件的相对目录 相对于classpath
maven项目会自动将java和resources都添加到classpath中
所以相对与resources来指定路径即可-->
<mapper resource="mapper/ProductsMapper.xml"/>
<!-- 2.指定文件的绝对路径,MyBatis支持但是一般不用-->
<mapper url="file:///Users/jerry/Downloads/MYB/src/main/resources/mapper/ProductsMapper.xml"/>
<!-- 3.通过指定接口 让MyBatis自动去查找对应的Mapper
这种方式要求映射文件和接口处于同一目录下,并且名称相同
要保证上述要求只需要在resources下创建于接口包名相同的目录即可
注意:运行前先clean,否则可能因为之前已经存在target而不会重新编译,导致无法加载新建的mapper文件
-->
<mapper class="com.kkb.mapper.ProductsMapper"/>
<!-- 4.指定包名称,扫描包下所有接口和映射文件
这种方式同样要求映射文件和接口处于同一目录下,并且名称相同-->
<package name="com.kkb.mapper"/>
</mappers>
第3,4种方式映射文件目录示例:
动态SQL
动态SQL指的是SQL语句不是固定死的,可以根据某些条件而发生相应的变化,这样的需求非常多见
例如:页面提供的搜索功能,要根据用户给出的一个或多个条件进行查询
在JDBC时代,我们通过Java代码来判断某个条件是否有效然后拼接SQL语句,这是非常繁琐的,MyBatis的动态SQL很好的解决了这个问题,使判断逻辑变得简洁直观;
在Mapper中通过标签来完成动态SQL的生成
1. if
从页面接受参数后我们会将参数打包为对象,然后将对象作为参数传给MyBatis执行查询操作,sql语句需要根据是否存在参数而动态的生成
mapper:
<!-- 根据姓名或cid进行搜索-->
<select id="searchProducts" parameterType="products" resultType="products">
select *from products
where 1=1
<if test="pname != null">
and pname like '%${pname}%'
</if>
<if test="cid != null">
and cid = #{cid}
</if>
</select>
测试:
@Test
public void searchTest(){
SqlSession session = factory.openSession();
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
//查询条件对象
Products condition = new Products();
condition.setName("新疆");
condition.setCid("s001");
//执行查询
List<Products> product = mapper.searchProducts(condition);
System.out.println(product);
session.close();
}
where1=1
用于保持sql的正确性,当不存在条件时,则查询全部
2.where
where的作用就是用于取出上面的where 1=1,因为这会让人看起来产生疑惑,其作用是将内部语句中的第一个and去除
<select id="searchProducts" parameterType="products" resultType="products">
select *from products
<where>
<if test="pname != null">
and pname like '%${pname}%'
</if>
<if test="cid != null">
and cid = #{cid}
</if>
</where>
</select>
3. foreach
当一个条件中中需要需要多个参数时则需要将多个参数拼接到一起,例如: in, not in
mapper:
<select id="searchProducts" parameterType="products" resultType="products">
select *from products
<where>
<if test="pname != null">
and pname like '%${pname}%'
</if>
<if test="cid != null">
and cid = #{cid}
</if>
<if test="ids != null">
<foreach collection="ids" open="and pid in (" close=")" separator="," item="id" index="i">
#{id}
</foreach>
</if>
</where>
</select>
<!--
<if test="ids != null"> 这里不仅判断属性是否为空还判断集合中是否有元素
foreache 标签属性说明:
强调:动态sql本质就是在拼接字符串,带着自己拼接sql的思路来编写动态sql会更好理解
collection 要遍历的集合
open 拼接的前缀
close 拼接的后缀
separator 拼接元素之间的分隔符
item 遍历得到的临时变量名
index 当前元素的索引(不常用)
-->
测试代码:
@Test
public void searchTest(){
SqlSession session = factory.openSession();
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
//查询条件对象
Products condition = new Products();
int[] ids = new int[]{1,2,3,4,5,};
condition.setIds(ids);
//执行查询
List<Products> product = mapper.searchProducts(condition);
System.out.println(product);
session.close();
}
注意需要为POJO(Products
)对象增加ids属性
4. set
set标签用于更新语句,当同事要更新多个字段时,我们需要留意当前是否是最后一个set,避免在后面出现,
符号,使用set标签后可自动去除最后的逗号
mapper:
<update id="updateProductTest" parameterType="products">
update products
<set>
<if test="pname != null and pname != ''">
pname = #{pname},
</if>
<if test="price != null and price > 0">
price = #{price},
</if>
<if test="pdate != null">
pdate = #{pdate},
</if>
<if test="cid != null and cid != ''">
cid = #{cid},
</if>
</set>
where pid = #{pid}
</update>
测试代码:
@Test
public void updateTest2(){
SqlSession session = factory.openSession();
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
//获取已有对象
Products product = mapper.selectProductById(7);
product.setPname("云南小土豆");
product.setPrice(10.5f);
//执行更新
mapper.updateProductTest(product);
System.out.println(product);
session.commit();
session.close();
}
5. sql与include
Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。 例如可以吧sql语句中的字段列表提取出来作为通用的sql片段。然后在sql语句中使用 include 节点引用这个sql片段。
<!--提取片段-->
<sql id="fields">pid,pname,price,cid</sql>
<select id="includeTest" resultType="products">
select
<include refid="fields"/> <!-- 引用片段-->
from products
</select>
高级映射
在一些情况下数据库的记录和POJO对象无法直接映射,包括两种情形:
- 数据库字段与POJO字段名称不同(可以避免);
- 关联查询时,需要将关联表的数据映射为另一个类型的POJO(一对一),或List中(一对多);
在MyBatis中通过resultMap来完成自定义映射
1.自定义字段与属性映射
先将Products表中的字段更名为p_xxx,如下所示:
mapper:
<!--自定义映射关系 id:该映射关系的标识 type:映射到的POJO类型此处为别名-->
<resultMap id="product_resultMap" type="products">
<!--主键-->
<id column="p_id" property="pid"/>
<!--其他字段-->
<result column="p_name" property="pname"/>
<result column="p_price" property="price"/>
<result column="p_date" property="pdate"/>
<result column="p_cid" property="cid"/>
</resultMap>
<!--引用映射关系-->
<select id="selectProductsCustomMapping" resultMap="product_resultMap">
select *from products
</select>
2. 关联查询
2.1 关联关系
两个表之间记录的对应关系,分为一对一和一对多,而多堆多则是三张表之间的关系,若掌握了两张表之间的一对多关系的处理,则多堆多也就不是问题了,因为本质上多对多就是两个一对多组成的
案例表:
这两张表之间存在一对一和一对多
站在user表角度来看 一个用户可能对应多个订单即一对一
站在order表角度来看 一个订单只能对应一个用户即一对多
POJO类:
Order.java
import java.util.Date;
public class Order {
private int id,user_id;
private String number;
private Date createtime;
private String note;
private User user;
//get/set....
}
User.java
import java.util.Date;
import java.util.List;
public class User {
private int id;
private String name;
private Date birthday;
private String sex,address;
private List<Order> orders;
public User() {
}
//get/set....
}
2.2 一对一映射
需求:根据订单编号查询订单信息以及用户名称和地址
sql语句为:
select o.*,u.username,u.address
from orders o join kuser u
on o.user_id = u.id
where o.id = 8;
#要查询的字段根据需求来定,这里只需要查询用户的姓名和地址
OrdersMapper.xml:
<!--自定义映射-->
<resultMap id="order_resultMap" type="com.kkb.pojo.Order">
<!--orders表映射-->
<id column="id" property="id"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property="note"/>
<!--关联的user表映射-->
<association property="user" javaType="user">
<id column="user_id" property="id"/>
<result column="username" property="name"/>
<result column="address" property="address"/>
</association>
</resultMap>
<select id="selectOrderByID" parameterType="int" resultMap="order_resultMap">
select
o.*,u.username,u.address
from
orders o join kuser u
on
o.user_id = u.id
where
o.id = #{oid};
</select>
测试代码:
@Test
public void test(){
SqlSession session = factory.openSession();
OrdersMapper mapper = session.getMapper(OrdersMapper.class);//记得提供映射接口
Order order = mapper.selectOrderByID(8);
System.out.println(order);
session.close();
}
补充:当连接查询出现重复字段时如:两个表都有id字段,MyBatis简单的取第一个匹配的字段值,很多时候这是不正确的,我们可以给这个重复的字段取个别名来避免,像这样:
<select id="selectOrderByID" parameterType="int" resultMap="order_resultMap">
select
u.*,o.*,o.id oid
from
orders o join kuser u
on
o.user_id = u.id
where
o.id = #{oid};
</select>
理所当然的 在resultMap中则使用oid来进行映射
<id column="oid" property="id"/>
2.3 一对多映射
需求:根据用户编号查询用户信息以及用户所有订单信息
sql语句为:
select u.*,o.*,o.id oid
from kuser u left join orders o
on o.user_id = u.id
where u.id = 1;
UserMapper.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="com.kkb.mapper.UserMapper">
<!--自定义映射-->
<resultMap id="user_resultMap" type="user" autoMapping="true">
<result column="username" property="name"/>
<collection property="orders" ofType="order" autoMapping="true">
<result column="oid" property="id"/>
</collection>
</resultMap>
<select id="selectUserByID" parameterType="int" resultMap="user_resultMap">
select
u.*,o.*,o.id oid
from
kuser u left join orders o
on
o.user_id = u.id
where
u.id = #{uid}
</select>
</mapper>
autoMapping=”true” 将自动映射字段名与属性名能对应的字段,我们只需要添加对应不上的即可
测试代码:
@Test
public void test2(){
SqlSession session = factory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUserByID(1);
System.out.println(user);
session.close();
}
注意:无论如何在collection标签中必须至少存在一个手动映射字段否则,将不会合并重复的主记录(user) 按照官方的说法,建议将手动映射id字段,可提高整体性能:去看看
另外collection标签中 ofType用于指定元素的类型 javaType指定容器类型
2.4嵌套映射,select
当关联查询非常复杂时,可以用嵌套的select,其原理是在映射复杂数据时执行另一个select来完成
<resultMap id="order_resultMap2" type="Order" autoMapping="true">
<id column="id" property="id"/>
<!-- 指定嵌套查询 column是传给内层查询的参数 -->
<association property="user" column="user_id" select="selectUserByUid" javaType="user"/>
</resultMap>
<!-- 外层查询-->
<select id="selectOrderByID2" parameterType="int" resultMap="order_resultMap2">
select
*
from
orders
where
id = #{id}
</select>
<!-- 嵌套查询-->
<select id="selectUserByUid" parameterType="int" resultType="user">
select *from kuser where id = #{id}
</select>
这种方式同样适用于一对多的关联关系
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>