目的:最近学习了mybatis框架的使用,所以写个博客用来记录mybatis动态代理学习中的问题以及感悟,本博客中的项目是基于mybatis动态代理高级查询的demo
对应的sql语句也放在了resources目录下:项目地址
目录
一、为什么我们要使用mybatis?
之前我们操作数据库都是使用的Jdbc连接池,而Jdbc存在着各种各样的问题。
而mybatis却能更好的解决这些问题,mybatis的特点又有哪些呢?
- 支持自定义SQL、存储过程、及高级映射
- 实现自动对SQL的参数设置
- 实现自动对结果集进行解析和封装
- 通过XML或者注解进行配置和映射,大大减少代码量
- 数据源的连接信息通过配置文件进行配置
可以发现,MyBatis是对JDBC进行了简单的封装,帮助用户进行SQL参数的自动设置,以及结果集与Java对象的自动映射。
二、mybatis整体架构
1.配置文件
全局配置文件(核心配置文件):mybatis-config.xml,作用:配置数据源(配置数据库连接信息),引入映射文件
映射文件:XxMapper.xml,作用:配置sql语句、参数、结果集封装类型等
2.SqlSessionFactory
作用:获取SqlSession
通过newSqlSessionFactoryBuilder().build(inputStream)来构建,inputStream:读取配置文件的IO流
3.SqlSession
作用:执行CRUD操作
它是线程不安全的。
4.Executor
执行器,SqlSession通过调用它来完成具体的CRUD
它是一个接口,提供了两种实现:缓存的实现、数据库的实现
5.Mapped Statement
在映射文件里面配置,包含3部分内容:
具体的sql,sql执行所需的参数类型,sql执行结果的封装类型
参数类型和结果集封装类型包括3种:
HashMap,基本数据类型,pojo
三、Mapper动态代理快速入门
Mapper接口的动态代理实现,需要遵循以下规范:
- 映射文件中的命名空间(名称空间)与Mapper接口的全路径一致
- 映射文件中的statement的Id与Mapper接口的方法名保持一致
- 映射文件中的statement的ResultType必须和mapper接口方法的返回类型一致(即使不采用动态代理,也要一致)
- 映射文件中的statement的parameterType必须和mapper接口方法的参数类型一致(不一定,该参数可省略)
3.1 在pom.xml中添加mybatis的依赖
注意:每次添加依赖后都要刷新一下。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demon.ibatis</groupId>
<artifactId>ibatis</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
<build>
<plugins>
<!--用来设置jdk版本的-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 编写pojo实体类
实体类中的属性名需要对应sql中的属性字段名,遇到sql中的字段名有下划线可以使用驼峰命名法命名。
3.3 创建配置文件
在resources下创建mybatis、jdbc和log4j的全局配置文件:
编写mybatis配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--引入jdbc配置文件-->
<properties resource="jdbc.properties"/>
<settings>
<!--开启自动驼峰命名规则-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--全局开启延迟加载,开启时所有关联对象都会延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<!--指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean-->
<typeAliases>
<package name="com.demon.pojo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--引入mapper映射文件,编写一个mapper映射文件就需要在这里引入一次-->
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
configuration配置:
编写顺序一定要严格按照:propertes属性、settings设置、typeAliases类型命名、typeHandlers类型处理器、objectFactory对象工厂、plugins插件、environments环境、databaseIdProvier数据库厂商标志、mappers映射器
-
properties 属性
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
-
settings 设置
设置参数 | 描述 | 有效值 | 默认值 |
cacheEnabled | 该配置影响的所有映射器中配置的缓存的全局开关。 | true | false | true |
lazyLoadingEnabled | 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。 | true | false | false |
aggressiveLazyLoading | 当启用时,带有延迟加载属性的对象的加载与否完全取决于对任意延迟属性的调用;反之,每种属性将会按需加载。 | true | false | true |
multipleResultSetsEnabled | 是否允许单一语句返回多结果集(需要兼容驱动)。 | true | false | true |
useColumnLabel | 使用列标签代替列名。不同的驱动在这方面会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。 | true | false | true |
useGeneratedKeys | 允许 JDBC 支持自动生成主键,需要驱动兼容。如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 | true | false | false |
autoMappingBehavior | 指定 MyBatis 是否以及如何自动映射指定的列到字段或属性。NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。FULL 会自动映射任意复杂的结果集(包括嵌套和其他情况)。 | NONE, PARTIAL, FULL | PARTIAL |
defaultExecutorType | 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements);BATCH 执行器将重用语句并执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
defaultStatementTimeout | 设置超时时间,它决定驱动等待数据库响应的秒数。 | Any positive integer | Not Set (null) |
safeRowBoundsEnabled | 允许在嵌套语句中使用行分界(RowBounds)。 | true | false | false |
mapUnderscoreToCamelCase | 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 | true | false | false |
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 | SESSION | STATEMENT | SESSION |
jdbcTypeForNull | 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER |
lazyLoadTriggerMethods | 指定哪个对象的方法触发一次延迟加载。 | A method name list separated by commas | equals,clone,hashCode,toString |
defaultScriptingLanguage | 指定动态 SQL 生成的默认语言。 | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags. XMLDynamicLanguageDriver |
callSettersOnNulls | 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。注意原始类型(int、boolean等)是不能设置成 null 的。 | true | false | false |
logPrefix | 指定 MyBatis 增加到日志名称的前缀。 | Any String | Not set |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | Not set |
proxyFactory | 为 Mybatis 用来创建具有延迟加载能力的对象设置代理工具。 | CGLIB | JAVASSIST | CGLIB |
-
typeAliases 类型命名
编写jdbc配置文件:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/user
jdbc.username=root
jdbc.password=root
编写log4j配置文件:
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
### 存放log日志的文件路径
log4j.appender.file.File=E:/workspace/ibatis1/src/main/resources/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=debug, stdout,file
3.4 编写Mapper接口
public interface UserMapper {
/**
* 通过订单编号20140921001查询order并延迟加载user
* @param orderNumber 订单编号
* @return
*/
Order queryOrderUserLazy(@Param("orderNumber")String orderNumber);
/**
* 多对多查询
* @param orderNumber 订单编号
* @return
*/
Order queryOrderAndUserAndOrderDetailsAndItemByOrderNumber(@Param("orderNumber") String orderNumber);
/**
* 一对多查询
* @param orderNumber 订单编号
* @return
*/
Order queryOrderAndUserAndOrderDetailsByOrderNumber(@Param("orderNumber") String orderNumber);
/**
* 一对一查询
* @param orderNumber 订单编号
* @return
*/
Order queryOrderAndUserByOrderNumber(@Param("orderNumber") String orderNumber);
}
3.5 在resources目录下创建mapper映射文件
Mapper映射文件中定义了操作数据库的sql,每一个sql都被包含在一个statement中。映射文件是mybatis操作数据库的核心。
编写UserMapper映射文件
<?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:名称空间,由于映射文件可能有多个,为了防止crud语句的唯一标识被重复,需要设置空间名称。
-->
<mapper namespace="com.demon.mapper.UserMapper">
<!--
select:查询的statement(声明),用来编写查询语句
id:语句的唯一标识,与mapper接口中的方法名保持一致
resultType:配置返回的结果集类型,如没配置typeAliases则需要编写全路径名
parameterType:传递的参数类型,可以省略
-->
<!--通过Order延迟加载User-->
<resultMap id="orderUserLazyResultMap" type="Order">
<!--
select属性:调用指定sql语句来执行延迟加载
column属性:延迟加载的sql语句中所需的参数
-->
<association property="user" javaType="User" select="queryUserByIdOfOrder" column="{id=user_id}"/>
</resultMap>
<!--通过订单编号查询订单-->
<select id="queryOrderUserLazy" resultMap="orderUserLazyResultMap">
select * from tb_order where tb_order.order_number = ${orderNumber}
</select>
<select id="queryUserByIdOfOrder" resultType="User">
select * from tb_user WHERE tb_user.id = ${id}
</select>
<!--多对多查询-->
<resultMap id="orderAndUserAndOrderDetailsAndItemResultMap" type="Order" autoMapping="true">
<id column="id" property="id"/>
<association property="user" javaType="User" autoMapping="true">
<id column="uid" property="id"/>
</association>
<collection property="orderdetails" javaType="List" ofType="Orderdetail" autoMapping="true">
<id column="detail_id" property="id"/>
<!--订单详情和商品的一对一的关系-->
<association property="item" javaType="Item" autoMapping="true">
<id column="iid" property="id"/>
</association>
</collection>
</resultMap>
<select id="queryOrderAndUserAndOrderDetailsAndItemByOrderNumber" resultMap="orderAndUserAndOrderDetailsAndItemResultMap">
SELECT
*, u.id as uid, od.id as detail_id, i.id as iid
FROM
tb_order o
INNER JOIN tb_user u ON o.user_id = u.id
INNER JOIN tb_orderdetail od ON o.id = od.order_id
INNER JOIN tb_item i ON od.item_id = i.id
WHERE
o.order_number = ${orderNumber}
</select>
<!--一对多查询-->
<resultMap id="orderAndUserAndOrderDetailsResultMap" type="Order" autoMapping="true">
<id column="id" property="id"/>
<association property="user" javaType="User" autoMapping="true">
<id column="uid" property="id"/>
</association>
<collection property="orderdetails" javaType="List" ofType="Orderdetail" autoMapping="true">
<id column="detail_id" property="id"/>
</collection>
</resultMap>
<select id="queryOrderAndUserAndOrderDetailsByOrderNumber" resultMap="orderAndUserAndOrderDetailsResultMap">
SELECT
*, u.id as uid, od.id as detail_id
FROM
tb_order o
INNER JOIN tb_user u ON o.user_id = u.id
INNER JOIN tb_orderdetail od ON o.id = od.order_id
WHERE
o.order_number = ${orderNumber}
</select>
<!--
配置自定义结果集
id属性:自定义结果集的唯一标识
type属性:结果集类型
autoMapping属性:多表查询时,必须设置为true,Order对象和tb_order表的属性和字段才会进行自动映射
-->
<resultMap id="orderAndUserResultMap" type="Order" autoMapping="true">
<!--配置Order的主键映射-->
<id column="id" property="id"/>
<!--
association标签:用于对一的映射
property属性:类中的关联属性的名称
javaType属性:属性对应的类型
autoMapping属性:autoMapping属性:多表查询时,必须设置为true,User对象和tb_user表的属性和字段才会进行自动映射
-->
<association property="user" javaType="User" autoMapping="true">
<id column="uid" property="id"/>
</association>
</resultMap>
<!--一对一查询-->
<select id="queryOrderAndUserByOrderNumber" resultMap="orderAndUserResultMap">
SELECT
*, u.id as uid
FROM
tb_order o
INNER JOIN tb_user u ON o.user_id = u.id
WHERE
o.order_number = ${orderNumber}
</select>
</mapper>
3.5.1 CRUD标签
1、Select
Select标签:用来编写查询语句的statement
id属性:唯一标识
resultType:查询语句返回的结果集类型
parameterType:参数类型,使用动态代理之后,需要和mapper接口中的参数类型一致,可以省略。
<!--
select:编写查询语句
id:语句的唯一标识
resultType:配置返回的结果集类型
parameterType:插入语句的参数类型,可以省略
-->
<select id="queryUserById" resultType="User">
select *,user_name as userName from tb_user where id = #{id}
</select>
2、Insert
Insert标签:编写新增语句的statement
insert:编写插入语句
id:插入语句的唯一标识
parameterType:插入语句的参数类型,使用动态代理之后,需要和mapper接口中的参数类型一致,可以省略。
useGeneratedKeys:开启主键自增回显,将自增长的主键值回显到形参中(即封装到User对象中)
keyColumn:数据库中主键的字段名称
keyProperty:pojo中主键对应的属性
<!--
insert:编写插入语句
id:插入语句的唯一标识
parameterType:插入语句的参数类型,可以省略。
useGeneratedKeys:开启主键自增回显,将自增长的主键值回显到形参中(即封装到User对象中)
keyColumn:数据库中主键的字段名称
keyProperty:pojo中主键对应的属性
-->
<insert id="insertUser" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
INSERT INTO tb_user (
user_name,
password,
name,
age,
sex,
birthday,
created,
updated
)
VALUES
(
#{userName},
#{password},
#{name},
#{age},
#{sex},
#{birthday},
now(),
now()
);
</insert>
3、Update
Update标签:编写更新语句的statement
id:语句的唯一标识
parameterType:语句的参数类型,使用动态代理之后,需要和mapper接口中的参数类型一致,可以省略。
<!--
update:更新的statement,用来编写更新语句
id:语句的唯一标识
parameterType:语句的参数,可以省略
-->
<update id="updateUser">
UPDATE tb_user
SET user_name = #{userName},
password = #{password},
name = #{name},
age = #{age},
sex = #{sex},
birthday = #{birthday},
updated = now()
WHERE
(id = #{id});
</update>
4、Delete
delete标签:编写删除语句的statement
id:语句的唯一标识
parameterType:语句的参数类型,使用动态代理之后,需要和mapper接口中的参数类型一致,可以省略。
<!--
delete:删除的statement,用来编写删除语句
id:语句的唯一标识
parameterType:语句的参数,可以省略
-->
<delete id="deleteUserById">
delete from tb_user where id = #{id};
</delete>
5、参数问题
当mapper接口要传递多个参数时,有两种传递参数的方法:
- 默认规则获取参数{arg0,arg1,param1,param2}
- 使用@Param注解指定参数名
方法一:
<select id="login" resultType="User">
select * from tb_user where user_name = #{arg0} and password = #{arg1}
</select>
方法二:
/**
* 用户登陆
* @param userName
* @return
*/
User login(@Param("userName") String userName,@Param("password") String password);
<!--
通过参数的固定名称传递参数:param1,param2,param3依次类推
-->
<select id="login" resultType="User">
select * from tb_user where user_name = #{userName} and password = #{password}
</select>
#{}和${}的区别总结:
#{}:
- 是预编译
- 编译成占位符
- 可以防止sql注入
- 自动判断数据类型
- 一个参数时,可以使用任意参数名称进行接收
${}:
- 非预编译
- sql的直接拼接
- 不能防止sql注入
- 需要判断数据类型,如果是字符串,需要手动添加引号。
- 一个参数时,参数名称必须是value,才能接收参数。
6、resultMap
- resultMap标签的作用:自定义结果集,自行设置结果集的封装方式
- id属性:resultMap标签的唯一标识,不能重复,一般是用来被引用的
- type属性:结果集的封装类型
- autoMapping属性:操作单表时,不配置默认为true,如果pojo对象中的属性名称和表中字段名称相同,则自动映射。
3.6 编写测试类
public class UserMapperTest {
private UserMapper userMapper;
@Before
public void setUp() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//将openSession方法中传递true,可以设置自动提交事务,就不需要使用sqlSession.commit方法提交事务了
SqlSession sqlSession = sqlSessionFactory.openSession(true);
userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void queryOrderUserLazy() {
Order order = userMapper.queryOrderUserLazy("20140921001");
System.out.println(JSON.toJSONString(order.getUser()));
}
@Test
public void queryOrderAndUserAndOrderDetailsAndItemByOrderNumber() {
Order order = userMapper.queryOrderAndUserAndOrderDetailsAndItemByOrderNumber("20140921001");
System.out.println(JSON.toJSONString(order));
}
@Test
public void queryOrderAndUserAndOrderDetailsByOrderNumber() {
Order order = userMapper.queryOrderAndUserAndOrderDetailsByOrderNumber("20140921001");
System.out.println(JSON.toJSONString(order));
}
@Test
public void queryOrderAndUserByOrderNumber() {
Order order = userMapper.queryOrderAndUserByOrderNumber("20140921003");
System.out.println(JSON.toJSONString(order));
}
}
四、延迟加载
- 延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
- 好处:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。。
- 坏处:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
4.1 需求
延迟加载需求:通过订单编号20140921001查询order并延迟加载user
如果通过订单编号查询order并且查询user信息,在正常情况下的查询语句应该是
如果改成延迟加载,也就意味着,先查询order,等需要的时候再去查询user,那就相当于将上面的一条语句变成了两条语句:
1、通过订单编号查询order
2、通过查询出来的order中的user_id查询user
4.2 编写接口方法
/**
* 通过订单编号20140921001查询order并延迟加载user
* @param orderNumber 订单编号
* @return
*/
Order queryOrderUserLazy(@Param("orderNumber")String orderNumber);
4.3 编写mapper映射
<!--通过Order延迟加载User-->
<resultMap id="orderUserLazyResultMap" type="Order">
<!--
select属性:调用指定sql语句来执行延迟加载
column属性:延迟加载的sql语句中所需的参数
-->
<association property="user" javaType="User" select="queryUserByIdOfOrder" column="{id=user_id}"/>
</resultMap>
<!--通过订单编号查询订单-->
<select id="queryOrderUserLazy" resultMap="orderUserLazyResultMap">
select * from tb_order where tb_order.order_number = ${orderNumber}
</select>
<select id="queryUserByIdOfOrder" resultType="User">
select * from tb_user WHERE tb_user.id = ${id}
</select>
4.4 测试
@Test
public void queryOrderUserLazy() {
Order order = userMapper.queryOrderUserLazy("20140921001");
System.out.println(JSON.toJSONString(order.getUser()));
}
测试结果:发现虽然已经将语句进行了分开查询,但是并没有延迟加载,还是一起查询了。
原因:没有开启延迟加载的开关
4.5 开启延迟加载
<settings>
<!--全局开启延迟加载,开启时所有关联对象都会延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
如果要使用Debug来演示延迟加载,需要设置如下:将自动toString的选项去掉就行。