Mapper代理
在上一个博客中,我们直接利用session和<select>
标签来执行sql的方式存在一些问题。
- session执行sql时都需要提供要执行sql的id,而这个id是字符串类型,意味着id是否正确在编译期间是无法获知的,必须等到运行时才能发现错误,
- sql需要的参数和返回值类都不明确,这也增加了出错的概率
我们最理想的方式应该像像调用方法一样调用sql,既避免了直接写id的问题,也可以明确指定方法的参数类型和返回值类型
Mybatis提供了动态代理的方式,来解决上面的问题:MyBatis中本来由Executor(被代理对象)来完成sql的执行,现在由代理对象(自动生成)来代理Executor完成,代理对象会将我们的操作转交给Executor
问题是:MyBatis怎么知道代理对象是什么样的对象呢?,这就需要为MyBatis提供Mapper接口,这个接口就是对mapper.xml中的sql语句的声明,与DAO层的接口类似。
我们继续上一篇博客的案例,先书写一个接口类
package mapper;
import Bean.Product;
public interface ProductsMapper {
Product selectProductById(int id);
}
注意我们的目录结构是这样的:
随后我们要记得修改ProductsMapper.xml
中的标签以关联我们的Mapper和我们的接口文件:<mapper namespace="mapper.ProductsMapper">
接着我们书写测试代码:
@Test
public void MapperTest(){
SqlSession session = factory.openSession(true);
//getMapper方法是Mybatis提供的可将接口进行实现的方法,能够返回接口的实现类
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
System.out.println(mapper.toString());
//这样我们在书写代码的时候就能提前知道我们是否代码书写错误(因为要和我们的接口匹配,不然编译不会通过)
Product product = mapper.selectProductById(3);
System.out.println(product);
}
这样我们就实现了动态代理,可以看出对象mapper就是一个代理对象。
注意事项:
- 必须保证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(映射器)
注意配置文件各个节点个层次是固定,需按照上述的顺序书写否则报错,
properties
properties可从配置文件或是properties标签中读取需要的参数,使得配置文件各个部分更加独立
同时呢,我们还可以将jdbc的配置信息写在独立的文件中,这样能够进一步降低耦合性。
我们可以配置jdbc.properties
位于resource包下
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql:///mybatisDB?serverTimezone=Asia/Shanghai&characterEncoding=utf8
user = root
password = 3692512
然后我们在这里引用
当内部和外部属性出现同名时,则优先使用外部的;
typeAliases
typeAliases用于为Java的类型取别名,从而简化mapper中类名的书写
动态SQL
在JDBC时代,我们通过Java代码来判断某个条件是否有效然后拼接SQL语句,这是非常繁琐的,MyBatis的动态SQL很好的解决了这个问题,使判断逻辑变得简洁直观:在Mapper中通过标签来完成动态SQL的生成
if
<!-- 根据姓名或cid进行搜索-->
<select id="selectByNameAndCid" parameterType="product" resultType="product">
select * from products where 1=1
<if test="name != null">
and name like ‘%${name}%‘
</if>
<if test="id != null">
and cid = #{id}
</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.selectByNameAndCid(condition);
System.out.println(product);
session.close();
}
其实从这里我们也能够看到,我们Mapper中写SQL的时候,传入参数实际要求的并不是太严格,只要传递的对象(无论是map ,list还是普通的对象)有相应的属性,mybatis就可以自动的去解析并寻找对应关系。
where
where的作用就是用于取出上面的where 1=1,因为这会让人看起来产生疑惑,其作用是将内部语句中的第一个and去除
<select id="selectByNameAndCid" parameterType="product" resultType="product">
select * from products
<where>
<if test="pname != null">
and name like ‘%${name}%‘
</if>
<if test="cid != null">
and cid = #{cid}
</if>
</where>
</select>
for each
当一个条件中中需要需要多个参数时则需要将多个参数拼接到一起,例如: in, not in
<!-- 动态 SQL for each -->
<select id="selectByIDs" parameterType="Bean.Product" resultType="Bean.Product">
<!-- select * from products where id in (1,2,3,4)-->
select * from products where id in
<foreach collection="ids" open="(" item="i" close=")" separator=",">
#{i}
</foreach>
</select>
<!--
<if test="ids != null"> 这里不仅判断属性是否为空还判断集合中是否有元素
foreache 标签属性说明:
强调:动态sql本质就是在拼接字符串,带着自己拼接sql的思路来编写动态sql会更好理解
collection 要遍历的集合
open 拼接的前缀
close 拼接的后缀
separator 拼接元素之间的分隔符
item 遍历得到的临时变量名
index 当前元素的索引(不常用)
-->
@Test
public void dynamicSQL_2(){
SqlSession session = factory.openSession(true);
ProductsMapper mapper = session.getMapper(ProductsMapper.class);
Product p = new Product( );
List list = new ArrayList();
list.add(2);
list.add(3);
list.add(4);
p.setIds(list);
List<Product> products = mapper.selectByIDs(p);
System.out.println(products);
}
set
set标签用于更新语句,当同时要更新多个字段时,我们需要留意当前是否是最后一个set,避免在后面出现,
符号,使用set标签后可自动去除最后的逗号(仅此而已)
<update id="updateByID" parameterType="Bean.Product">
update products
<set>
<if test="pname != null and pname != ‘‘">
name = #{name},
</if>
<if test="price != null and price > 0">
price = #{price},
</if>
<if test="pdate != null">
date = #{date},
</if>
<if test="cid != null and cid != ‘‘">
cid = #{cid},
</if>
</set>
where id = #{id}
</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.updateByID(product);
System.out.println(product);
session.commit();
session.close();
}
include
Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。其实就是替换了一下sql语句。
<!--提取片段-->
<sql id="fields">id,name,price,cid</sql>
<select id="includeTest" resultType="products">
select
<include refid="fields"/> <!-- 引用片段-->
from products
</select>
高级映射
在一些情况下数据库的记录和POJO对象无法直接映射,包括两种情形:
- 数据库字段与POJO字段名称不同(可以避免);
- 关联查询时,需要将关联表的数据映射为另一个类型的POJO(一对一),或List中(一对多);
在MyBatis中通过resultMap来完成自定义映射
自定义字段与属性的映射
如果我们表中某些字段的名称更改后,我们可以通过高级映射的方式实现查询(实际就是起了个别名,更多的应用在下面)
<!--自定义映射关系 id:该映射关系的标识 type:映射到的Bean类型此处为别名-->
<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>
其中 column 是查询结果返回中对应的哪个字段, property是mybatis返回Bean对象中的某个属性
关联查询
两个表之间对应关系,分为一对一和一对多,而多对多则是三张表之间的关系,若掌握了两张表之间的一对多关系的处理,则多对多也就不是问题了,因为本质上多对多就是两个一对多组成的(比如上图老师和学生的关系)
一对一映射
下面我们首先来看一个一对多的关联查询,我们有用户和订单两个表如下:
随后我们进行代码层面的书写,首先创建对应的Order和User两个Bean对象(对应的set和get方法就不写了):
package Bean;
import java.util.Date;
/**
* Created by Jeason Luna on 2020/6/21 21:52
*/
public class Order {
private int id , user_id;
private int number;
private Date createtime;
private String note;
private User user;
}
package Bean;
import java.util.Date;
import java.util.List;
/**
* Created by Jeason Luna on 2020/6/21 21:56
*/
public class User {
private int id;
private String username;
private Date birthday;
private int sex;
private String address;
}
然后书写代理接口:
package mapper;
import Bean.Order;
/**
* Created by Jeason Luna on 2020/6/21 21:58
*/
public interface OrderMapper {
Order selectByID(int id);
}
最后建立OrderMapper.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">
<!--namespace 用于多个Mapper出现相同的sql时区分不同包-->
<mapper namespace="mapper.OrderMapper">
<resultMap id="order_map" type="Bean.Order" autoMapping="true">
<!--association 用于一对一的映射-->
<id property="id" column="oid" />
<association property="user" javaType="Bean.User" autoMapping="true">
<id property="id" column="uid" />
</association>
</resultMap>
<select id="selectByID" parameterType="int" resultMap="order_map">
<!-- select * from orders where id = #{id}-->
SELECT * , orders.id oid , kuser.id uid FROM kuser JOIN orders ON kuser.id = orders.user_id
WHERE orders.id = #{id}
</select>
</mapper>
测试代码如下:
import Bean.Order;
import mapper.OrderMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by Jeason Luna on 2020/6/21 22:01
*/
public class test2 {
private SqlSessionFactory factory;
@Before
public void init() throws IOException {
//获取的工厂构造器
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//加载配置文件
InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
//获得会话工厂
factory = builder.build(stream);
}
@Test
public void test1(){
try(SqlSession session = factory.openSession(true)){
OrderMapper orderMapper = session.getMapper(OrderMapper.class);
Order order = orderMapper.selectByID(3);
System.out.println(order);
}
}
}
输出:
Order{id=3, user_id=1, number=113, createtime=Wed May 27 00:00:00 CST 2020, note=‘null‘, user=User{id=1, username=‘王建森‘, birthday=Sun Jun 21 00:00:00 CST 2020, sex=1, address=‘黑龙江‘}}
一对多映射
我们还是使用上面的那两个数据库的表,这次我们统计一个用户有多少的订单。
首先,我们增加Bean.User对象的属性:
public class User {
private int id;
private String username;
private Date birthday;
private int sex;
private String address;
private List<Order> orders;
}
创建相应的动态代理:
package mapper;
import Bean.User;
/**
* Created by Jeason Luna on 2020/6/22 10:42
*/
public interface UserMapper {
User selectByID(int id);
}
编写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">
<!--namespace 用于多个Mapper出现相同的sql时区分不同包-->
<mapper namespace="mapper.UserMapper">
<resultMap id="user_map" type="Bean.User" autoMapping="true">
<id column="uid" property="id"/>
<collection property="orders" javaType="list" ofType="Bean.Order" autoMapping="true">
<id column="oid" property="id"/>
</collection>
</resultMap>
<select id="selectByID" parameterType="int" resultMap="user_map">
select u.* ,o.* ,o.id oid, u.id uid
from kuser u join orders o
on o.user_id = u.id
where u.id = #{id}
</select>
</mapper>
注意:当我们使用collection
标签的时候,我们的内层和外层都需要指定相应的映射(自己指定自己也行),这是因为resultMap中如果不定义类似主键之类的能够区分每一条结果集的字段的话,会引起后面一条数据覆盖前面一条数据的现象。
如果我们不指定,Mybatis不会合并重复的主记录,进而报错如下(注释<id column="uid" property="id"/>
):
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 4
最后我们编写测试代码:
import Bean.Order;
import Bean.User;
import mapper.OrderMapper;
import mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by Jeason Luna on 2020/6/22 11:07
*/
public class test3 {
private SqlSessionFactory factory;
@Before
public void init() throws IOException {
//获取的工厂构造器
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//加载配置文件
InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
//获得会话工厂
factory = builder.build(stream);
}
@Test
public void test1(){
try(SqlSession session = factory.openSession(true)){
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectByID(2);
System.out.println(user);
}
}
}
得到输出如下:
User{id=2, username=‘张建森‘, birthday=Thu Jun 11 00:00:00 CST 2020, sex=2, address=‘吉林‘,
orders=[
Order{id=1, user_id=2, number=111, createtime=Wed Jun 10 00:00:00 CST 2020, note=‘null‘, user=null},
Order{id=7, user_id=2, number=167, createtime=Thu Nov 14 00:00:00 CST 2019, note=‘null‘, user=null},
Order{id=8, user_id=2, number=188, createtime=Sat May 30 00:00:00 CST 2020, note=‘null‘, user=null},
Order{id=9, user_id=2, number=199, createtime=Thu Feb 18 00:00:00 CST 2021, note=‘null‘, user=null}
]}
另外我们还可以使用子查询来实现上述功能,修改UserMapper.xml
如下
<resultMap id="user_map2" type="Bean.User" autoMapping="true">
<id column="id" property="id"/>
<collection property="orders" javaType="list" ofType="Bean.Order" select="selectOrderByUserID" column="id">
</collection>
</resultMap>
<select id="selectByID2" parameterType="int" resultMap="user_map2">
select * from kuser where id = #{id}
</select>
<select id="selectOrderByUserID" parameterType="int" resultType="Bean.Order">
select * from orders where user_id = #{id}
</select>
上面的代码中,property
指定数据要放在Bean对象的哪个属性中,javaType
是容器类型要和属性相对应,ofType
是容器内的元素类型,select
是子查询的id,column
是要传给子查询的参数。
这样我们就实现了子查询的功能,先利用selectByID2
来查询出id = 2(如果我们输入的参数是2的话)的用户信息,随后利用selectOrderByUserID
将刚才得到的用户信息中的id号码传入,得到对应订单表的所有该用户的数据,Mybatis会帮我们合并重复数据,这样就实现了子查询。