先做个科普吧,MyBatis是其3.0以后的叫法,之前的版本都叫iBatis,这个名字肯定听过吧,也是大名鼎鼎的Apache旗下的产品,后来易主谷歌,正式改名为MyBatis,代码迁到github上。它是一种持久化框架,和Hibernate一样都是ORM(Object Relation Mapping,对象关系映射)框架,他们需要完成的任务都是将JavaBean映射成数据库中的一条记录。
不管是什么框架,Java和数据库交互,内部肯定少不了jdbc的支持(所有的框架都是对原生JDBC方式的封装),但是之前接触的都是jdbc的小工具,而不是框架。jdbc本身sql语句都是写在java代码中的,逻辑很混乱,耦合度也很高,所以不好,这里先回顾一下jdbc操作数据的流程:
Hibernate是我学习的第一个ORM框架,觉得挺好用,后来接触到了MyBatis,由于追星的原因,我从来原来的SSH转成了SSM架构开发,网上很多大牛都说Hibernate不如MyBtais好用,不够灵活,我也不懂为什么这么说,但是那么多比我厉害的人都说它好用,那就用呗,反正我是不知道哪里好,今天终于好像有点懂了_。首先看一下Hibernate,它是一个全自动的ORM框架,它的工作流程如下:
Hibernate把jdbc的工作都做完了,连编写SQL这活儿都包了,其实挺好的,我就只要面向对象去撸代码就好了,而且Hibernate框架是全映射的ORM(啥意思,就是如果JavaBean有多少属性,它查询时候就直接将对应表中的属性字段全部查询出来,这里我想问一下懒加载这一块有没有反驳的理由),但是这样就带来一个问题,它的SQL的控制权没有留给开发人员带来方便(针对小项目)的同时也带来了麻烦。因为在写大的工程时,不可能不涉及SQL的优化问题,我们需要去自己写SQL,但是Hibernate的本质目的就是为了消除SQL语句,让不懂SQL的人员也可以实现增删改查,那怎么办,当然在Hibernate中也提供了HQL语言,通过HQL我们可以定制SQL语句,但是这个过程还是比较麻烦的,所以说Hibernate不灵活。
基于上述Hibernate的全封装弊端,我们希望SQL由我们自己写,所以MyBatis出现了,它和Hibernate最大的不同就是将SQL的控制权交给了我们自己(单独放到配置文件中),因为作为一个框架要是要有点逼格的,不能啥都不做吧(所以说MyBatis是一个半自动化框架),它的工作流程如下:
同理,首先给出MyBatis官网教程,这个是最好的老师,同时,在MyBatis的压缩包中有一个pdf文件也是文档教程。好了,开始搞第一个Demo吧,本文中所有Mybatis的栗子默认都是基于当前最新版本MyBatis-3.4.6。
public class Employee {
private int id;
private String lastName;
private String gender;
private String email;
//getter and setter as well as toString
}
mybatis-config.xml
<configuration>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="921228jack"/>
dataSource>
environment>
environments>
<mappers>
<mapper resource="conf/EmployeeMapper.xml"/>
mappers>
configuration>
因为配置了log4j日志,所以这里需要额外写一个log4j的配置文件log4j.xml
:
<log4j:configuration debug="true">
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n"/>
layout>
appender>
<logger name="java.sql">
<level value="debug"/>
logger>
<logger name="org.apache.ibatis">
<level value="info"/>
logger>
<root>
<level value="debug"/>
<appender-ref ref="STDOUT"/>
root>
log4j:configuration>
EmployeeMapper.xml
,里面封装了每一条SQL语句,并将其配置到全局配置文件中:<?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 namespace="com.hhu.entity.Employee">
<!--id:唯一标识,就是方法名
resultType:返回值类型,即执行sql后的返回值想要怎么处理,封装成什么对象
#{id}:从方法传入的参数“id”获取传入sql语句的参数
-->
<select id="selectEmp" resultType="com.hhu.entity.Employee">
<!--这里是查不到lastName属性的,MyBatis默认根据JavaBean中的属性名的方式去查找
数据库,如果属性和字段名对不上,那么就查不到;
解决方法:在mapper文件属性名和字段名不一样地方,用JavaBean中的属性名做该字段的别名,
不用通配符*,即可
-->
select id, last_name lastName, gender,email from tbl_employee where id = #{id}
</select>
</mapper>
/**
* 1. 根据xml配置文件创建一个SqlSessionFactory,再由此创建sqlSession,通过它可以实现MyBatis对数据库的增删改查,每一次用完后一定要将其关闭;
* 2. sql映射文件(即mapper文件),配置每一条sql语句和其封装规则
* 3. 将sql映射文件配置到全局配置文件mybatis-config.xml
*/
@Test
public void testSqlSession() throws IOException {
//读取配配置文件(这是一MyBatis的核心配置文件,也是一个全局配置文件)
String resource = "conf/mybatis-config.xml";
//注意这里的Resources是apach包下面的类
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取sqlSession,它可以执行已经映射的SQL语句(即mapper中的文件)
SqlSession sqlSession = sqlSessionFactory.openSession();
//注意这里使用try-finally,不管如何,最后一定要将sqlSession关闭
try{
//第一个参数是唯一标识,第二个参数是执行SQL传入的参数
Employee employee = sqlSession.selectOne("selectEmp", 2);
System.out.println(employee);
} finally {
//关闭Session
sqlSession.close();
}
}
在前一小节,使用MyBatis的方式有些古老,正常情况我们都是通过在Dao层的接口直接去关联的mapper的sql映射文件,而不是通过想selectOne
类似指定唯一标识符的方式去执行sql,而且这个接口我们不需要有实现类,下面看下MyBatis更为常见的接口使用方式。
public interface EmployeeDao {
public Employee getEmployeeById(int id);
}
<?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 namespace="com.hhu.dao.EmployeeDao">
<!--id:唯一标识,就不是随意写了,需要写成绑定接口中对应的方法名
resultType:返回值类型,即执行sql后的返回值想要怎么处理,封装成什么对象
#{id}:从方法传入的参数“id”获取传入sql语句的参数
-->
<select id="getEmployeeById" resultType="com.hhu.entity.Employee">
<!--这里是查不到lastName属性的,MyBatis默认根据JavaBean中的属性名的方式去查找
数据库,如果属性和字段名对不上,那么就查不到;
解决方法:在mapper文件属性名和字段名不一样地方,用JavaBean中的属性名做该字段的别名,
不用通配符*,即可
-->
select id, last_name lastName, gender,email from tbl_employee where id = #{id}
</select>
</mapper>
@Test
public void testInterface() throws IOException {
//从配置文件的输入流读取配置创建SessionFactory
InputStream is = Resources.getResourceAsStream("conf/mybatis-config.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);
//打开一个会话
SqlSession sqlSession = sessionFactory.openSession();
try {
//这里虽然是获取的是接口类型的类,但是一旦将接口和sql映射文件动态绑定后
//MyBatis会自动为接口创建代理对象
EmployeeDao employeeDao = sqlSession.getMapper(EmployeeDao.class);
//查看获取对象的类型,可以发现是一个代理对象,可以通过这个代理对象实现增删改查
System.out.println(employeeDao.getClass());
Employee e = employeeDao.getEmployeeById(1);
System.out.println(e);
} finally {
sqlSession.close();
}
}
经过上述的工作就完成了,在学习过Hibernate和MyBatis之后发现两者都有SqlSession,并且都是通过它来操作数据库,也就是说通过这个对象可以获取Connection对象。
【注意】SqlSession和Connection一样都是非线程安全的,在多线程环境下不能将它们作为成员变量放在外面!比如:
public class TestSqlSessionFactory {
private SqlSession sqlSession = null;
@Test
public void test1() {...}
@Test
public void test1() {...}
@Test
public void test1() {...}
}
上面的写法就是有问题的,如果多线程都获取了这个SqlSession,A线程操作完直接关闭了,那其他线程怎么办,对吧,所以每次都要去获取显得实例。
mybatis-config.xml
) mybatis-config.xml
是MyBatis的全局配置文件,也是其核心配置文件,上面是以xml的形式来搞的,当然我们也可以不去搞这个文件,而是通过其他的方式,具体参见官方文档。关于MyBatis的全局配置文件需要写的东西基本都涵盖在了
和标签中,下面着重对这个标签中的各个子标签做说明。
这里重要的地方再说一遍,被坑过啊,MyBatis的配置文件中标签必须按照指定的顺序去写,不按顺序写将直接报错!!
正确的标签顺序应该依次为:properties
,settings
,typeAliases
,typeHandlers
,objectFactory
,objectWrapperFactory
,reflectorFactory
,plugins
,environments
,databaseIdProvider
,mappers
,下面看一下关于其中一些常用标签的用法
properties
标签 这个标签见名知意,很明显是用于配置文件的,在MyBatis这个标签是用来引入外部资源文件的,通常会将数据库的连接配置单独拎出来写一个db.properties
文件,下面的是一个小栗子:
<properties resource="db.properties">properties>
在properties
除了resource
标签外,还有一个url
标签,两者作用:
这里在写的时候还要注意,我是将所有的配置文件单独放在了src一个文件下,在IDEA需要将这个文件夹标记成Resources
才能这么写,否则前面要加上路径
settings
标签这个标签很重要,它可以直接影响MyBatis运行时的行为,里面也有很子标签,具体参见文档,下面是一个关于下划线驼峰命名的设置栗子:
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
settings>
typeAliases
标签 这个标签是用来给JavaBean
起别名(在mapper
文件中的别名),在mapper
映射文件中,对于返回实体类的SQL语句,之前是写JavaBean
的全类名,现在就可以直接写在此标签的中别名,用法如下:
<typeAliases>
<typeAlias type="com.hhu.entitieses.Employee" alias="employee"/>
typeAliases>
上述是对单个JavaBean
起别名,如果数量比较多的话,这样的方式倒也是很麻烦,对某个包中以及子包中的JavaBean起别名可以使用package
标签进行配置,如下:
<typeAliases>
<package name="com.hhu.entities" />
typeAliases>
在使用package
批量起别名的时候可能遇见这样的情况,父类包和子类包中含有同名的JavaBean,这就比较尴尬了,MyBatis中提供使用注解的方式来解决这个这个问题,只要在有冲突的JavaBean的类上使用@Alias("别名")
注解为该JavaBean
重新起一个没有冲突的别名即可。
typeHandlers
标签它是类型处理器标签,是架起Java类型和数据库数据类型的桥梁,比如将java中String类型如何保存到数据库兼容的varchar或者char,又比如将数据库中查出的int类型转成java的Integer或者int类型。
objectFactory
标签这个标签我们一般不动,使用MyBatis默认的就挺好。
plugins
标签插件标签也是其中比较重要的标签,MyBatis可以通过插件对SQL语句进行拦截。
environments
标签 用于配置MyBatis的运行环境(我理解下来就是数据连接的配置),通过default
属性指定使用的环境(值为环境的唯一标识符id
),总体结构如下:
<environments default="dev_mysql">
<environment id="dev_mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
.
.
.
dataSource>
environment>
environments>
在environments
标签中的environment
标签可以配置多种MyBatis的运行环境,每个environment
标签中必须配置事务管理器transactionManager
和数据源dataSource
才算完整:
transactionManager
:事务管理器,通过type
属性指定事务管理器(正常为JDBC
),在MyBatis中主要有三种:JDBC和MANAGED以及自定义事务管理器
databaseIdProvider
标签MyBatis可以根据不同的底层数据库的类型执行不同的SQL语句,其实就是MyBatis对于移植性的考虑,Hibernate在这一块做的特别好,因为SQL语句不需要我们自己写,都是由Hibernate自动帮我们发送SQL语句的,但是MyBatis可以根据这个标签指定我们在SQL映射文件使用的SQL语句是基于哪种数据库去写的,从而达到MaBatis根据不同的数据库动态的发送SQL。通过这个标签可以配置不同数据库底层的标识符,MyBatis根据这个标识符执行不同的SQL语句,主要体现MyBatis和数据库底层的解耦,一般的配置如下:
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="SQL Server" value="sql server"/>
databaseIdProvider>
由于是不同的数据库,所以很容易就能想到需要配置其他的数据的环境吧,也就是说这个标签需要和前面的environments
标签结合使用,另外mapper文件中在写SQL语句的时候需要使用databaseId
属性指定使用的是哪种数据库厂商(它的值就是这里配置的各个数据库厂商和别名,比如MySQL
我们自定义的别名为mysql
),同时SQL映射文件中也要为不同的数据库的查询方式写不同的SQL语句的实现,下面以MySQL和Microsoft SQL Server两种数据库为例,看一下这个标签的用法:
mssql-jdbc-6.4.0.jre8.jar
employees
表,也有Employee
对象的属性,然后在db.properties
配置文件中加入MSSQL的连接配置:# MySQL Configuration--db_mybatis
jdbc.userName=root
jdbc.password=9j
jdbc.url=jdbc:mysql://localhost:3306/db_mybatis
jdbc.driver=com.mysql.jdbc.Driver
# MicroSoft SQL Server--SchoolServer
mssqlserver.userName=sa
mssqlserver.password=9
mssqlserver.url=jdbc:sqlserver://localhost:1433;DatabaseName=SchoolServer
mssqlserver.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
environments
和databaseIdProvider
标签:<environments default="dev_mysql">
<environment id="dev_mysql">
<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>
<environment id="dev_mssql">
<transactionManager type="JDBC">transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${mssqlserver.driver}"/>
<property name="url" value="${mssqlserver.url}"/>
<property name="username" value="${mssqlserver.userName}"/>
<property name="password" value="${mssqlserver.password}"/>
dataSource>
environment>
environments>
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sql server"/>
databaseIdProvider>
databaseId
指定数据库厂商的别名:
<select id="getEmployeeById" resultType="employee" databaseId="mysql">
select * from tbl_employee where id = #{id}
select>
<select id="getEmployeeById" resultType="employee" databaseId="sql server">
select * from employees where id = #{id}
select>
然后运行面向接口的测试代码,通过控制台发现MyBatis发送的是mysql
的SQL语句,然后通过更改environments
标签的default
属性值为default="dev_mssql"
,再次运行,控制台打印的是MSSQL的语句。
【注意】这里除了在select
标签中使用databaseId
属性来指定SQL映射的哪种版本的数据库以外,还可以使用MyBatis内置参数_databaseId
和结合if
标签完成上面的工作,因为上述的方式必须要写两个select
标签,并不符合程序员偷懒的好习惯,改造一下如下:
<select id="getEmployeeById" resultType="employee">
<if test="_databaseId=='mysql'">
select * from tbl_employee where id = #{id}
if>
<if test="_databaseId=='sql server'">
select * from employees where id = #{id}
if>
select>
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sql server"/>
databaseIdProvider>
mappers
标签通过该标签可以将SQL映射文件注册到全局配置文件中,用法如下:
<mappers>
<mapper resource="EmployeeMapper.xml"/>
mappers>
当然在其子标签mapper
是注册各个SQL映射文件的,这个子标签下主要有3个属性:
resource
:引用类路径下的SQL映射文件;url
:引用网络路径或者磁盘路径下的SQL映射文件;class
:直接引用接口。前两种不作太多说明,主要关于第三种class
属性做说明,如果直接引用接口,需要将接口和接口绑定的SQL映射文件必须在同一路径下并且两者的同名;但是更常规的做法是利用注解直接将SQL写在接口中的方法身上而不写mapper文件了,然后引用接口的类,比如:
@Select("select * from tbl_employee where id=#{id}")
Employee getEmployeeById(int id);
但是又不太推荐这么去写,因为MyBatis好不容易将java代码和SQL语句分离开来,现在又给他搞进去,确实不太好,而且不利于复杂SQL语句后期的优化,但是又因为用注解的方式实在太方便了,所以可以将简单的、不太重要的SQL直接用注解去搞,复杂的SQL写在xml中。
上面是单个SQL映射文件的注入,同时MyBatis也提供了批量映射文件的注册,只用如下的方式:
<mappers>
<package name="包名"/>
mappers>
但是这种适用于注解的方式,如果在xml的形式,就需要将SQL的映射文件和绑定的接口类放到同一包路径下并且同名。
xxmapper.xml
SQL映射文件是MyBatis的核心文件之一,主要用于SQL语句的存放和接口的绑定。在一开始的Demo中就涉及到了关于mapper
文件的一些写法,该文件中的主体是
标签,里面还有其他的一些常用的标签:
cache
:配置接口的缓存cache-ref
:引用其他接口的缓存resultMap
:描述从数据库中加载出来的结果集parameterMap
:已废弃sql
:一个可重用的SQL语句,可以被其他语句引用;insert
:插入操作的SQL映射;update
:更新操作的SQL映射;delete
:删除操作的SQL映射;select
:查询操作的SQL映射;下面先对其中用的最多的增删改查标签搞一个小栗子:
<mapper namespace="com.hhu.dao.EmployeeDao">
<select id="getEmployeeById" resultType="com.hhu.entities.Employee">
select id, last_namemm lastName, gender,email from tbl_employee where id = #{id}
select>
<update id="upDateById" parameterType="employee">
UPDATE tbl_employee SET last_NameMM=#{lastName}, gender=#{gender},email=#{email}
WHERE id = #{id}
update>
<delete id="deleteById">
DELETE FROM tbl_employee WHERE id = #{id}
delete>
<insert id="saveEmp" parameterType="employee">
INSERT INTO tbl_employee(last_nameMM,gender,email)VALUES
(#{lastName}, #{gender}, #{email})
insert>
mapper>
在看完常用的增删改查的操作后,再来看下sql
标签,他是用来抽取可重用的SQL语句元素,有点类似于模板的意思,一般都和include
一起出现,就是一处定义,处处可引用,看一下小栗子:
<sql id="insertSql">
last_NameMm, gender, email
sql>
<insert id="insertTest">
INSERT INTO tbl_employee(
<include refid="insertSql">include>
) VALUES (#{lastName}, #{gender}, #{email})
insert>
同时在include
标签中可以自定属性,反过来在sql
中使用,注意使用形式${属性}
,比如:
<sql id="insertSql">
last_NameMm, gender, email, ${testProperty}
sql>
<insert id="insertTest">
INSERT INTO tbl_employee(
<include refid="insertSql">
<property name="testProperty" value="abc" />
include>
) VALUES (#{lastName}, #{gender}, #{email}, #{testABC})
insert>
那么最后拼接出来的SQL语句为:
INSERT INTO tbl_employee(last_NameMm, gender, email, abc)
VALUES (?,?,?,?)
如果希望写入操作的方法返回boolean
,mapper
中正常写,接口中指明返回类型,比如public boolean saveEmp(Employee employee);
在返回的时候MyBatis会直接将SQL语句影响的行数转成boolean
类型的返回值。在跑测试案例的时需要手动提交sqlSession
,上面案例中我们过去Session
的方式是sessionFactory.openSession()
,这种方式获取的Session
是不会自动提交的,关闭Session
前,必须先进行session.cmoit()
的执行,之前不写是因为只是用来对数据库进行读操作,但凡涉及到数据库的写操作就必须进行提交,否则MySQL只是发送SQL语句,但是这个SQL不会落实到数据库上对其造成影响,但是可以通过sessionFactory.openSession(true)
的方式获取可以自动提交的Session
。
在实际开发中,保存一条记录时,可能想要获取它的id
,由于id
在数据库中设置的是自增,所以一般不会传进去,比如下面的:
Employee employee = new Employee();
employee.setLastName("testInsert2");
employee.setEmail("[email protected]");
employee.setGender(0);
System.out.println(employeeDao.saveEmp(employee));
System.out.println("新增主键:" + employee.getId());
//涉及到写操作的行为必须手动提交,否则不会提交,读操作到没有什么影响
sqlSession.commit();
就这样去做,获取的新增主键为0,必须在mapper文件对应的标签设置使用主键生成器,并且将这个生成的id
绑定到JavaBean
中的某个属性上,在mapper文件使用useGeneratedKeys="true"
开启此功能,使用keyProperty="属性名"
将获取的主键绑定到javaBean的属性上即可:
<insert id="saveEmp" parameterType="employee" useGeneratedKeys="true" keyProperty="id">
INSERT INTO tbl_employee(last_nameMM,gender,email)VALUES
(#{lastName}, #{gender}, #{email})
insert>
再次测试上面的案例即可获取到新增id
的值;
上面一小节是通过数据库将主键设置为自增的方式,id
属性可以直接不传入,但是对于不支持id
自增的数据库(比如Oracle数据库),怎么来做呢,比如id
通过序列的方式生成,那么怎么来获取id
。传入的保存记录对象照样不用传id
属性,我们在SQL语句上做文章,下面以序列id
为例看一下MyBatis是如何完成插入操作的:
首先到数据库中查询用户的序列表:select * from user_sequences;
,可以查看用户所有的序列表的,然后选择对应表的序列表,这里以表employees
主键对应的序列表EMPLOYEES_SEQ
为例,然后xml中如下:
<insert id="saveEmp" databaseId="oracle">
<selectKey keyProperty="id" order="BEFORE">
SELECT EMPLOYEES_SEQ.nextval FROM dual
selectKey>
INSERT INTO employees(id,last_name,gender,email) VALUES
(#{id},#{lastName}, #{gender}, #{email})
insert>
MyBatis中对于参数的处理,有多种情况,一一做说明:
#{参数名}
的形式,比如:public Employee getEmployeeById(int id);
那在mapper中写的映射语句为:
<select id="getEmployeeById" resultType="employee">
select * from tbl_employee where id=#{id}
select>
其实这里只有一个参数,不管你接口方法中参数名是什么,在mapper文件中可以任意写,甚至可以瞎写,比如上述的映射语句可以写成:
<select id="getEmployeeById" resultType="employee">
select * from tbl_employee where id=#{hh}
select>
只不过写成接口方法中的参数名好理解些。
Map
),其中Map
的Key
为依次为param1
、param2
、param3
……以此类推,接口方法中传入几个参数,value
就是调用接口时传入的具体内容,就这样封装,然后在mapper中获取其中的属性时就用#{Key名}
的发方式来获取封装好的value
,这里首先做一个说明,MyBatis封装的多参数Map的名字叫做_parameter
,这也是MyBatis的内置参数,在mapper文件中可以直接使用它来判断诸如传入的多参数对象(这个Map对象可以是MyBatais自动封装的,也可以是一个JavaBean对象)是否为空,比如下面的:public Employee getByIdAndLastName(int id, String lastName);
然后在mapper
文件中获取:
<select id="getByIdAndLastName" resultType="employee">
SELECT * FROM tbl_employee e WHERE e.id=#{param1} AND e.last_namemm=#{param2}
select>
但是在实际开发中,如果有多个参数,以param1
、param2
……这种方式出现在SQL语句中确实不能起到见名知意的效果,所以这里可以在接口方法参数前面使用@Param("别名")
注解,这样就可以指定MyBatis在封装Map
时的Key
,最后在mapper中取参数的时候就可以用我们自己指定的别名来获取,而不用param1
、param2
这种方式来获取,比如下面:
public Employee getByIdAndLastName(@Param("id") int id, @Param("lastName") String lastName);
mapper中获取
<select id="getByIdAndLastName" resultType="employee">
SELECT * FROM tbl_employee e WHERE e.id=#{id} AND e.last_namemm=#{lastName}
select>
对于多个参数的处理,除了使用的上面MyBatis自动封装的Map
,我们还可以手动自己封装Map
,比如上面的getByIdAndLastName
方法,其实他没有必要封装,因为传入的参数比较少,使用MyBatis自动封装就挺好,这里纯粹是为了演示手动封装参数的方法而为之:
public Employee getByMap(Map map);
mapper中:
<select id="getByMap" resultType="employee">
SELECT * FROM tbl_employee WHERE id=#{id} and last_namemm=#{lastName}
select>
调用测试:
Map<String, Object> map = new HashMap();
map.put("id", 6);
map.put("lastName","jack");
Employee e = employeeDao.getByMap(map);
System.out.println(e);
【注意】这里的手动封装的Map中的key
值就是我们在mapper中使用的别名。当然如果传入的参数刚好完全是JavaBean
的几个属性(如果id
可以不带id
),那么接口传入参数可以直接传入JavaBean对象,在mapper中获取的它的属性时,直接使用#{属性名}
的方式来获取即可。
除了上述的注意的地方还有几个需要注意的点:
/*MyBatis自动封装,mapper获取方式:
* id: #{id}或者#{param1}
* name: #{param2}
*/
public Employee getEmp(@Param("id")int id, String name);
/*MyBatis自动封装,mapper获取方式:
* id: #{param1}
* name: #{param2.name}而不是直接用#{name}
*/
public Employee getEmp(int id, Employee e);
/*MyBatis对Collection(List、Set)和数组有特殊的封装方式,
* 虽然也是封装到Map中,但是Map的Key的名字变了,可以统一
* 使用“collection”,或者:
* List使用“list”,如果取list的首个元素可以使用#{list[0]}
* 数据使用“array”
*/
public Employee getById(List<Integer> ids);
在SQL映射文件中,我们通常是以#{}
形式来获取传入的参数,但是在MyBatis中也是可以用${}
的形式来取值的,两者虽然都能达到我们所需要的结果,但是差异还是很大的。
#{}
是以预编译的形式将参数设置到sql语句中,PreparedStatement
可以防止sql注入;${}
是以拼接sql语句的形式直接将参数搞到SQL语句上,有安全问题。比如举个栗子:
<select id="getByMap" resultType="employee">
SELECT * FROM tbl_employee WHERE id=${id} and last_namemm=#{lastName}
select>
我们将参数封装好调用接口,从控制台的输出可以看到MyBatis的发送的SQL语句如下:
SELECT * FROM tbl_employee WHERE id=6 and last_namemm=?
和上面所说的一样,${}
取值id
的参数直接拼接到了SQL语句上,而#{}
取值的lastName
则是以占位符?
的形式在SQL语句中,所以一般情况下我们都是使用#{}
。
【注意】那还有一种取值方式在哪种场景下使用呢:原生JDBC不支持占位符的地方我们就必须用${}
进行SQL语句的拼接了(比如数据库的表名,根据某个字段排序,里面的“字段”和排序的方式名都是不支持占位符的),他就是不支持占位符的,那此时就必须使用${}
的方式了,使用’#{}'会报错,比如公司的员工的薪资表按照“年份_salary”的方式命名的,我们需要动态的查询薪资表的记录(有时查2016_salary
有时需要查2018_salary
表),此时我们可以这样写:
public Salary getSalary(int year,String user);
在mapper中就可以这么写:
<select id="getSalary" resultType=salary>
select * from ${param1}_salary where username=#{param2}
select>
调用接口getSalary(2015,"jack")
此时MyBatis发送的SQL语句为:
select * from 2015_salary where username=?
是完美符合我们的要求的。
null
的情况 在MyBatis中,如果方法参数传入null
,那么它会自动应映射为jdbcType类中的OTHER(Types.OTHER)
的形式即OTHER
类型,大部分数据库是可以识别的(比如MySQL、Sql Server),但是在有些数据库不能识别MyBatis处理null
的OTHER
类型(比如Oracle数据库就不能识别),这个时候就需要在传值的时候指定如果参数传入为null
,MyBatis应该将它转成JDBCType中什么类型,正常情况传入的null
转成NULL
都是可以被识别的(Oracle和MySQL以及MSSQL)。有两种用法如下:
null
值转换的处理:
<insert id="saveEmp" parameterType="employee" databaseId="oracle">
INSERT INTO employees(last_name,gender,email) VALUES
(#{lastName}, #{gender}, #{email, jdbcType=NULL})
insert>
中进行设置,这是针对所有的null
设置,而不是像第一种方法对某个字段进行null
的处理:<settings>
<setting name="jdbcTypeForNull" value="NULL"/>
settings>
可以如果配置的话建议直接用第二种方式,将全局null
值在转换的时候转成NULL
而不是OTHER
。
在接口的查询的方法中会有返回值,也有可能没有返回值,返回值可以是一个基本数据对象,也可以是一个对象(pojo对象直接指定为设定的别名),下面对接口中不同方法的不同返回值的类型的处理做一个探讨;在mapper映射文件中存在返回值的只有标签(除了MyBatis将增、删、改三种操作影响的行数转成
boolean
类型的返回值),返回值可以用标签中的
resultType
属性指定(指定为别名或者全类名)或者resultMap
自定义封装,如果返回的是集合类型,那么指定为集合中元素的类型,注意resultType
属性不能和resultMap
属性同时使用。
比如返回的是一个List
对象,据上所述,可以如下:
public List<Employee> getEmps();
在mapper中的返回值类型需要指定为List
集合中存放元素的类型,这个方法的映射就是Employee
这个JavaBean的实体类型。
<select id="getEmps" resultType="employee">
select * from tbl_employee
select>
如果接口的返回值类型是Map
类型,这里需要注意一下,如果返回值就是由一条记录里面所有属性都单独封装起来的多个Map
,此时key
就是列名,value
就是对应的属性值,比如下面的:
public Map<String, Object> getEmpByIdReturnMap(int id);
mapper文件中为:
<select id="getEmpByIdReturnMap" resultType="map">
SELECT * FROM tbl_employee WHERE id=#{id}
select>
最后调用接口返回值为Map
类型,JavaBean的每个属性在数据库表中对应的列名都被MyBatis封装到Map
中,Key
为属性名,value
就是查询出来的对应属性值,上面是将一个JavaBean对象的各个对应表的字段名封装到多个Map
中。但是更好的做法是直接将javaBean作为一个对象封装到一个Map
中,而不是将各个字段单独封装成多个Map
,就像Map
的形式,以Employee
的主键id
作为Map的Key
,如下:
//这要需要指定Map的key是什么,这里是id,当然也可以是其他属性
//需要注意下key的唯一性
@MapKey("id")
public Map<Integer, Employee> getEmps();
mapper文件:
<select id="getEmps" resultType="employee">
SELECT * FROM tbl_employee
select>
返回值除了上述的resultType
指定返回值类型外(基本用于MyBatis提供的自动封装),还可以使用一个非常重要的resultMap
属性指定自定义结果集。如果JavaBean的属性名和数据库对应的列名不一致,MyBatis是无法自动封装的,出现这种情况,一般有以下几种方法:
select last_name lastName
);
),前提是满足条件,如:数据库C_Action==>JavaBean中的cAtion;resultMap
来自定义返回集,指定Employee中的属性对应数据库中的哪个字段。 在开始之前先把resultMap
标签中注意点说一下,和
标签一样,resultMap
下的子标签必须按照一定的顺序排列,否则报错,正确顺序如下:constructor?
,id*
,result*
,association*
,collection*
,discriminator?
。
下面具体看一下resultMap
的用法:
//接口中的方法照常写
public Employee getEmployeeById(int id);
mapper文件中使用自定义类型:
<resultMap id="MyEmp" type="employee">
<id column="id" property="id"/>
<result column="last_nameMm" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
resultMap>
<select id="getEmployeeById" resultMap="MyEmp">
select * from tbl_employee where id = #{id}
select>
在实际开发中会遇到不少级联属性的查询,小栗子(Employee
和Department
,Employee
中持有Department
对象),
第一部分以多对一的关联关系为例,直接看下两个JavaBean:
public class Employee {
private int id;
private String lastName;
private int gender;
private String email;
private Department dep;
//getter and setter as well as toString(no dep)
}
public class Department {
private int dId;
private String dpName;
//getter and setter as well toString
}
数据库的表结构为:tbl_departments
和tbl_employee
这里主要是针对在查询Employee
对象时怎么将它关联的Department
属性查询出来。
看一下具体的实现,第一种方式,自定义封装返回类型,mapper中如下:
<resultMap id="EmpWithDep" type="employee">
<id column="id" property="id"/>
<result column="last_nameMn" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<result column="d_id" property="dep.dId"/>
<result column="dp_Name" property="dep.dpName"/>
resultMap>
<select id="getEmpAndDpById" resultMap="EmpWithDep">
SELECT *
FROM tbl_employee e, tbl_departments d
WHERE d.d_id = e.department AND e.id = #{id}
select>
这样以后直接调用getEmpAndDpById(id)
方法即可获取Employee
以及它关联的Department
对象。
第二种方式,使用association
标签在封装ResultMap
时将dep
属性和Department
对象关联:
<resultMap id="EmpWithDep2" type="employee">
<id column="id" property="id"/>
<result column="last_nameMn" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<association property="dep" javaType="department">
<result column="d_id" property="dId"/>
<result column="dp_Name" property="dpName"/>
association>
resultMap>
使用的时候向上面的用法一样使用即可。
除了上述的方式,还有一种利用association
标签进行分步查询的实现方式,步骤多一些,看一下完整的实现流程:先直接根据员工id
获取员工信息,再根据员工信息里面的dep
属性(虽然写的是Department dep
,实则查询出来是关联的id
)去获取Department对象属性,将复杂的级联查询SQL拆分为两个简单的SQL。
首先提供一个Department
根据它的id
获取对象的方法,所以这里直接给Department
这个对象提供一个接口(实际开发中一般都会用到),mapper文件如下(接口自己脑补下):
<mapper namespace="com.hhu.dao.DepDao">
<select id="getDepById" resultType="department">
SELECT * FROM tbl_departments WHERE d_id=#{id}
select>
mapper>
注意写完了要将mapper文件注册到全局配置文件中mybatis-config.xml
,然后在到EmployeeMapper.xml
搞一下:
<resultMap id="EmpStep" type="employee">
<id column="id" property="id"/>
<result column="last_nameMm" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
<association property="dep"
select="com.hhu.dao.DepDao.getDepById" column="department">
association>
resultMap>
<select id="getEmpAndDepByStep" resultMap="EmpStep">
SELECT * from tbl_employee WHERE id=#{id}
select>
这样就将一开始级联查询SQL语句分解成两个简单的SQL语句进行分步查询。
【注意】在做分步查询时,可以在上述的association
标签中使用fetchType
标签指定级联属性是否进行懒加载,fetchType="lazy"
表示进行懒加载,fetchType="eager"
表示不进行懒加载,注意fetchType
可以覆盖全局配置文件中对全局级联属性懒加载的设置,在哪个级联属性上加了fetchType
只改变当前的级联属性,其他地方的级联属性还是跟全局走。
第二部分在第一部分的基础上增加一个一对多的新需求,即在对Department查询时也想将该部门下的员工也一起查询出来,基于上面的方式,我们也依次来看一下实现:
首先在Department类里面添上员工集合吧,不然怎么查询对吧:
//改造原有的Department类添加如下的属性
private List<Employee> employees;
// add getter and setter
对应的Department
的mapper文件自定义结果集,注意集合属性用collection
标签:
<resultMap id="DepWithEmp" type="department">
<id column="d_id" property="dId"/>
<result column="dp_Name" property="dpName"/>
<collection property="employees" ofType="employee">
<id column="id" property="id"/>
<result column="last_nameMm" property="lastName"/>
<result column="gender" property="gender"/>
<result column="email" property="email"/>
collection>
resultMap>
<select id="getDepPlusById" resultMap="DepWithEmp">
SELECT * FROM tbl_departments d
LEFT JOIN tbl_employee e ON d.d_id = e.department
WHERE d.d_id = #{id}
select>
上面这样就可以用了,经过上面多对一的情况,这里的一对多也不难想到分步查询的方式,同样是可以的(分步查询时可以使用延迟加载,这里还是比较推荐这种方式的),思路还是和上面的类似:我先查询Department
的信息,然后利用查询出来Department
信息中的did
属性去Employee
对应的表中根据did去查询所有员工信息。好,看具体实现:
首先在员工中提供一个根据部门id查询员工的方法:
public List<Employee> getAllEmployeeByDep(int id);
比较简单,在mapper中映射SQL:
<select id="getAllEmployeeByDep" resultType="employee">
select id, last_nameMm lastName, gender,email from tbl_employee WHERE department=#{id}
select>
前提做好了,就去Department
的mapper中做具体的查询工作了:
<resultMap id="DepWithEmpsStep" type="department">
<id column="d_id" property="dId"/>
<result column="dp_Name" property="dpName"/>
<collection property="employees" select="com.hhu.dao.EmployeeDao.getAllEmployeeByDep"
column="d_id">collection>
resultMap>
<select id="getDepPlusByIdStep" resultMap="DepWithEmpsStep">
SELECT * FROM tbl_departments WHERE d_id=#{id}
select>
这就号完成了一对多关联关系的分步查询了。
【注意】
collection
标签中同样可以使用fetchType
标签指定级联属性是否进行懒加载,fetchType="lazy"
表示进行懒加载,fetchType="eager"
表示不进行懒加载,注意fetchType
可以覆盖全局配置文件中对全局级联属性懒加载的设置,在哪个级联属性上加了fetchType
只改变当前的级联属性,其他地方的级联属性还是跟全局走。association
标签做分步,还是级联属性是集合用collection
标签做分步,在调用第二步接口方法查询级联属性时需要的参数都是一个,使用column
标签将第一步查询结果中所需要的参数列名写进入,但是如果这个接口参数有多个的情况下,可以使用map的形式进行封装(用“key
=value
”的形式表示,key
随便取,就是在接口方法SQL映射调用处就用这个名字,value
就是传入参数的列名),比如column={dId=d_id, lastName=last_NameMm}
【思考】这里多对一关联关系情况下做分步查询情况下查询“一方”时使用的是association
标签,在一对多情况下做分步查询情况查询“多方”时使用的是collection
标签。这是很多老师和网友说的情况,实则我在跑测试Demo的时候,发现居然可以混用,这一点让我有些迷糊,标记一下。
在上面刚提过级联属性的查询问题,关于级联属性这一块自然又可以想到懒加载问题,这一块本来是应该放到上一大章节里,但是放在级联属性这一块很和谐。比如在上面的栗子中,我们查询Employee
的时候会同时查询到它里面的Department
属性,但是懒加载就是想如果我只用Employee
里面除dep
外的任意属性,我完全没有必要去加载它关联的Department
的对象呀。是的基于这种需求,懒加载需要使用association
标签实现的分步查询稍加配置就可以实现了(很好理解,分两步,我要什么就发对应的SQL语句去查询获取,如果是整体的级联SQL语句,根本无法实现懒加载啊)。
基于分步查询的方式配置好了之后,只需要在全局配置文件中mybatis-config.xml
配置开启懒加载和禁用侵入懒加载即可:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
settings>
这里我在测试的时候,遇到特别诡异的情况,报出了如下异常信息
org.apache.ibatis.builder.BuilderException: The setting LazyLoadingEnabled is not known.
无赖啊,搞了半天,拼写没错,复制的官方文档上的,没管它,晚上异常就消失了( ̄▽ ̄)"。
在设置过全局配置文件后,我们在做分步查询的时可以实现懒加载了,可以通过association
标签和collection
标签中的fetchType
属性进行局部级联属性懒加载的更改。
discriminator
在MyBatis中允许用户对返回值使用resultMap
标签进行自定义封装,这里有种需求,就是根据查询的结果进行不同的返回结果集封装,比如上述的栗子中,在查询员工的时候根据性别属性有额外要求:
好了,对于上述两中不同的封装行为我们做实现:
<resultMap id="EmpGen" type="employee">
<id column="id" property="id"/>
<result column="gender" property="gender"/>
<result column="last_NameMm" property="lastName"/>
<discriminator javaType="int" column="gender">
<case value="0" resultType="com.hhu.entities.Employee">
<result column="email" property="email"/>
<association property="dep" select="com.hhu.dao.DepDao.getDepById" column="department"/>
case>
<case value="1" resultType="com.hhu.entities.Employee">
<result column="last_NameMm" property="email"/>
case>
discriminator>
resultMap>
<select id="getEmpGen" resultMap="EmpGen">
SELECT * FROM tbl_employee WHERE id=#{id}
select>
在做上述案例的时候,不知道怎么回事,IDEA一直报别名异常,前面所有SQL映射全部失效,重新搞了个SQL映射文件才搞定。
【注意】在discriminator
标签前面的不管是男人还是女人都会被封装,唯一不同的就是鉴别器discriminator
中的属性会根据指定列的属性值的不同作不同的封装行为,这里男人、女人有封装行为差异的就是email
的属性和是否查询所在部门两种行为。
动态SQL是MyBatis提供的一项重大特性,如果你使用过jdbc或者类型的框架,一定了解拼接SQL语句的痛苦,有时候还要根据传入的参数条件变换不同的SQL语句(比如传入4个参数就需要拼接4个,传入5个就要拼接5个),而且哪些地方少个空格或其他字符就会有SQL语句的异常,MyBatis为此提供了动态SQL的功能,其中动态SQL标签有:
if
choose
(when
, otherwise
)trim
(where
,set
)foreach
if
标签 首先来看if
标签,在如下场景:我传给接口方法有几个合法参数,那么对应映射的SQL语句就拼接几个条件,比如前面章节的Employee
对象,为了方便测试,将它所有的属性的类型全部改为String
类型,那么正常来说,用原生jdbc来做会非常麻烦,因为不确定哪个属性存在、哪个属性不存在,这里可以利用MyBatis提供的if
标签来做这个需求:
接口方法
/*带啥条件就往Employee类型的参数中封装,
* 明白为啥要改造JavaBean的属性为String了吧
*/
public List<Employee> getEmpsByConditionIf(Employee employee);
mapper映射的SQL
<select id="getEmpsByConditionIf" resultType="employee">
SELECT * FROM tbl_employee WHERE
<if test="id!=null">id=#{id}if>
<if test="lastName!=null and lastName!=''">AND last_nameMm=#{lastName}if>
<if test="gender==0 or gender==1">AND gender=#{gender}if>
<if test="email!=null and email.trim()!=''">AND email=#{email}if>
select>
上述的SQL语句可以实现体条件拼接,注意if
的语法,只有符合test
属性中表达式条件的时候才会将内部的SQL拼接上去,但是这样拼接如果id为空会有一个问题,SQL语句为
SELECT * FROM tbl_employee WHERE and ……
这明显不太好,哪有WHERE
后面直接跟AND
的,SQL有语法错误,AND不合法,这里有两种解决方案:
1=1
的条件,后续所有的条件追加AND ...
即可,这样不管是有没有id
属性,都是合法的,就变成了SELECT * FROM tbl_employee WHERE 1=1
,然后再追加if
标签即可,如下:<select id="getEmpsByConditionIf" resultType="employee">
SELECT * FROM tbl_employee WHERE 1=1
<if test="id!=null">AND id=#{id}if>
<if test="lastName!=null and lastName!=''">AND last_nameMm=#{lastName}if>
<if test="gender==0 or gender==1">AND gender=#{gender}if>
<if test="email!=null and email.trim()!=''">AND email=#{email}if>
select>
标签来包裹即可(此时注意将之前SQL语句中的WHERE
关键字去除),MyBatis会自动帮我处理掉像上述情况多出来的AND这类关键字(只会去除每个一条件句首的多余的AND关键字,如果将AND放在每个条件的句末仍然可能会出现语法错误),改造后如下:<select id="getEmpsByConditionIf" resultType="employee">
SELECT * FROM tbl_employee
<where>
<if test="id!=null">id=#{id}if>
<if test="lastName!=null and lastName!=''">AND last_nameMm=#{lastName}if>
<if test="gender==0 or gender==1">AND gender=#{gender}if>
<if test="email!=null and email.trim()!=''">AND email=#{email}if>
where>
select>
trim
标签 基于上述的案例,我们继续来说,在使用where
标签或者加1=1
条件可以解决id
没有传的情况语法错误,这个都是条件SQL中AND
写在前面的,万一我就是犯贱,我就像写在后面:
<select id="getEmpByTrim" resultType="employee">
SELECT * FROM tbl_employee
<where>
<if test="id!=null">id=#{id} AND if>
<if test="lastName!=null and lastName!=''">last_NameMm=#{lastName} AND if>
<if test="gender==0 or gender==1">gender=#{gender} AND if>
<if test="email!=null and email!=''">email=#{email}if>
where>
select>
此时用where
标签无法解决问题,它只能处理每个条件SQL句首的AND
标签,如果查询条件不带email
属性,SQL语句也是有语法问题的,MyBatis为有这样强迫症的同学提供了trim
标签,来看一下它是如何满足这群迫症的:
<select id="getEmpByTrim" resultType="employee">
SELECT * FROM tbl_employee
<trim prefix="WHERE" prefixOverrides="" suffix="" suffixOverrides="and">
<if test="id!=null">id=#{id} AND if>
<if test="lastName!=null and lastName!=''">last_NameMm=#{lastName} AND if>
<if test="gender==0 or gender==1">gender=#{gender} AND if>
<if test="email!=null and email!=''">email=#{email}if>
trim>
select>
注意各个参数是针对的拼接后的trim
标签中的整体,而不是针对的单个条件SQL。
choose
标签 分支选择,有点类似与switch-case
的用法,比如下面有一种查询场景,JavaBean中带了哪一个属性就按照哪个属性条件去查,而不是想上面拼接很多,这里有且仅带一个属性:
<select id="getEmpsByChoose" resultType="employee">
SELECT * FROM tbl_employee WHERE
<choose>
<when test="id!=null">id=#{id}when>
<when test="lastName!=null">last_NameMm=#{lastName}when>
<when test="gender==0 or gender==1">gender=#{gender}when>
<when test="email!=null and email.trim()!=''">email=#{email}when>
<otherwise>1=1otherwise>
choose>
select>
【注意】这里面choose
标签只有一个when
标签生效!!就是说最后MyBatis拼接出来的SQL语句最后只带一个条件,如果同时有多个条件生效,那么在choose
标签中越靠前的优先级别越高!
set
标签 上面的几个标签都是查询时候的标签,但是这里的set
标签是用于修改时候的标签。在前面章节更新Employee
对象的时候,是带所有字段进行更新的,但是在动态SQL中显然是探讨的未知字段的更新(即不知道要跟新哪些字段),可能想到前面根据if
标签,自然想到这么去搞:
<select id="updateEmployee">
UPDATE tbl_employee SET
<if test="lastName!=null and lastName.trim()!=''">last_nameMm=#{lastName},if>
<if test="gender==0 or gender==1">gender=#{gender},if>
<if test="email!=null and email.trim()!=''">email=#{email}if>
WHERE id=#{id}
select>
这样以后现然可以动态发送SQL了,比如:
UPDATE tbl_employee SET last_nameMm=?,gender=?,email=? WHERE id=?
这个全字段,和之前引入
标签时的情况一样,如果只有前面的字段没有最后的email
字段就会出现类似于gender=?,
这样尴尬的状态,多了一个,
,出现SQL语法错误,联系之前的标签不难想到trim
标签,难后对其进行改造,思路正确,如下:
<select id="updateEmployee">
UPDATE tbl_employee SET
<trim suffixOverrides="," suffix="" prefixOverrides="" prefix="">
<if test="lastName!=null and lastName.trim()!=''">last_nameMm=#{lastName},if>
<if test="gender==0 or gender==1">gender=#{gender},if>
<if test="email!=null and email.trim()!=''">email=#{email}if>
trim>
WHERE id=#{id}
select>
如果被tirm
包裹的整体条件语句出现后缀,
,那么就将它去掉,这是一种解决思路。在MyBatis中,它还提供了一个set
标签来帮助我们解决在动态更新字段时候出现多余,
的尴尬,和where
标签如出一辙,记得拿到之前手写的SET
,如下:
<select id="updateEmployee">
UPDATE tbl_employee SET
<set>
<if test="lastName!=null and lastName.trim()!=''">last_nameMm=#{lastName},if>
<if test="gender==0 or gender==1">gender=#{gender},if>
<if test="email!=null and email.trim()!=''">email=#{email}if>
set>
WHERE id=#{id}
select>
foreach
标签 好了,跋山涉水终于看到了我们最后一个标签foreach
标签了,顾名思义,就是用于遍历集合的标签,遍历的都是跟批量操作绑在一起的,批量操作的话又分为好几种场景:批量查询、批量更新、批量插入、批量删除。下面我们就按照上述的几种场景来看一下foreach
标签的用法。
第一种场景:批量查询。这里搞一个场景,我想根据id
集合批量查询员工,传入的参数是一个员工id
的集合,那么查询的时候SQL语句是想如下这样的写:
SELECT * FROM tbl_employee
WHERE id IN (1,2,3,4)
主要是看(1,2,3,4)
这个玩意儿这么写,好,具体来看语法:
接口方法:
/* 批量查询,传入的参数是一个集合,注意集合在MyBatis中封装方式
* 集合类型的参数MyBatis自动将它封装到一个Map中,key就叫collection
* (根据不同的集合类型还有其他的名字,比如List类型的参数还可以叫list),
* 当然我们也可以在接口方法的传入参数上使用@Param("ids")指定该集合的封
* 装为Map时候的key的名字为ids,在mapper文件使用对应的key的名字获取参数
* /
public List getBatchEmployee(List ids);
对应的SQL映射文件:
<select id="getBatchEmployee" resultType="employee">
SELECT * FROM tbl_employee WHERE id IN
<foreach collection="collection" item="id" separator="," open="(" close=")">
#{id}
foreach>
select>
第二种场景:批量插入。首先看下MySQL批量插入语法:
INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
("jack","0","[email protected]"),("air","1","[email protected]")
可以看出来MySQL中批量插入可以通过,
的分隔符一次插入多条记录,那么在mapper文件中实现就可简单了:
接口方法:
//批量插入
public void batchInsert(List<Employee> employees);
映射的SQL语句:
<insert id="batchInsert">
INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
<foreach collection="collection" item="e" separator=",">
(#{e.lastName}, #{e.gender}, #{e.email})
foreach>
insert>
注意涉及写操作的SQL需要在最后sqlSession.commit()
否则不能提交到数据库。
这是一种方式,有了foreach
标签,其实我一开始想到的我直接发送多条SQL语句(因为我开始只知道单个记录的插入语法,并不知道还可以用上面的方式来批量插入),插入几条记录就发几条SQL,这是我作为一个头脑简单的禽兽想到的插入方式,这种方式也是我们所推荐的,比如:
INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES (?,?,?);
INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES (?,?,?);
是吧,我们来实现:
<insert id="batchInsert">
<foreach collection="collection" item="e" separator=";">
INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
(#{e.lastName}, #{e.gender}, #{e.email})
foreach>
insert>
但是遗憾的是在MySQL默认是不支持这种一次发送多条SQL的查询方式,我们需要修改这个属性为true
:allowMultiQueries=true
,我们在db.properties
配置连接地址的时候带上这个参数即可:
jdbc.url=jdbc:mysql://localhost:3306/db_mybatis?allowMultiQueries=true
好了,做完这里的更改就可以大胆跑了。
第三种批量删除和第四种批量更新,结合批量插入的第二种方式我们很容易就可以实现了,这里不做赘述。
同样的,缓存机制作为一个持久层框架几乎是必不可少的,它可以提高查询速度和系统运行速率,MyBatis也提供了强大的缓存功能,并且它默认定义两级缓存(一级缓存和二级缓存)。
首先来看一级缓存,和Hibernate一样,MyBatis默认就是开启一级缓存的(并且我们无法手动关闭),即Session
缓存,在MyBatis叫SqlSession
,同一个SqlSession
查询同一个数据库对象的话,第二次及以后的查询结果都是从MyBatis的一级缓存中获取结果,而不是再从数据库中查询一次,来看测试代码:
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
Employee e2 = mapper.getEmployeeById(1)
System.out.println(e2);
//还记得==号是干嘛的吗?对喽,比较的是两者内存地址
System.out.println(e1==e2);
控制台输出:
DEBUG 03-27 08:48:35,462 ==> Preparing: SELECT * FROM tbl_employee WHERE id = ? (BaseJdbcLogger.java:159)
DEBUG 03-27 08:48:35,524 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:159)
DEBUG 03-27 08:48:35,559 <== Total: 1 (BaseJdbcLogger.java:159)
Employee{id=1, lastName='jacksonary', gender=0, email='[email protected]'}
Employee{id=1, lastName='jacksonary', gender=0, email='[email protected]'}
true
所以瞄一眼就大家就都懂了。很神奇对吧,和Hibernate中一样,我们来看一下什么样的情况会让一级缓存失效呢,主要有如下的四种情况:
SqlSession
,因为一级缓存是SqlSession
级别的,所以在重新实例化一个SqlSession
去获取mapper对象操作数据库就会使上个Session
缓存失效,怎么可能不失效,都不在一个频道上,就像小明家今天吃肉和小红有啥关系(●ˇ∀ˇ●),所以此时两者是不同的,好来看代码:EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
sqlSession.close();
sqlSession = sqlSessionFactory.openSession();
EmployeeDao mapper2 = sqlSession.getMapper(EmployeeDao.class);
Employee e2 = mapper2.getEmployeeById(1);
System.out.println(e2);
System.out.println(e1==e2);
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
Employee e2 = mapper.getEmployeeById(2);
System.out.println(e2);
System.out.println(e1==e2);
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
mapper.deleteById(7);
Employee e2 = mapper.getEmployeeById(2);
System.out.println(e2);
System.out.println(e1==e2);
SqlSession
的缓存:EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
sqlSession.clearCache();
Employee e2 = mapper.getEmployeeById(2);
System.out.println(e2);
System.out.println(e1==e2);
MyBatis中二级缓存又称为全局缓存,基于namespace
级别的缓存,一个namespace
对应于一个二级缓存。它的工作机制如下:
首先一个会话查询一个对象后,这个对象就会被放入这个SqlSession
的一级缓存中,如果这个SqlSession
关闭了(注意这里是关闭,不是重新实例化,两者有区别,只要SqlSession
不关,里面的缓存对象就不会移到二级缓存中),MyBatis会将这个SqlSession
中缓存的对象移到二级缓存中(如果这个对象配置了二级缓存),下次实例化新的SqlSession
的时候,getMapper
获取的是同一个映射SQL文件的mapper.xml
,由于二级缓存是基于namespace
的,所以再次获取同一个Mapper的时候,可以从二级缓存中拿到缓存对象。
下面我们来看一下二级缓存的配置方式:
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
标签中加一个
标签即可,如:
<mapper namespace="com.hhu.dao.EmployeeDao">
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024">cache>
mapper>
public class Employee implements Serializable {
....
}
做完以上的工作即可测试:
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
//关闭SqlSession,原来查询到的e1对象被移至二级缓存中
sqlSession.close();
sqlSession = sqlSessionFactory.openSession();
EmployeeDao mapper1 = sqlSession.getMapper(EmployeeDao.class);
Employee e2 = mapper1.getEmployeeById(1);
System.out.println(e2);
System.out.println(e1==e2);
瞄一眼控制台:
DEBUG 03-27 10:12:39,263 Cache Hit Ratio [com.hhu.dao.EmployeeDao]: 0.0 (LoggingCache.java:62)
DEBUG 03-27 10:12:39,307 ==> Preparing: SELECT * FROM tbl_employee WHERE id = ? (BaseJdbcLogger.java:159)
DEBUG 03-27 10:12:39,460 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:159)
DEBUG 03-27 10:12:39,746 <== Total: 1 (BaseJdbcLogger.java:159)
Employee{id=1, lastName='jacksonary', gender=0, email='[email protected]'}
DEBUG 03-27 10:12:39,881 Cache Hit Ratio [com.hhu.dao.EmployeeDao]: 0.5 (LoggingCache.java:62)
Employee{id=1, lastName='jacksonary', gender=0, email='[email protected]'}
false
确实是只发了一条SQL,有些同学可能会疑问,既然从缓存中拿的东西怎么最后输出了fasle
,再缕一缕,第一次从数据库中查询到记录放进在SqlSession
的一级缓存中(因为之前的缓存中没有这个对象,所以缓存命中率为0%),第二次将SqlSession
关闭后,将这个对象移到了二级缓存中,获取的时候是从二级缓存中获取的(所以缓存命中率为50%),所以不一样。
在看完上面的一级、二级缓存的配置之后,我们来看下MyBatis中有关缓存的配置:
标签中的cacheEnabled
属性,它是用来设置是否启用二级缓存,跟一级缓存是否启用没啥关系;SqlSession.clearCache()
方法是清空的一级缓存,二级缓存不受其影响;
中都有一个useCache
的属性,默认为true
,可以将它设置为false
实现局部方法禁用二级缓存;
、
、
、
标签中其实都有一个flushCache
属性,表示该方法执行后是否清空缓存(这里是清空所有缓存,包含了一级缓存和二级缓存),其中
标签中的flushCache
的值默认为false
,执行完查询的操作后缓存不会被清空,而其他写操作的三个标签中的flushCache
的值默认为true
,表示执行完该标签的方法后会清空所有缓存,所以这就解释了为什么在两次查询之间夹杂了其他的写操作为什么缓存会失效的问题;
标签有一个localCacheScope
属性,可以配置本地缓存作用域,有两种取值SESSION
和STATEMENT
,其中SESSION
表示当前会话也就我们所说的一级缓存,STATEMENT
则设置当前会话没有缓存了,也就没有所谓的缓存对象来共享了,默认是SESSION
,这个配置可以禁用一级缓存,别瞎改,影响MyBatis的查询效率。 这里结合下面的一幅图来说:
每次MyBatis通过SqlSession
去数据库查询一个新对象的时候,会将这个对象缓存到一级缓存中(即图中的Session Cache
),然后如果该SqlSession
关闭后,MyBatis会将该Session
中的对象移至二级缓存(即namespace Cache
)中,每次一个新的SqlSession
实例化后去查询对象的时候,首先将去namesapce
二级缓存中查询,如果没有再到session cache
中去查找,如果没有,才到数据库中查询。在MyBatis中,缓存就是一个Map
的数据结构。
在上面小节中,我们就说过MyBatis中的缓存比较简陋,就是一个Map
,来看一下它的实现接口:
public interface Cache {
String getId();
void putObject(Object var1, Object var2);
Object getObject(Object var1);
Object removeObject(Object var1);
void clear();
int getSize();
ReadWriteLock getReadWriteLock();
}
在前面说
标签里面的type
属性的时候就说过可以通过实现org.apache.ibatis.cache.Cache
接口的方式自定义缓存方式,然后重写其中存取数据的方法putObject
和removeObject
方法即可,现在比较流行的方式是用Redis、Ehcache以及MemCache用于数据的缓存,下面来看一下MyBatis整合Ehcache的流程,并且在MyBatis的github工程中为我们提供实现这个接口后的实体方法的重写,下拉至ehcache-cache
,点击去可以查看它的源码,其中这里是MyBatis和Ehcache的整合官方文档。
比前中的type
属性,指定为这里的Ehcache在MyBatis适配器中的全类名即可:<mapper namespace="org.acme.FooMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
...
mapper>
ecache.xml
:<ehcache>
<diskStore path="F:\\testTemp"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<cache name="com.hhu.entities.Employee"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
ehcache>
好了配置完上面的东西后,跑案例即可发现在F:\testTemp
文件夹创建并且有缓存文件。
MyBatis和Spring整合是实际开发中用的比较多,同样在MyBatis的github工程下面找到Spring-MyBatis整合工程,拉到页面下面点击See the docs
查看文件教程,和上面整合Ehcache一样也需要有适配器才能完成整合,对于各自版本的要求如下:
MyBatis-Spring | MyBatis | Spring |
---|---|---|
1.0.0 and 1.0.1 | 3.0.1 to 3.0.5 | 3.0.0 or higher |
1.0.2 | 3.0.6 | 3.0.0 or higher |
1.1.0 or higher | 3.1.0 or higher | 3.0.0 or higher |
1.3.0 or higher | 3.4.0 or higher | 3.0.0 or higher |
根据上面版本要求,我这里用的是mybatis-3.4.6.jar,然后适配器用的是mybatis-spring-1.3.2.jar,c3p0为c3p0-0.9.1.2.jar,这里直接搞一个SSM的整合,将Spring MVC也搞进来,看一下整合的思路。
我必须强调一下,这一块非常重要!!!基本就是我以后搭建SSM项目全部步骤(基本都是修修改改就好了,值得珍藏)。之前一直用的maven,回顾Spring的时候,用的最low的本地包导入,发现上述的搭配好像不太行,一直报错(可能不是原配),异常如下:
java.lang.AbstractMethodError: com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.isClosed()Z
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at net.bull.javamelody.JdbcWrapper$StatementInvocationHandler.invoke(JdbcWrapper.java:186)
at net.bull.javamelody.JdbcWrapper$DelegatingInvocationHandler.invoke(JdbcWrapper.java:251)
at $Proxy12.isClosed(Unknown Source)
at com.acteksoft.common.util.sql.ActekPreparedStatement.isClosed(ActekPreparedStatement.java:2437)
at com.acteksoft.common.util.DefaultDatabaseWorker$2.in(DefaultDatabaseWorker.java:153)
at com.acteksoft.common.util.jdbc.DataSourceConnectionProvider$1.doInConnection(DataSourceConnectionProvider.java:62)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:341)
想换个版本看看,最近Spring网页改了,下载jar包搞的特别麻烦,这里直接放出地址Spring各种版本集合,要啥就点啥,简单粗暴。我去,换一个还不行,此时的我已经有种预感,不会c3p0这货又抽抽了吧,又搞了下,发现这是c3p0 0.9.1.2
抽抽了,在5+版本中已经修复了这个Bug,最终关于数据库连接池这一块选择如下版本的jar包:commons-logging-1.1.1.jar
和mchange-commons-java-0.2.11.jar
,最终通过测试,所有的jar包如下:
c3p0-0.9.5.2.jar
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
commons-logging-1.1.1.jar
mchange-commons-java-0.2.11.jar
mybatis-3.4.6.jar
mybatis-spring-1.3.2.jar
mysql-connector-java-5.1.7-bin.jar
spring-aop-4.3.4.RELEASE.jar
spring-aspects-4.3.4.RELEASE.jar
spring-beans-4.3.4.RELEASE.jar
spring-context-4.3.4.RELEASE.jar
spring-core-4.3.4.RELEASE.jar
spring-expression-4.3.4.RELEASE.jar
spring-jdbc-4.3.4.RELEASE.jar
spring-orm-4.3.4.RELEASE.jar
spring-tx-4.3.4.RELEASE.jar
spring-web-4.3.4.RELEASE.jar
spring-webmvc-4.3.4.RELEASE.jar
taglibs-standard-impl-1.2.1.jar
taglibs-standard-spec-1.2.1.jar
【注意】上述jar包也是SSM框架整合都需要的jar包。
下面我们来看一下完整的整合过程:
db.properties
:# MySQL
jdbc.username=root
jdbc.password=921228jack
# allowMultiQueries=true表示开启一次指定多条SQL语句,用分号分割,默认是关闭的
jdbc.url=jdbc:mysql://localhost:3306/db_mybatis?allowMultiQueries=true
jdbc.driver=com.mysql.jdbc.Driver
mybatis-config.xml
:
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="jdbcTypeForNull" value="NULL"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
settings>
<typeAliases>
<package name="com.hhu.entities"/>
typeAliases>
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sql server"/>
databaseIdProvider>
configuration>
springmvc.xml
:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.hhu" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
bean>
<mvc:annotation-driven>mvc:annotation-driven>
<mvc:default-servlet-handler/>
beans>
applicationContext.xml
,所有的整合工作都在这里做:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.hhu" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
bean>
<mvc:annotation-driven>mvc:annotation-driven>
<mvc:default-servlet-handler/>
beans>
web.xml
:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:applicationContext.xmlparam-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<servlet>
<servlet-name>springDispatcherServletservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:springmvc.xmlparam-value>
init-param>
servlet>
<servlet-mapping>
<servlet-name>springDispatcherServletservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
web-app>
瞄一眼整体Module的结构:
好了,其实到这里,整个SSM的整合配置基本就已经结束了,剩余看你们自由开发就行了,这里把两个页面也贴出来:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
SSM
查询所有员工
<%--
Created by IntelliJ IDEA.
User: WeiguoLiu
Date: 2018/3/27
Time: 21:10
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
员工列表
lastName
gender
email
${emp.lastName}
女
男
${emp.email}
基本的控制器、Dao层的接口以及对应的mapper映射SQL文件、Service层怎么去写就不浪费时间了。
MyBatis可以根据数据库的表生成对应的映射文件、接口、以及JavaBean,这个过程就是MyBatis的逆向工程MyBatis Generator,简称MBG(不是BGM…),他是一个为开发者提供的一个友好的代码生成器。但是表之间的连接、存储过程等复杂SQL需要我们手动编写。同样的,开头先上MBG文档。
直接看栗子,首先根据文档,我们先在原来的基础引入适配器的包:mybatis-generator-core-1.3.6.jar
,在项目根目录(和src
同级)下创建MBG的配置文件mbg.xml
,如下:
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3Simple">
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/db_mybatis"
userId="root"
password="9j">
jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
javaTypeResolver>
<javaModelGenerator targetPackage="com.hhu.entities" targetProject=".\src">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject=".\src">
<property name="enableSubPackages" value="true" />
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.hhu.dao" targetProject=".\src">
<property name="enableSubPackages" value="true"/>
javaClientGenerator>
<table tableName="tbl_employee" domainObjectName="Employee">table>
<table tableName="tbl_departments" domainObjectName="Department">table>
context>
generatorConfiguration>
注意将原来的mapper文件接口类全部删除,然后运行如下的代码:
/**
* 测试逆向工程是否可以创建接口和mapper
* @throws Exception
*/
@Test
public void mbgTest() throws Exception {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("mbg.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
即可生成我们指定表的相关接口(包含基本的增删改查操作)和SQL映射文件。其他的MyBatis全局配置文件正常些即可,调用的时候直接调用即可。
在正常开发中简单的增删改查可能无法满足我们的需求,所以MyBatis为我们提供了更为先进的方式,将上面的mbg.xml
文件中的
标签中的targetRuntime
属性指定为MyBatis3
可以生成动态SQL,并且除了原来的接口类和SQL映射文件外,还会为每个JavaBean生成一个xxxExample
类用于封装查询条件执行带条件的查询行为,来看一下用法:
/**
* 测试逆向工程生成的接口方法
*/
@Test
public void testSimpleMethod() {
//注意将conf文件夹标记位为
InputStream is = null;
try {
is = Resources.getResourceAsStream("mybatis-config.xml");
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
//org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
// com.hhu.dao.EmployeeMapper.selectAll
EmployeeMapper eMapper = sqlSession.getMapper(EmployeeMapper.class);
//使用带条件的查询,都是用生成的Example来封装查询条件
EmployeeExample employeeExample = new EmployeeExample();
//创建Criteria,用于拼装查询条件
EmployeeExample.Criteria criteria = employeeExample.createCriteria();
//比如按照姓名中有a的,且性别为男
criteria.andLastNamemmLike("%a%").andGenderEqualTo(1);
//上面封装的条件都是AND的关系,如果想中间夹杂OR的条件,可以创建新的查询条件Criteria
//然后封装进入Example中,用or方法即可,那么criteria和criteria2之间就是或的关系,
EmployeeExample.Criteria criteria2 = employeeExample.createCriteria();
criteria2.andEmailIsNotNull();
employeeExample.or(criteria2);
//进行查询获取结果
List<Employee> list = eMapper.selectByExample(employeeExample);
System.out.println(list);
}
注意下一条件之间AND
和OR
是怎么实现的。
提供一个实际工作中使用的逆向工程的配置文件:
文件名一般是generatorConfig.xml
否则找不到配置文件
<generatorConfiguration>
<context id="docTables" targetRuntime="MyBatis3" defaultModelType="flat">
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://xxx:3306/xxx"
userId="xx"
password="xx">
jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
javaTypeResolver>
<javaModelGenerator targetPackage="com.xxx.xxx.dao.model" targetProject="./src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
javaModelGenerator>
<sqlMapGenerator targetPackage="com.xxx.xxx.dao.mapper" targetProject="./src/main/resources">
<property name="enableSubPackages" value="true"/>
sqlMapGenerator>
<javaClientGenerator targetPackage="com.xxx.xxx.dao.mapper" targetProject="./src/main/java"
type="XMLMAPPER">
<property name="enableSubPackages" value="true"/>
javaClientGenerator>
<table tableName="xxx_handle" domainObjectName="xxxHandle"
enableInsert="true"
enableSelectByPrimaryKey="true"
enableUpdateByPrimaryKey="true"
enableDeleteByPrimaryKey="true"
enableSelectByExample="false"
enableDeleteByExample="false"
enableCountByExample="false"
enableUpdateByExample="false"
selectByPrimaryKeyQueryId="false"
selectByExampleQueryId="false">
table>
context>
generatorConfiguration>
【注意】
在实际开发中,可能存在对同一张表多次生成使用逆向插件生成,采用MyBatis逆向工程插件生成model
和mapper
以及interface
,对于已有的表再次是使用逆向插件生成时,注意,model
和interface
会重新生成,但不会覆盖已有的文件,同城会在指定的文件名帮你自动使用一个数字标记,比如在配置文件中指定的文件名为RfaParametric.java
,如果指定目录下已经存在了,逆向工程插件会帮我们命名为RfaParametric.java.1
interface
也是和model
一样,以追加数字区分,但是mapper
映射文件不会重新生成,只会在已有的mapper
中追加新内容(如果有的话),已有的内容不会覆盖。
在MyBatis中提供了很多插件开发的机制,可以通过插件为目标对象创建代理对象,插件为四大对象创建出代理对象(代理对象可以拦截到四大对象的每个执行方法)。所以插件在一定程度上可以更改MyBatis的底层,来看一下具体的流程:
步骤:
org.apache.ibatis.plugin.Interceptor
接口@Intercepts
注解完成插件签名的package com.hhu.plugins;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
/*
插件签名主要就是
使用注解的方式告诉MyBatis当前插件用来拦截哪个类的哪个方法
type:指定拦截的类
method:指定拦截的方法,这里是拦截往SQL语句中设值的方法
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//执行目标方法(有没有想到SpringAOP的执行过程呢,有点像)
Object proceed = invocation.proceed();
return proceed;
}
/*
这个方法是用来包装目标对象的,为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object o) {
/*
利用MyBatis的Plugin中静态方法封装我们的目标对象o为一个代理对象
*当然这里也可以不使用MyBatis中的自带的类封装,可以自己手动封装出一个代理对象,功能一样
* 不要忘了动态代理的创建过程,Proxy.newProxyInstance()
*/
System.out.println("MyFirstPlugin...plugin:MyBatis将要包装的对象");
Object wrap = Plugin.wrap(o, this);
//返回目标对象的动态代理
return wrap;
}
/*
将插件注册时的property属性值设置出来
*/
@Override
public void setProperties(Properties properties) {
System.out.println("获取插件的配置信息:" + properties);
}
}
【注意】插件中的三个方法的顺序依次为:setProperties()
(获取配置信息)————>plugin()
(创建代理对象)————>intercept()
(拦截指定方法做完处理通过动态代理执行目标方法)。
然后在MyBatis的全局配置文件中注册我们自定义的插件即可:
<plugins>
<plugin interceptor="com.hhu.plugins.MyFirstPlugin">
<property name="userName" value="root"/>
<property name="password" value="12345"/>
plugin>
plugins>
然后正常测试一个即可(带设置的SQL,比如带条件的查询)。
在实际开发中,可能存在多个插,这里就涉及到它们的指定是顺序,开始直接给出结论:不是顺序执行也不是像之前的拦截器一样S形执行,比如在MyBtais的全局配置文件中注册了两个插件,在配置和为目标对象生成代理对象时,是按照全局配置文件中的配置顺序执行的,但是在通过代理对象执行目标方法时是按照逆序执行的,这个可以根据下图进行理解:
图中很清楚的标注插件的封装顺序和执行顺序。下面看一下具体代码:
/*插件部分直接将上面的插件类复制一份,稍微给一下输出语句以作区分,然后注册到
* MyBatis的全局配置文件中,即可
*/
在注册完有运行带有参数的查询即可实现上述的多插件的运行顺序。
这种有点像AOP的意思,其他代码不变,主要在拦截到的时候利用动态代理执行目标方法前作处理(主要在intercept()
方法中做处理),下面来看代码:
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("执行目标方法1..." + invocation.getMethod());
//执行目标方法(有没有想到SpringAOP的执行过程呢,有点像,这里可以做一些非业务逻辑)
//获取目标对象
Object target = invocation.getTarget();
System.out.println("获取目标对象:" + target);
//获取目标对象的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("获取的源数据:" + value);
//更改源数据,即MyBatis发送SQL传入的参数为该6
metaObject.setValue("parameterHandler.parameterObject", 6);
Object proceed = invocation.proceed();
return proceed;
}
【注意】在上面的获取源数据metaObject
时,如果是普通的参数是可以直接得到(比如是按id
查询,那么这个就直接获取id
),但是如果是带条件的查询(比如利用xxxExample
查询),那么metaObject.getValue("parameterHandler.parameterObject")
获取的是xxxExample
对象而不是具体的类似于具体的’id’数值。
PageHelper
是一个分页插件,比较好用,之前在PageHelper分页的实现就已经做了使用说明,下面记录一下完整的使用过程,官方使用说明文档。
jar
包,我这里使用的是pagehelper-5.1.2.jar
和jsqlparser-0.9.5.jar
;<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="param1" value="value1"/>
plugin>
plugins>
3.使用,配置完上述的东西我们就可以使用了
@Test
public void testPage() throws IOException {
//读取配置文件的流
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
//利用配置文件创建Session工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeDao emapper = sqlSession.getMapper(EmployeeDao.class);
//传入分页参数,第一个参数表示第几页(首页是1不是0),第二个参数表示每个显示几条记录,
Page<Object> page = PageHelper.startPage(5, 1);
List<Employee> allEmps = emapper.getAllEmps();
System.out.println("当前页码:" + page.getPageNum());
System.out.println("总记录数:" + page.getTotal());
System.out.println("每页记录数:" + page.getPageSize());
System.out.println("总页数:" + page.getPages());
System.out.println("+++++++++++++++++++++");
PageInfo<Employee> pageInfo = new PageInfo<>(allEmps);
System.out.println("当前页码:" + pageInfo.getPageNum());
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("每页记录数:" + pageInfo.getPageSize());
System.out.println("总页数:" + pageInfo.getPages());
System.out.println("是否首页:" + pageInfo.isIsFirstPage());
System.out.println("是否末页:" + pageInfo.isIsLastPage());
//在使用PageInfo封装List时,还可以指定连续显示多少页
PageInfo<Employee> pageInfo2 = new PageInfo<>(allEmps, 2);
int[] navigatepageNums = pageInfo2.getNavigatepageNums();
for (int i = 0; i < navigatepageNums.length; i++) {
System.out.println(navigatepageNums[i]);
}
}
需要分页的时候,我们只需要在调用接口的查询方法前面加上一个Page
即可完成对紧跟在该语句后的查询方法进行分页(第一个参数是指查询第几页,第二个参数是每个显示多少条记录),然后输出查询方法得到的List
结果就是第5页结果(只有一条记录);当然也可以将PageHelper.startPage(5, 1)
的结果赋个一个Page
对象,里面有很多详细信息,在上面测试了几个方法;最后,还有可以用PageInfo
对象来封装查询出来的List
结果,这个东西据说很重要o( ̄▽ ̄)o,可以用new PageInfo<>(allEmps)
这样的形式封装,用法比第一种稍微丰富一点,当然除此之外还可以使用new PageInfo<>(allEmps, 2)
这样的形式指定连续显示多少页,这里就连续显示2页,
for (int i = 0; i < navigatepageNums.length; i++) {
System.out.println(navigatepageNums[i]);
}
这个代码就是获取连续显示的页码,这里需要注意一下连续的含义,比如上面的小栗子是获取的第5页的记录,然后它需要连续显示2页,那么就是显示的第4、5两页,而不是第5、6两页,就是以当前页为参照点,如果连续显示1页,那么就是显示第5页,如果连续显示2页,那么就是显示第4、5页,如果连续显示3页,那么就是显示第4、5、6页(先插左再插右),我是这么理解的,看图或许更好理解:
在之前通过其他的一些方法实现不断拼接SQL语句达到批量操作的目的,一般情况下服务器都有限制SQL的长度,所以前面的方式只能做简单的一些批量操作,而不能做太复杂的批量操作,毕竟她不是专业的,MyBatis中提供批量操作,下面分作两部分来看一下:
第一部分是MyBatis单独使用批量操作,不作其他框架的整合,非常简单只需要在获取SqlSession
的时候指定获取批量操作的SqlSession
即可,看一下代码:
@Test
public void batchTest() throws IOException {
//读取配置文件的流
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
//利用配置文件创建Session工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
//指定获取批量操作的SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
EmployeeDao emapper = sqlSession.getMapper(EmployeeDao.class);
for (int i = 0; i < 100; i++) {
emapper.addEmp(new Employee(UUID.randomUUID().toString().substring(1,5),"1", "[email protected]"));
}
sqlSession.commit();
sqlSession.close();
}
代码和普通添加员工的代码是一样的,唯一不同的是sqlSessionFactory.openSession(ExecutorType.BATCH)
,指定批量SqlSession
,在执行的时候,100次的时候控制台是设置了100次的参数,很快(SQL预编译1次 + 参数设置100次 + 数据库1次执行SQL);如果使用sqlSessionFactory.openSession()
获取SqlSession
,那么同样的插入操作,很费时间(SQL预编译100次 + 参数设置100次 + 数据库执行100次SQL)。
其次我们来看一下在MyBatis在和Spring作整合工作时,SqlSession
由Spring负责生成,那么我们需要在配置了SqlSessionFactory
后指定SqlSession
的生成方式,要上面的构造方法,看一下配置:
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
<constructor-arg name="executorType" value="BATCH"/>
bean>
通过以上的配置后,我们在SSM整合时就可以直接在Service层注入上述的SqlSession
使用即可:
@Autowired
private SqlSession sqlSession;
//然后在某个逻辑方法中使用上述的注入的SqlSession获取mapper接口操作即可
在实际开发中会有一些很复杂的逻辑,存储过程为了避免反复去写这样的逻辑而诞生,下次有相同的逻辑调用存储过程即可。
TypeHandler在java和数据库交互的过程中,主要负责数据库类型和javaBean类型的映射。