MyBatis框架基础
1、MyBatis简介
MyBatis(曾用名iBatis)世界上流行最广泛的半自动ORM映射框架,由Clinton
Begin在2002年创建,其后,捐献给了Apache基金会,成立了iBatis项目。2010
年5月,将代码库迁移至Google Code,并更名为MyBatis。iBatis原始网址为
http://ibatis.apache.org,MyBatis新网址为
https://code.google.com/p/mybatis。MyBatis是一个可以自定义SQL,存储过程和高级映射的持久层框架,MyBatis摒除了大部分的JDBC代码、手工设置参数和结果集重获。MyBatis使用简单的XML和注解来配置和映射基本数据类型,Map接口和POJO到数据库记录。
小结:MyBatis是一个可自定义SQL的半自动ORM映射持久层框架。
2、MyBatis工作原理
工作原理转自:https://blog.csdn.net/u014745069/article/details/80788127。
以前只是会用,没有深究过它的工作原理,现在想了解一下它是怎么工作的,查了下资料,觉得别人写的挺不错的,拿过来学习一下。
原理图示:
- mybatis应用程序通过SqlSessionFactoryBuilder从mybatis-config.xml配置文件(也可以用Java文件配置的方式,需要添加@Configuration)来构建SqlSessionFactory(SqlSessionFactory是线程安全的)。
- SqlSessionFactory的实例直接开启一个SqlSession,再通过SqlSession实例获得Mapper对象并运行Mapper映射的SQL语句,完成对数据库的CRUD和事务提交,之后关闭SqlSession。
注意:SqlSession是单线程对象,因为它是非线程安全的,是持久化操作的独享对象,类似jdbc中的Connection,底层就封装了jdbc连接。
详细流程说明:
- 加载mybatis全局配置文件(数据源、mapper映射文件等),解析配置文件,MyBatis基于XML配置文件生成Configuration,和一个个MappedStatement(包括了参数映射配置、动态SQL语句、结果映射配置),其对应着
<select | update | delete | insert>
标签项。 - SqlSessionFactoryBuilder通过Configuration对象生成SqlSessionFactory,用来开启SqlSession。
- SqlSession对象完成和数据库的交互:
a、用户程序调用mybatis接口层api(即Mapper接口中的方法)。
b、SqlSession通过调用api的Statement ID找到对应的MappedStatement对象。
c、通过Executor(负责动态SQL的生成和查询缓存的维护)将MappedStatement对象进行解析,sql参数转化、动态sql拼接,生成jdbc Statement对象。
d、JDBC执行sql。
e、借助MappedStatement中的结果映射关系,将返回结果转化成HashMap、JavaBean等存储结构并返回。
MyBatis的层级图解:
3、MyBatis配置文件
这个配置文件很关键,正是通过这个核心的配置文件来生成Configuration对象。SqlSessionFactory是通过Configuration对象来生成的,如果配置文件配置错误或者无法加载,那么后面的一系列动作都没什么意义。
配置文件一般命名为mybatis-config.xml,示例如下:
<?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">
<!-- mybatis的核心配置文件,注意,节点配置有顺序要求 -->
<configuration>
<!-- 先加载数据库属性文件 -->
<properties resource=""></properties>
<!-- 再配置settings节点 -->
<settings>
<!-- 配置超时时间 -->
<setting name="defaultStatementTimeout" value=""/>
<!-- 配置执行器 -->
<setting name="defaultExecutorType" value=""/>
<!-- 开启二级缓存,需要配置 -->
<setting name="cacheEnabled" value=""/>
</settings>
<!-- 为实体pojo设置别名 -->
<typeAliases>
<typeAlias type="com.ycz.model.domain.Students" alias="stu"/>
</typeAliases>
<!-- 配置数据源 -->
<environments default="demo">
<environment id="demo">
<!-- 事务管理类型使用jdbc -->
<transactionManager type="JDBC"></transactionManager>
<!-- 数据源使用POOLED -->
<dataSource type="POOLED">
<property name="url" value="${jdbc_url}"/>
<property name="driver" value="${jdbc_driverClass}"/>
<property name="username" value="${jdbc_username}"/>
<property name="password" value="${jdbc_password}"/>
</dataSource>
</environment>
</environments>
<!-- 加载缓存配置文件及SQL映射文件 -->
<mappers>
<mapper resource="com/ycz/conf/mybatis/GlobalCache.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/Students-mapper.xml"/>
</mappers>
</configuration>
4、MyBatis核心组件
4.1、Configuration组件
加载mybatis-config.xml全局配置文件,解析配置文件,MyBatis基于XML配置文件会先生成Configuration对象。这个对象是用来构建SqlSessionFactory对象的关键。
org.apache.ibatis.session.Configuration类的部分源码:
这些属性在XML中都是可配置的。
这个类有2个构造方法,一般第一种用的较多,传一个Environment类型的参数,在xml中是以标签配置的,正是数据源的配置:
4.2、SqlSessionFactoryBuilder组件
SqlSessionFactoryBuilder实例的主要作用是为了创建一个SqlSessionFactory对象。
org.apache.ibatis.session.SqlSessionFactoryBuilder类的源码:
这个类只有一个build方法,重载方法有多种,核心作用就是用来创建一个SqlSessionFactory的实例。
4.3、SqlSessionFactory组件
SqlSessionFactory是MyBatis框架的核心引擎。SqlSessionFactory包含了
MyBatis最基础的元数据配置并且提供了访问数据库的具体SqlSession实例的维
护,通常使用SqlSessionFactoryBuilder来构建一个SqlSessionFactory的实例。
要想获取SqlSessionFactory这个实例,核心配置文件中必须配置数据源。
示例如下:
<environments default="demo">
<environment id="demo">
<!-- 事务管理类型使用jdbc -->
<transactionManager type="JDBC"></transactionManager>
<!-- 数据源使用POOLED -->
<dataSource type="POOLED">
<property name="url" value="${jdbc_url}"/>
<property name="driver" value="${jdbc_driverClass}"/>
<property name="username" value="${jdbc_username}"/>
<property name="password" value="${jdbc_password}"/>
</dataSource>
</environment>
</environments>
org.apache.ibatis.session.SqlSessionFactory接口源码:
这个组件的核心作用是调用其openSession方法,来获取SqlSession对象。
4.4、SqlSession组件
SqlSession对象用来和数据库进行交互,也是执行业务SQL的最核心组件。
org.apache.ibatis.session.SqlSession接口源码:
这个api比较简单,核心方法说明:
- insert方法:添加记录,返回int值。
- update方法:修改记录,返回int值。
- delete方法:删除记录,返回int值。
- selectOne方法:查询单个对象。
- selectList方法:查询多个对象的集合,返回List。
- selectMap方法:查询对象集合,返回Map。
4.5、SqlMap基本映射
MyBatis框架中Sql语句必须通过映射配置才能被MyBatis相关API组件调用执行,MyBatis提供以下两种方式实现Sql映射。
4.5.1、xml方式映射Sql
这种使用起来更加灵活,功能更加强大,一般稍微复杂一点的sql语句都会选择在xml中配置映射。SQL映射文件一般命名为xxxxmapper.xml。
sql映射文件需要在MyBatis核心配置文件中进行加载,如下:
<mappers>
<!--加载sql映射配置文件-->
<mapper resource="com/ycz/conf/mybatis/mapper/xxx-mapper.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/xxx-mapper.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/xxx-mapper.xml"/>
</mappers>
xxxxmapper.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="">
<!--添加记录使用insert标签-->
<insert id="" parameterType="">
<![CDATA[
insert into department(id,name,code,newDate,descs) values
(#{id},#{name},#{code},#{newDate},#{descs})
]]>
</insert>
<!--删除记录使用delete标签-->
<delete id="" parameterType="">
<![CDATA[
delete from department where id=#{id}
]]>
</delete>
<!--修改记录使用update标签-->
<update id="" parameterType="">
<![CDATA[
update department set name=#{name},code=#{code}
where id=#{id}
]]>
</update>
<!-- 查询记录使用select标签 -->
<select id="" resultType="" parameterType="">
<![CDATA[
select * from students where age>=#{minAge} and age<=#{maxAge}
and email like #{email}
order by age desc
]]>
</select>
</mapper>
某个方法的具体映射是namespace.id
。
4.5.2、注解方式映射Sql
这种方式可以减少xml文件的配置,但是功能受限,一般比较简单的SQL可以使用这种方式。
示例:
@Mapper
public interface DepartmentMapper{
@Select(value="select * from xxx")
public List<Department> findDepList();
}
查询可以使用@Select
注解,添加可以使用@Insert
注解,修改可以使用@Update
注解,删除可以使用@Delete
注解,接口上需要标注@Mapper
注解。
小结:比较简单的SQL,直接使用注解方式映射,复杂的SQL语句,使用xml方式配置映射,这两种映射方式结合起来使用,往往会提高开发效率。
4.6、核心组件实例的生命周期
(1)SqlSessionFactoryBuilder实例
SqlSessionFactoryBuilder实例的主要作用是为了创建一个SqlSessionFactory而生存,这个类的实例就可以被丢弃了,长时间保存此对象在内存中是没有意义的,所以强烈建议此类的生命周期尽可能的短,推荐在某个方法体内使用,随着方法的调用而衰亡。
(2)SqlSessionFactory实例
SqlSessionFactory创建后在整个应用过程中始终存在,一个应用中一般不涉及多个数据源时,也只会有一个SqlSessionFactory实例,所以它的生命周期应该是Application。通常定义此实例为单例模式。
(3)SqlSession实例
SqlSession是操作数据库业务最主要的接口,此实例不是线程安全的,不能共享此对象。它的作用域应在当前请求中或一个方法体内,也不应该使用类的静态变量来引用一个SqlSession实例,甚至不要使用类的一个实例变量来引用。绝对不能在web应用中的某个会话作用域中引用此对象,应随着用户的某个HTTP请求结束而释放此对象,在使用完SqlSession执行完数据操作后要及时关闭SqlSession。
(4)Mapper实例
Mapper实例必须由一个SqlSession对象所创建,而且在使用Mapper实例实现数据库操作时也必须保证创建它的SqlSession是处于打开状态下。因此Mapper的生命周期通常和SqlSession的生命周期相同,在一个SqlSession关闭或销毁后,基于此SqlSession创建的Mapper实例将不能被使用。即Mapper的使用必须基于SqlSession。
4.7、测试
先加入相关jar:
mysql驱动包和mybatis相关包是关键依赖,其他是与日志相关的依赖。
(1)创建数据库连接配置文件
内容如下:
## 配置数据库连接信息
jdbc_url=jdbc:mysql://rm-m5e130nm7h37n6v982o.mysql.rds.aliyuncs.com:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
jdbc_driver=com.mysql.cj.jdbc.Driver
jdbc_username=xxxxxx
jdbc_password=xxxxxx
(2)创建mybatis核心配置文件
创建mybatis核心配置文件mybatis-config.xml:
内容如下:
<!-- 根节点是configuratio -->
<configuration>
<!-- 加载数据库属性文件 -->
<properties resource="com/ycz/conf/db/mysqldb.properties"></properties>
<!-- 配置settings节点 -->
<settings>
<!-- 配置超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 配置执行器 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
<!-- 配置数据源 -->
<environments default="demo">
<environment id="demo">
<!-- 配置事务管理 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 使用POOLED数据源 -->
<dataSource type="POOLED">
<property name="url" value="${jdbc_url}"/>
<property name="driver" value="${jdbc_driver}"/>
<property name="username" value="${jdbc_username}"/>
<property name="password" value="${jdbc_password}"/>
</dataSource>
</environment>
</environments>
</configuration>
(3)获取SqlSession实例
工具类SqlSessionManager如下:
/*
* 该组件用来获取SqlSession实例
*/
public class SqlSessionManager {
// mybatis核心配置文件路径
private static final String MYBATIS_CONF = "com/ycz/conf/mybatis/mybatis-config.xml";
private static SqlSessionFactory sqlSessionFactory = null;
// 获取SqlSession实例
public static SqlSession getSqlSession() {
if (sqlSessionFactory == null) {
getSqlSessionFactory();
}
// SqlSessionFactory实例获取SqlSession实例
SqlSession sqlSession = sqlSessionFactory.openSession();
if (sqlSession != null) {
System.out.println("SqlSession实例构建成功!");
System.out.println(sqlSession);
}
return sqlSession;
}
// 获取SqlSessionFactory的实例
private static void getSqlSessionFactory() {
Reader reader = null;
try {
reader = Resources.getResourceAsReader(MYBATIS_CONF);
// SqlSessionFactoryBuilder对象构建sqlSessionFactory实例
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试:
public class Test {
public static void main(String[] args) {
SqlSessionManager.getSqlSession();
}
}
控制台:
SqlSession实例获取成功。
5、MyBatis核心配置文件详解
MyBatis的核心配置文件是框架的基础元数据信息和数据库连接信息配置的最主要文件,此文件内容信息将提供给创建SqlSessionFactory实例并应用,这个核心文件必须在使用MyBatis前最先被加载从而创建SqlSessionFactory实例。以下对MyBatis的核心配置文件mybatis-config.xml中常用配置进行说明。
5.1、配置文件DTD
示例:
<?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 -->
<configuration>
...
</configuration>
标准的DTD部分格式节点如下:
<!--configuration是根节点,其他节点在此基础上构建-->
configuration
--properties
--settings
--setting
--typeAliases
--typeAlias
--typeHandlers
--objectFactory
--plugins
--environments
--environment
--transactionManager
--dataSource
--property
--mappers
注意:每个元素的位置一旦出现必须要遵守先后顺序,顺序不对可能会报错。
5.2、常用元素及属性
常用元素标签有<settings>
标签、<typeAliases>
标签、<environments>
标签、<mappers>
标签等。
5.2.1、settings元素
settings元素的作用是通过各个属性的配置信息设置MyBatis在运行过程中的各种特征。可设置多个settting子元素。
setting子元素可用属性:
- cacheEnabled:对此配置文件下的所有cache进行全局性开关设置,默认为true。
- lazyLoadingEnabled:全局设置懒加载,如果设为关,则所有相关联的都会被初始化加载。默认为true。
- aggressivelLazyLoading:当设置为开时,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载。默认为true。
- multipleResultSetsEnabled:允许和不允许单条语句返回多个数据集(取决于驱动需求)。默认为true。
- useColumnLabel:使用列标签代替列名称。不同的驱动器有不同的做法,参考一下驱动器文档,或者用这两个不同的选项进行测试一下。默认为true。
- useGeneratedKeys:允许JDBC生成主键,需要驱动器的支持,如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。默认为false。
- autoMappingBehavior:如何来自动映射数据表字段与对象的属性。PARTIAL将只自动映射简单的,没有嵌套的结构。FULL将自动映射所有复杂的结果。默认为PARTIAL。
- defaultExecutorType:配置和设定执行器,SIMPLE执行器执行其他语句。
REUSE执行器可能重复使用prepared statements语句,BATCH执行器可以重复执行语句和批量更新。默认为SIMPLE。 - defaultStatementTimeout:设置一个时限,以决定让驱动器等待数据库回应多长时间为超时。无默认值。
示例:
<!-- 配置settings节点 -->
<settings>
<!-- 配置超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 配置执行器 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 开启二级缓存,需要配置 -->
<setting name="cacheEnabled" value="true"/>
......
</settings>
5.2.2、typeAliases元素
typeAliases的作用是配置映射Java Bean类型的别名,别名可以方便的在其他地方使用。typeAliases元素中可设置多个typeAlias子元素。
typeAlias子元素可配置属性:
- type:JavaBean的类型,全限定名。
- alias:JavaBean类型对应的别名。
示例如下:
<!-- 为实体pojo设置别名 -->
<typeAliases>
<typeAlias type="com.ycz.model.domain.Students" alias="stu"/>
<typeAlias type="com.ycz.model.domain.Department" alias="dept"/>
<typeAlias type="com.ycz.model.domain.Employee" alias="emp"/>
</typeAliases>
类型的别名不能乱起,需遵守一定的规则。如下:
- 所有基本数据类型别名是加前缀下划线(如int类型别名_int)。
- 所有基础类型的包装类行为首字母变小写(如Interger类型别名为integer)。
- Date、Decimal、BigDecimal、Object、Map、HashMap、List、ArrayList、Collection、Iterator等别名首字母小写,其余不变。
5.2.3、environments元素
environments用来配置数据源信息,其中至少有一个数据源(environment子元素)配置。非必要的话,一个应用中通常有一个数据源即可。
示例:
<!-- 配置数据源 -->
<environments default="demo">
<environment id="demo">
<!-- 配置事务管理 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 使用POOLED数据源 -->
<dataSource type="POOLED">
<property name="url" value="${jdbc_url}"/>
<property name="driver" value="${jdbc_driver}"/>
<property name="username" value="${jdbc_username}"/>
<property name="password" value="${jdbc_password}"/>
</dataSource>
</environment>
</environments>
transactionManager子元素
transactionManager子元素定义MyBatis处理数据库事务的类型。MyBatis处理数据库事务支持2种类型:JDBC和MANAGED。
- JDBC:此类型使用的提交和回滚功能。它依靠使用连接的数据源来管理事务的作用域。
- MANAGED:此类型什么也不做,从不提交,回滚和关闭连接。让其他容器或代码实现中来管理事务的全部生命周期(如Spring或者JAVAEE服务器)。
dataSource子元素
dataSource元素用来配置在某个环境下的JDBC数据库属性信息MyBatis提供3种类型的数据源。如下:
- UNPOOLED:此类型在每次请求的时候简单的打开和关闭同一个连接,不使用任何连接池技术,性能较差不建议使用此种方式。
- POOLED:此类型缓存JDBC连接对象用于避免每次都要连接和生成连接实例而需要的验证时间(使用连接池的优点)。对于并发Web应用,建议使用此种方式的数据源。
- JNDI:此类型使用匹配应用服务器上配置的数据源,通过Java命名目录接口方式查找一个服务器上的数据源对象。
UNPOOLED数据源常用属性:
- driver:指定JDBC驱动器的Java类(class文件)。
- url:连接数据库实例的URL路径。
- username:登录数据库的用户名。
- password:登录数据库的密码。
- defaultTransactionsolationLevel:指定连接的默认事务隔离级别。
POOLED数据源常用属性:
POOLED除了具有UNPOOLED类型属性外还具备以下属性:
- poolMaximumActiveConnections:特定的时间里可同时使用的连接数。
- poolMaximumldleConnections:特定时间里闲置的连接数。
- poolMaximumCheckoutTime:在连接池强行返回前,一个连接可以进行检出的总计时间。
- poolTimeToWait:这是一个底层的设置,给连接一个机会去打印log状态,并重新尝试重新连接,避免长时间的等待。
- poolPingQuery:发送给数据库的Ping信息,测试数据库连接是否良好和是否准备好了接收请求。默认值是"NO PING QUERY SET",让大部分数据库都不使用Ping,返回一个友好的错误信息。
- poolPingEnabled:设置PingQuery是否可可用。如果是可用的,可以使用一个最简单的SQL语句进行测试。默认值是false。
- poolPingConnectionsNotUsedFor:配置poolPingQuery多长时间可以用。通常匹配数据库连接的超时,避免无谓的ping。默认是0。表示随时允许ping,必须在poolPingEnabled设为true的前提下才有效。
JNDI数据源:
<environments default="demo">
<environment id="demo">
<transactionManager type="JDBC" />
<dataSource type="JNDI">
<property name="initial_context" value="java:comp/env"></property>
<property name="data_source" value="jndi/mybatis"></property>
</dataSource>
</environment>
</environments>
配置好以后,需要在Tomcat容器的conf目录下修改context.xml信息,代码如下:
<Resource auth="Container"
driverClassName="com.mysql.jdbc.Driver"
maxActive="20"
maxIdle="10"
maxWait="-1"
name="jdbc/demo"
password="system"
type="javax.sql.DataSource"
url="jdbc:mysql://localhost:3306/demo"
username="root"/>
注意:使用JDNI时要特别注意,所有的支持jar依赖包必需要导入容器内项目的lib中,因为eclipse中的lib与Tomcat容器内的lib一点关系也没有,eclipse中的lib有,但是容器的lib中没有,必需手动放入其中。
5.2.4、mappers元素
mappers元素的作用是告诉MyBatis框架在初始化的时候如何加载SqlMap映射文件或标识了注解的接口。此元素内可配置多个mapper子元素,mapper子元素常用属性resource,即xml配置文件的位置。
示例:
<!-- 加载缓存配置文件及SQL映射文件 -->
<mappers>
<mapper resource="com/ycz/conf/mybatis/GlobalCache.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/Students-mapper.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/Employee-mapper.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/Department-mapper.xml"/>
</mappers>
5.3、测试
按照上面的,对mybatis-config.xml进行修改。
先创建与表对应的3个pojo。
person表:
pojo:
public class Person {
private Integer id;
private String name;
private String password;
private String address;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
department表:
pojo:
public class Department {
private Integer depId;
private String depName;
private String depLeader;
public Integer getDepId() {
return depId;
}
public void setDepId(Integer depId) {
this.depId = depId;
}
public String getDepName() {
return depName;
}
public void setDepName(String depName) {
this.depName = depName;
}
public String getDepLeader() {
return depLeader;
}
public void setDepLeader(String depLeader) {
this.depLeader = depLeader;
}
}
employee表:
pojo:
public class Employee {
private Integer empId;
private String empName;
private String gender;
private String email;
private Integer depId;
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getDepId() {
return depId;
}
public void setDepId(Integer depId) {
this.depId = depId;
}
}
在mybatis-config.xml中开启驼峰命名转换:
因为数据表中有字段名包含下划线,而pojo所有属性统一设置成了首字母大写的驼峰形式,所以需要转换,不开启的话做增删改查会报错。
配置pojo的别名:
<!-- 为pojo实体类设置别名 -->
<typeAliases>
<typeAlias type="com.ycz.model.pojo.Person" alias="per" />
<typeAlias type="com.ycz.model.pojo.Department" alias="dep" />
<typeAlias type="com.ycz.model.pojo.Employee" alias="emp" />
</typeAliases>
然后创建对应的SqlMapper映射xml文件:
在mybatis-config.xml中配置加载sqlMapper的映射xml文件:
<!-- 加载sqlMapper映射xml文件 -->
<mappers>
<mapper resource="com/ycz/conf/mybatis/mapper/personMapper.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/departmentMapper.xml"/>
<mapper resource="com/ycz/conf/mybatis/mapper/employeeMapper.xml"/>
</mappers>
再测试获取SqlSession,看是否会报错:
没问题,说明配置是正确的。
6、SqlMapper映射xml文件详解
6.1、DTD节点
固定格式:
<?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 -->
<mapper namespace="p">
</mapper>
mapper元素是根节点,有一个属性namespace,命名空间,映射的时候格式为namespace.id
。
6.2、insert元素
insert标签元素用来插入数据。常用属性如下:
- id:SQL语句引用的唯一标识,必须属性。
- parameterType:传给SQL语句的参数的完整类名或者别名。
- flushCache:执行更新时是否刷新缓存数据,默认值是true。
- useGeneeatedKeys:使用利用自身生成唯一的标识(主键),适用于自动增长主键表。
- keyProperty:指定在保存对象时自动生成标识反馈到对象的哪个属性。
- keyColumn:指定在保存对象时自动生成标识映射到底层表的主键列名。
- statementType:此条SQL语句生成哪种类型的Statement对象。
其中id和parameterType是必须属性,其他可选。
示例:
<insert id="addStudent" useGeneratedKeys="true" keyProperty="id"
keyColumn="ID" parameterType="map" statementType="PREPARED">
<![CDATA[
insert into student(name,sex,birth) values(#{name},#{sex},#{birth}
)]]>
6.3、update元素
insert标签元素用来修改数据。常用属性见insert元素。
示例:
<update id="modifyStudentById" parameterType="map" databaseld="mysql"
keyColumn="id" keyProperty="id" useGeneratedKeys="true">
<![CDATA[
update student set name=#{name},sex=#{sex} where id=#{id}
]]>
</update>
6.4、delete元素
delete标签元素用来删除数据。
示例:
<delete id="deleteStudentById" parameterType="int">
<![CDATA[
delete student from student where id=#{id}
]]>
</delete>
6.5、增删改测试
personMapper.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 -->
<mapper namespace="p">
<!-- 添加记录 -->
<insert id="addPerson" parameterType="map">
<![CDATA[
insert into person (name,password,address,age) values
( #{name},#{password},#{address},#{age})
]]>
</insert>
<!-- 修改记录 -->
<update id="updatePerson" parameterType="map">
<![CDATA[
update person set
name=#{name},password=#{password},address=#{address},age=#{age}
where id=#{id}
]]>
</update>
<!-- 删除记录 -->
<delete id="delPerson" parameterType="int">
<![CDATA[
delete from person where id=#{id}
]]>
</delete>
</mapper>
service层:
public interface PersonService {
// 添加用户
int addPerson(Map<String,Object> map);
// 更新用户
int updatePerson(Map<String,Object> map);
// 删除用户
int delPerson(int id);
}
public class PersonServiceImpl implements PersonService{
@Override
public int addPerson(Map<String, Object> map) {
return new PersonDaoImpl().addPerson(map);
}
@Override
public int updatePerson(Map<String, Object> map) {
return new PersonDaoImpl().updatePerson(map);
}
@Override
public int delPerson(int id) {
return new PersonDaoImpl().delPerson(id);
}
}
dao层:
public interface PersonDao {
int addPerson(Map<String, Object> map);
int updatePerson(Map<String, Object> map);
int delPerson(int id);
}
public class PersonDaoImpl implements PersonDao{
// 添加
@Override
public int addPerson(Map<String, Object> map) {
// 获取SqlSession实例
SqlSession sqlSession = SqlSessionManager.getSqlSession();
// 插入记录
// 第一个参数为sql映射调用的SQL,第二个参数是传递给SQL的参数
int res = sqlSession.insert("p.addPerson",map);
// 增删改需要手动提交
sqlSession.commit();
// 关闭
sqlSession.close();
return res;
}
// 修改
@Override
public int updatePerson(Map<String, Object> map) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
// 修改记录
int res = sqlSession.update("p.updatePerson",map);
sqlSession.commit();
sqlSession.close();
return res;
}
// 删除
@Override
public int delPerson(int id) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
// 删除记录
int res = sqlSession.delete("p.delPerson",id);
sqlSession.commit();
sqlSession.close();
return res;
}
}
创建新的Servlet组件,如下:
@WebServlet("/PersonServlet")
public class PersonServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 测试添加
Map<String, Object> map = new HashMap<>();
map.put("name", "猿AAA");
map.put("password", "123456");
map.put("address", "湖北十堰");
map.put("age", 27);
PersonService personService = new PersonServiceImpl();
// 如果要传给SQL多个参数,可以使用Map进行封装
int res = personService.addPerson(map);
if (res > 0) {
System.out.println("记录添加成功!");
} else {
System.out.println("记录添加失败!");
}
}
}
web.xml中配置servlet:
<!-- 配置Servlet -->
<servlet>
<servlet-name>personServlet</servlet-name>
<servlet-class>com.ycz.model.servlet.PersonServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>personServlet</servlet-name>
<url-pattern>/personServlet.do</url-pattern>
</servlet-mapping>
启动web容器,浏览器地址栏中访问:
控制台:
数据表:
修改PersonServlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 测试修改
Map<String, Object> map = new HashMap<>();
map.put("id", 26);
map.put("name", "猿AAA修改");
map.put("password", "123456666");
map.put("address", "湖北十堰222");
map.put("age", 28);
PersonService personService = new PersonServiceImpl();
// 如果要传给SQL多个参数,可以使用Map进行封装
int res = personService.updatePerson(map);
if (res > 0) {
System.out.println("记录修改成功!");
} else {
System.out.println("记录修改失败!");
}
}
启动web容器,再访问。控制台:
数据表:
修改PersonServlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 测试删除
PersonService personService = new PersonServiceImpl();
int res = personService.delPerson(25);
if(res > 0) {
System.out.println("id为25的记录已删除!");
} else {
System.out.println("记录删除失败!");
}
}
启动web容器,再访问。控制台:
数据表:
增删改比较简单,需要注意的是增删改操作需要手动提交,SqlSession使用完毕后应该关闭,传递单个参数给SQL直接传就行了,传多个参数使用Map封装参数,再将map传给SQL。
6.6、select元素
select标签元素用来从数据库中实现查询数据结果。select标签元素属性规范查询中的动作。比如如何传递查询参数,映射返回结果,是否使用缓存,查询超时限定等特性。select标签元素是应用的比较多的,也是稍微复杂一点。
常用属性:
- id:SQL查询语句引用的唯一标识,必须的属性。
- parameterType:传给SQL语句的参数的完整类名或者别名。
- resultType:查询返回数据行的类型名称或别名。
- resultMap:引用的外部resultMap元素id值,这个map描述如何封装查询结果。
- flushCache:设置执行sql语句时是否清空缓存,默认为false。
- useCache:设置查询结果是否被缓存,默认为false,此属性影响flushCache。
- timeout:设置数据操作超时时长,默认未设置。
- fetchSize:设置抓取的数据量,达到数量后强制返回结果。
- statementType:STATEMENT,PREPARED或CALLABLE中的任意一个,这就告诉MyBatis分别使用Statement,PreparedStatement或者CallableStatement。默认为PREPARED。
- resultSetType:FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE三个中的任意一个。默认是不设置,由驱动器去决定。
其中id、parameterType、resultType、resultMap属性是经常使用的。
示例:
<!--使用了pojo的别名dep-->
<select id="queryDepList" resultType="dep">
select id,name,code,newDate,descs from department
</select>
以下进行查询的简单测试。
personMapper.xml配置文件:
<!-- 按照指定id查询多条记录 -->
<select id="findById" resultType="per" parameterType="int">
select id,name,address,age from person where id=#{id}
</select>
<!-- 查询指定年龄范围的记录 -->
<select id="findByAge" resultType="per" parameterType="int">
select id,name,address,age from person where age>#{age}
</select>
<!-- 查询指定地址的的记录 -->
<select id="findByAddress" resultType="per" parameterType="string">
select id,name,address,age from person where address like CONCAT(#{address},'%')
</select>
<!-- 查询指定年龄范围指定姓的记录 -->
<select id="findByNameAndAge" resultType="per" parameterType="map">
select id,name,address,age from person where name like CONCAT(#{name},'%') and age>#{age}
</select>
service层:
//按照指定id查询
Person findById(int id);
// 按照年龄查询记录
List<Person> findByAge(int age);
// 按照地址模糊查询记录
List<Person> findByAddress(String address);
// 按照姓名和地址模糊查询记录
List<Person> findByNameAndAge(Map<String,Object> map);
@Override
public Person findById(int id) {
return new PersonDaoImpl().findById(id);
}
@Override
public List<Person> findByAge(int age) {
return new PersonDaoImpl().findByAge(age);
}
@Override
public List<Person> findByAddress(String address) {
return new PersonDaoImpl().findByAddress(address);
}
@Override
public List<Person> findByNameAndAge(Map<String, Object> map) {
return new PersonDaoImpl().findByNameAndAge(map);
}
dao层:
Person findById(int id);
List<Person> findByAge(int age);
List<Person> findByAddress(String address);
List<Person> findByNameAndAge(Map<String,Object> map);
// 按照id查询
@Override
public Person findById(int id) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
Person p = sqlSession.selectOne("p.findById",id);
sqlSession.close();
return p;
}
// 按照年龄查询
@Override
public List<Person> findByAge(int age) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Person> list = sqlSession.selectList("p.findByAge",age);
sqlSession.close();
return list;
}
@Override
public List<Person> findByAddress(String address) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Person> list = sqlSession.selectList("p.findByAddress",address);
sqlSession.close();
return list;
}
@Override
public List<Person> findByNameAndAge(Map<String, Object> map) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Person> list = sqlSession.selectList("p.findByNameAndAge",map);
sqlSession.close();
return list;
}
修改PersonServlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PersonService personService = new PersonServiceImpl();
// 获取id
int id = Integer.parseInt(request.getParameter("id"));
// 测试按照id查询
Person p = personService.findById(id);
if(p!=null) {
System.out.println(p.getName() + "===>" + p.getAge() + "===>" + p.getAddress());
}
}
index.jsp如下:
启动web容器测试:
点击第一个链接:
数据表:
没问题。
修改PersonServlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PersonService personService = new PersonServiceImpl();
// 获取年龄
int age = Integer.parseInt(request.getParameter("age"));
// 测试按年龄查询
List<Person> persons = personService.findByAge(age);
if(persons!=null && persons.size()>0) {
for(Person p:persons) {
System.out.println(p.getName() + "===>" + p.getAge() + "===>" + p.getAddress());
}
}
}
启动web容器测试:
点击第2个链接:
没问题。
修改PersonServlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PersonService personService = new PersonServiceImpl();
// 获取地址
String address = request.getParameter("address");
// 测试按照地址查询
List<Person> persons = personService.findByAddress(address);
if(persons!=null && persons.size()>0) {
for(Person p:persons) {
System.out.println(p.getName() + "===>" + p.getAge() + "===>" + p.getAddress());
}
}
}
启动web容器测试:
点击第3个链接:
没问题。
修改PersonServlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PersonService personService = new PersonServiceImpl();
// 按照姓氏和年龄查询
String name = request.getParameter("name");
int age = Integer.parseInt(request.getParameter("age"));
Map<String, Object> map = new HashMap<>();
map.put("name", name);
map.put("age", age);
List<Person> persons = personService.findByNameAndAge(map);
if(persons!=null && persons.size()>0) {
for(Person p:persons) {
System.out.println(p.getName() + "===>" + p.getAge() + "===>" + p.getAddress());
}
}
}
启动web容器测试:
点击最后一个链接:
没问题。
6.7、sql块重用
示例:
上面标记的地方是重复的地方,有时候需要在每个SQL语句中都要写相同的部分,这样挺多余的,可以使用<sql>
标签元素来定义重复的地方,然后使用<include>
标签元素引用定义的重复块即可。
修改如下:
<!-- 定义重复的sql块 -->
<sql id="sameFields">id,name,address,age</sql>
<!-- 按照指定id查询多条记录 -->
<select id="findById" resultType="per" parameterType="int">
select <include refid="sameFields" /> from person where id=#{id}
</select>
<!-- 查询指定年龄范围的记录 -->
<select id="findByAge" resultType="per" parameterType="int">
select <include refid="sameFields" /> from person where age>#{age}
</select>
<!-- 查询指定地址的的记录 -->
<select id="findByAddress" resultType="per" parameterType="string">
select <include refid="sameFields" /> from person where address like CONCAT(#{address},'%')
</select>
<!-- 查询指定年龄范围指定姓的记录 -->
<select id="findByNameAndAge" resultType="per" parameterType="map">
select <include refid="sameFields" /> from person where name like CONCAT(#{name},'%') and age>#{age}
</select>
重启web容器测试,点击最后一个链接:
没问题,使用sql块后还是可以正常查询。
注意:使用了sql块include包围时,不能再使用<![CDATA[]]>,因为无法解析,运行时会出错。即include与常量<![CDATA[]]>不能同时存在,只能使用两者中的一个。
6.8、缓存
数据缓存可以提高数据的访问操作效率,MyBatis提供了设置sql的缓存策略和在sql映射中引用缓存策略。MyBatis使用<cache>
元素在sql映射中设置缓存策略及使用<ref-cache>
引用缓存策略。缓存只对查询select起作用。
MyBatis中有4种缓存策略。如下:
- LRU:最近最少使用法(默认),移出最近较长周期内都没有被使用的对象。
- FIFO:先进先出,移出队列里较早的对象。
- SOFT:软引用,基于软引用规则,使用垃圾回收机制gc来移出对象。
- WEAK:弱引用,基于弱引用规则,使用垃圾回收机制来强制性的移出对象。
建议使用LRU和FIFO这两种缓存策略。
先指定一个缓存配置文件globalCache.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="global-cache">
<!-- 设置缓存策略为LRU -->
<cache eviction="LRU" readOnly="true" size="200"
flushInterval="60000" type=""></cache>
</mapper>
说明:配置了一个LRU类型缓存,60秒clear清空一次,存储200个对象结果或列表引用,并且返回的结果是只读read的。type属性用来指定自定义缓存的实现类,实现自定义缓存需要提供对org.mybatis.cache接口的实现(一般不需要,不建议初学者使用)。
然后需要在核心配置文件mybatis-config.xml中开启缓存和引入:
最后在需要使用缓存的sql映射文件中引入就行了:
这种用的其实是二级缓存。
MyBatis的缓存机制
MyBatis提供一级缓存和二级缓存。
一级缓存
一级缓存只对单个SqlSession生效。每一个SqlSession都是独享缓存数据。导致一级缓存查询失败的几个因素如下:
- 使用了不同的sqlSession查询。
- 同一个SqlSession,但是查询条件发生了变化。
- 两次查询之间的SqlSession执行了commit,clearCache。
- 两次查询之间SqlSession执行了insert、update、delete操作。
- select标签的flushCache属性设置成了true。
二级缓存
所有SqlSession可共享缓存数据。
导致二级缓存查询失败(不命中)的几个因素如下:
- 二级缓存未开启
<setting name="cacheEnabled" value="false">
。 - 第一次查询之后,SqlSession未执行commit或close操作,导致该查询的二级缓存还未生效。
- 两次查询之间,mapper(同一命名空间)执行了insert、update、delete操作。
- select标签的flushCache属性(默认为false)的值设为了true。
注意:insert、update、delete标签的flushCache属性默认为true时,此时会清除一级缓存和二级缓存。为false时,不会清除二级缓存,会清除一级缓存。
二级缓存的缺点:
- 针对一个表的操作不在他独立的namespace下执行(即单表操作)。
- 多表操作严重受限,导致获取的缓存数据发生不正确的情况。
- 缓存对象类型必须是序列化的实现。
- 数据量比较庞大时可能会出现更多的未知错误。
事实上,在实际开发中一般不会使用mybatis自带的缓存,而是使用第三方专业缓存工具代替MyBatis二级缓存,如Ehcache、Redis等。
6.9、resultMap元素
MyBatis通常在映射<select>
元素执行sql时使用returnType设置返回结果类型,有时需要对查询返回的结果进行特殊处理,<select>
元素提供了returnMap属性,为查询返回结果提供一种进行处理的更灵活的方式。returnMap属性是对sql映射中某个returnMap元素的引用,而returnMap元素定义决定了如何处理查询返回结果。
当数据表的字段名和定义的pojo对应属性名不一致时,如下:
表中字段名可以通过驼峰转换成pojo中的属性名,myBatis提供了驼峰命名自动转换的功能,这时需要在配置文件中开启:
下面测试查询。
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">
<!-- 根元素是mapper -->
<mapper namespace="d">
<!-- 查询所有部门 -->
<select id="findAllDeps" resultType="dep">
select dep_id,dep_name,dep_leader from department
</select>
</mapper>
service层:
public interface DepartmentService {
// 查询所有部门
List<Department> getAllDeps();
}
public class DepartmentServiceImpl implements DepartmentService{
@Override
public List<Department> getAllDeps() {
return new DepartmentDaoImpl().getAllDeps();
}
}
dao层:
public interface DepartmentDao {
List<Department> getAllDeps();
}
public class DepartmentDaoImpl implements DepartmentDao{
@Override
public List<Department> getAllDeps() {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
// 查询所有记录
List<Department> list = sqlSession.selectList("d.findAllDeps");
sqlSession.close();
return list;
}
}
Servlet组件:
@WebServlet("/DepartmentServlet")
public class DepartmentServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
DepartmentService depService = new DepartmentServiceImpl();
List<Department> depts = depService.getAllDeps();
if(depts!=null && depts.size()!=0) {
for(Department dep:depts) {
System.out.println(dep.getDepId() + "===>" + dep.getDepName() + "===>"
+ dep.getDepLeader());
}
}
}
}
配置Servlet组件:
<!-- 配置Servlet -->
<servlet>
<servlet-name>depServlet</servlet-name>
<servlet-class>com.ycz.model.servlet.DepartmentServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>depServlet</servlet-name>
<url-pattern>/departmentServlet.do</url-pattern>
</servlet-mapping>
index.jsp页面:
启动web容器测试:
点击链接:
控制台输出没问题。
如果将自动驼峰命名转换关闭,如下:
这里直接注释掉,然后再测试,点击链接:
报错了,因为查询的结果映射不到pojo,表的字段名和pojo属性名不一致,无法完成映射。在不开启驼峰自动转换的情况下,可以使用<resultMap>
标签元素来手动处理名称不一致的问题。
resultMap元素属性说明:
- id:元素的唯一标识,用来被其他元素引用。
- type:设置结果映射为哪种Java类型(可以使用别名)。
- autoMapping:设置是否将列名称自动映射为对象属性名,值可以是true或false。
- extends:设置元素继承哪个其他resultMap,以方便扩展。
resultMap子元素说明:
<id>
:设置表主键和Java对象唯一标识的关联。<result>
:设置表字段和Java对象属性的关联。<constructor>
:用来将结果反射给一个实例化好的类的构造器。<association>
:复杂类型的集合,适用于一对一的关联映射。<collection>
:复杂类型集合,适用于一对多的关联映射。<discriminator>
:适用一个结果值以决定适用哪个resultMap。
6.9.1、单表查询
在sql映射文件中配置resultMap元素,如下:
<!-- 配置resultMap元素,解决表字段和pojo属性名不一致的问题 -->
<resultMap id="rMap" type="com.ycz.model.pojo.Department" autoMapping="true">
<!-- id标签设置主键列的映射 -->
<!-- column是表字段名, property是pojo属性名,jdbcType是类型-->
<id column="dep_id" property="depId" jdbcType="INTEGER"/>
<result column="dep_name" property="depName" jdbcType="VARCHAR"/>
<result column="dep_leader" property="depLeader" jdbcType="VARCHAR"/>
</resultMap>
<!-- 查询所有部门 -->
<!-- 查询结果使用resultMap进行处理 -->
<select id="findAllDeps" resultMap="rMap">
select dep_id,dep_name,dep_leader from department
</select>
再测试,点击链接:
没有报错,查询结果正确。
注意:resultType和resultMap映射对查询结果的封装返回只能出现一个,否则会出现错误。
6.9.2、多表连接查询
多表连接查询一定会用到resultMap元素。多表查询又分为一对一和一对多。
(1)一对一查询
hill表引用了前面表的主键,即这两张表存在主外键关联关系。道教4大名山,每个省对应一座,这就是一对一关系。一对一可以在resultMap元素中使用<association>
子元素来关联映射。
下面进行查询。
pojo:
public class Hill {
private Integer id;
private String hillName;
private Integer provinceId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getHillName() {
return hillName;
}
public void setHillName(String hillName) {
this.hillName = hillName;
}
public Integer getProvinceId() {
return provinceId;
}
public void setProvinceId(Integer provinceId) {
this.provinceId = provinceId;
}
}
public class Province {
private Integer id;
private String provinceName;
// 省份对应的山
private Hill hill;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getProvinceName() {
return provinceName;
}
public void setProvinceName(String provinceName) {
this.provinceName = provinceName;
}
public Hill getHill() {
return hill;
}
public void setHill(Hill hill) {
this.hill = hill;
}
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">
<mapper namespace="ph">
<resultMap id="phMap" type="com.ycz.model.pojo.Province" autoMapping="true">
<id column="p_id" property="id" jdbcType="INTEGER"/>
<result column="p_name" property="provinceName" jdbcType="VARCHAR"/>
<!-- 一对一映射 -->
<association property="hill" javaType="com.ycz.model.pojo.Hill">
<result column="h_name" property="hillName" jdbcType="VARCHAR"/>
</association>
</resultMap>
<!-- 表连接查询 -->
<select id="findProHill" resultMap="phMap">
select p.id p_id,p.province_name p_name,h.hill_name h_name
from province p inner join hill h
on p.id = h.province_id
</select>
</mapper>
sql映射文件加载:
service层:
dao层:
@Override
public List<Province> findProHill() {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Province> list = sqlSession.selectList("ph.findProHill");
sqlSession.close();
return list;
}
修改Servlet:
@WebServlet("/PersonServlet")
public class PersonServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PersonService personService = new PersonServiceImpl();
List<Province> provinces = personService.findProHill();
if(provinces!=null) {
for(Province p:provinces) {
// 获取Hill对象
Hill hill = p.getHill();
System.out.println(p.getId() + "===>" + p.getProvinceName()
+ "===>" + hill.getHillName());
}
}
}
}
修改index.jsp:
启动web容器,访问测试:
一对一关联映射成功,查询结果没问题。
(1)一对多查询
部门表和员工表之间存在着主外键关联,部门与员工之间是一对多关系。一对多可以在resultMap元素中使用<collection>
子元素来关联映射。
修改Department实体类,添加一个字段,并提供set和get方法。如下:
修改departmentMapper.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 -->
<mapper namespace="d">
<resultMap id="deMap" type="com.ycz.model.pojo.Department" autoMapping="true">
<id column="dep_id" property="depId" jdbcType="INTEGER"/>
<result column="dep_name" property="depName" jdbcType="VARCHAR"/>
<!-- 一对多映射 -->
<collection property="empList" ofType="com.ycz.model.pojo.Employee">
<result column="emp_name" property="empName" jdbcType="VARCHAR"/>
<result column="gender" property="gender" jdbcType="CHAR"/>
<result column="email" property="email" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<!-- 查询每个部门的所有员工 -->
<select id="findDepEmps" resultMap="deMap">
select d.dep_id,dep_name,emp_name,gender,email
from department d
inner join employee e
on d.dep_id = e.dep_id
</select>
</mapper>
service层:
dao层:
@Override
public List<Department> getDepEmps() {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
// 查询每个部门的所有员工记录
List<Department> list = sqlSession.selectList("d.findDepEmps");
sqlSession.close();
return list;
}
修改Servlet:
@WebServlet("/DepartmentServlet")
public class DepartmentServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
DepartmentService depService = new DepartmentServiceImpl();
List<Department> depts = depService.getDepEmps();
if(depts!=null) {
for(Department dep:depts) {
System.out.println(dep.getDepName() + "所有员工如下:");
// 获取该部门下的所有员工
List<Employee> empList = dep.getEmpList();
if(empList!=null) {
for(Employee emp:empList) {
System.out.println(dep.getDepId() + "===>" + dep.getDepName()
+ "===>" + emp.getEmpName()
+ "===>" + (emp.getGender().equals("1") ?"男":"女")
+ "===>" + emp.getEmail());
}
}
System.out.println("------------------------------------");
}
}
}
}
修改index.jsp:
访问测试:
点击链接:
查询结果没问题。
MyBatis中,这种多表之间的一对一和一对多关系映射属于高级查询,开发中用的比较多,要十分熟练的掌握。
6.10、动态SQL
MyBatis3.2.7版本在前版本的基础上对动态SQL进行了简化,利用这些动态sql我们可以以灵活的根据实际情况而改变sql执行语义,在MyBatis3.2.7中主要有以下元素构成MyBatis中的动态SQL语句:if、choose、when、otherwise、trim、where、set、foreach。
6.10.1、if元素
<if>
标签元素是构成动态SQL语句非常重要的和使用比较频繁的元素,它表示执行一个逻辑判断,如果为true则执行if元素之内的内容,if元素有唯一的一个test属性用来接收以一个布尔表达式,if经常用来阻止where语句的连接生成。
示例:
<select id="findStuBySex" parameterType="student" resultType="student">
select * from student s
<if test="sex!=null">
where s.sex=#{sex}
</if>
</select>
以下代码进行测试。
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">
<!-- 根元素是mapper -->
<mapper namespace="e">
<!-- if动态元素查询 -->
<select id="findEmpBySex" resultType="emp" parameterType="string">
select * from employee
<if test="gender!=null">
where gender=#{gender}
</if>
</select>
<!-- 多重if -->
<select id="findEmpNameSex" resultType="emp" parameterType="map">
select * from employee where 1=1
<if test="gender!=null">
and gender=#{gender}
</if>
<if test="empName!=null">
and emp_name like CONCAT(#{empName},'%')
</if>
</select>
</mapper>
将驼峰命名自动转换开启:
service层:
public interface EmployeeService {
List<Employee> findEmpSex(String gender);
List<Employee> findEmpNameSex(Map<String, Object> map);
}
public class EmployeeServiceImpl implements EmployeeService{
@Override
public List<Employee> findEmpSex(String gender) {
return new EmployeeDaoImpl().findEmpSex(gender);
}
@Override
public List<Employee> findEmpNameSex(Map<String, Object> map) {
return new EmployeeDaoImpl().findEmpNameSex(map);
}
}
dao层:
public interface EmployeeDao {
// 按照性别查询员工
List<Employee> findEmpSex(String gender);
// 按照性别和姓氏查询员工
List<Employee> findEmpNameSex(Map<String, Object> map);
}
public class EmployeeDaoImpl implements EmployeeDao{
@Override
public List<Employee> findEmpSex(String gender) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Employee> list = sqlSession.selectList("e.findEmpBySex",gender);
return list;
}
@Override
public List<Employee> findEmpNameSex(Map<String, Object> map) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Employee> list = sqlSession.selectList("e.findEmpNameSex",map);
return list;
}
}
Servlet组件:
@WebServlet("/EmployeeServlet")
public class EmployeeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EmployeeService empService = new EmployeeServiceImpl();
String gender = null;
List<Employee> employees = empService.findEmpSex(gender);
System.out.println("一共找到" + employees.size() + "条符合条件的记录:");
if(employees!=null &&employees.size()>0) {
for(Employee emp:employees) {
System.out.println(emp.getEmpName() + "===>"
+ (emp.getGender().equals("1")?"男":"女"));
}
}
}
}
配置Servlet:
<!-- 配置Servlet -->
<servlet>
<servlet-name>empServlet</servlet-name>
<servlet-class>com.ycz.model.servlet.EmployeeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>empServlet</servlet-name>
<url-pattern>/employeeServlet.do</url-pattern>
</servlet-mapping>
访问测试:
控制台:
修改一下Servlet中的参数:
再查询:
再改一下参数:
查询结果:
可以看到使用动态SQL的好处,参数变化,但是接口和SQL语句可以不用改,SQL语句的通用性较强。
修改Servlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EmployeeService empService = new EmployeeServiceImpl();
Map<String,Object> map = new HashMap<>();
// 传两个参数
map.put("gender", "1");
map.put("empName", "刘");
List<Employee> employees = empService.findEmpNameSex(map);
System.out.println("一共找到" + employees.size() + "条符合条件的记录:");
if(employees!=null &&employees.size()>0) {
for(Employee emp:employees) {
System.out.println(emp.getEmpName() + "===>"
+ (emp.getGender().equals("1")?"男":"女"));
}
}
}
查询结果:
改成只传1个参数:
查询结果:
6.10.2、choose、when、otherwise元素
<choose>
、<when>
、<otherwise>
元素配合使用类似与Java中的switch分支语句。
示例:
<select id="findStudent" parameterType="Student" resultType="Student">
select * from student where 1=1
<choose>
<when test="name!=null">and name like '东%'</when>
<when test="age!=null">and age>35</when>
<otherwise>
<if test="cid!=null">and cid=#{cid}</if>
</otherwise>
</choose>
</select>
以下进行测试。
sql映射文件:
<!-- choose when分支动态语句 -->
<select id="findEmpByArgs" resultType="emp" parameterType="map">
select * from employee where 1=1
<!-- 只会选择一条分支 -->
<choose>
<when test="name!=null">
and emp_name like CONCAT(#{empName},'%')
</when>
<when test="gender!=null">
and gender=#{gender}
</when>
<when test="email!=null">
and email like CONCAT('%',#{email})
</when>
<otherwise>
<if test="empId!=null">
and emp_id=#{empId}
</if>
</otherwise>
</choose>
</select>
service层:
List<Employee> findEmpByArgs(Map<String, Object> map);
@Override
public List<Employee> findEmpByArgs(Map<String, Object> map) {
return new EmployeeDaoImpl().findEmpByArgs(map);
}
dao层:
// 按照参数查询
List<Employee> findEmpByArgs(Map<String, Object> map);
@Override
public List<Employee> findEmpByArgs(Map<String, Object> map) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Employee> list = sqlSession.selectList("e.findEmpByArgs",map);
return list;
}
修改Servlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EmployeeService empService = new EmployeeServiceImpl();
Map<String,Object> map = new HashMap<>();
map.put("email", "@163.com");
List<Employee> employees = empService.findEmpByArgs(map);
System.out.println("一共找到" + employees.size() + "条符合条件的记录:");
if(employees!=null &&employees.size()>0) {
for(Employee emp:employees) {
System.out.println(emp.getEmpName() + "===>" + emp.getEmail());
}
}
}
查询结果:
只查询163邮箱的用户,查询结果正确。
6.10.3、where元素
<where>
元素用来代替用户手写的where关键字,它基本可以解决没有给定条件时而出现错误的sql语句。
示例:
<select id="findActiveBlogLike" parameter="Blog" resultType="Blog">
select * from blog
<where>
<if test="state!=null">state=#{state}</if>
<if test="title!=null">and title like#{title}</if>
<if test="author!=null and author.name!=null">
and title like#{author.name}
</if>
</where>
</select>
以下进行测试。
sql映射文件:
<!-- where动态语句 -->
<select id="findByArgs2" resultType="emp" parameterType="map">
select * from employee
<where>
<if test="gender!=null">
and gender=#{gender}
</if>
<if test="email!=null">
and email like CONCAT('%',#{email})
</if>
</where>
</select>
service层:
List<Employee> findEmpByArgs2(Map<String, Object> map);
@Override
public List<Employee> findEmpByArgs2(Map<String, Object> map) {
return new EmployeeDaoImpl().findEmpByArgs2(map);
}
dao层:
// 按照参数查询
List<Employee> findEmpByArgs2(Map<String, Object> map);
@Override
public List<Employee> findEmpByArgs2(Map<String, Object> map) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Employee> list = sqlSession.selectList("e.findByArgs2",map);
return list;
}
修改Servlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EmployeeService empService = new EmployeeServiceImpl();
Map<String,Object> map = new HashMap<>();
map.put("gender", "0");
map.put("email", "@163.com");
List<Employee> employees = empService.findEmpByArgs2(map);
System.out.println("一共找到" + employees.size() + "条符合条件的记录:");
if(employees!=null &&employees.size()>0) {
for(Employee emp:employees) {
System.out.println(emp.getEmpName() + "===>"
+ (emp.getGender().equals("1")?"男":"女") + "===>"
+ emp.getEmail());
}
}
}
查询结果:
6.10.4、trim元素
<trim>
元素可以用来代替<where>
、<set>
等元素设置自定义动态sql语句。
它有3个属性。如下:
- prefix:指定前缀。
- suffix:指定后缀。
- prefixOverrides:设置分隔符(空格也属于分隔符之列)。
示例:
<select id="findActiveBolgLike" parameterType="Blog" resultType="Blog">
select * from blog
<trim prefix="where" prefixOverrides="AND|OR">
<if test="state!=null">state=#{state}</if>
<if test="title!=null">and title like #{title}</if>
<if test="author!=null and author.name!=null">
and title like #{author.name}
</if>
</trim>
</select>
直接修改刚才where的动态SQL语句,如下:
<!-- trim动态语句 -->
<select id="findByArgs2" resultType="emp" parameterType="map">
select * from employee
<!-- trim元素代替where元素 -->
<trim prefix="where" prefixOverrides="and|or">
<if test="gender!=null">
and gender=#{gender}
</if>
<if test="email!=null">
and email like CONCAT('%',#{email})
</if>
</trim>
</select>
其他不变,再查询:
效果是一样的。
6.10.5、set元素
<set>
元素用来实现update语句的更新语句,它取代set关键字设置更新信息,允许更新部分字段。
示例:
<update id="modifyEmployee" parameterType="emp" statementType="PREPARED">
update emp
<set>
<if test="name!=null">name=#{name},</if>
<if test="gender!=null">gender=#{gender},</if>
<if test="address!=null">address=#{address},</if>
<if test="address!=null">descs=#{descs},</if>
</set>
where id=#{id}
</update>
以下进行测试。
sql映射文件:
<!-- set元素动态更新 -->
<update id="updateEmpById" parameterType="emp" statementType="PREPARED">
update employee
<set>
<if test="empName!=null">
emp_name=#{empName},
</if>
<if test="gender!=null">
gender=#{gender},
</if>
<if test="email!=null">
email = #{email},
</if>
<if test="depId!=null">
dep_id=#{depId}
</if>
</set>
where emp_id=#{empId}
</update>
service层:
int updateEmp(Employee emp);
@Override
public int updateEmp(Employee emp) {
return new EmployeeDaoImpl().updateEmp(emp);
}
dao层:
// 更新员工信息
int updateEmp(Employee emp);
@Override
public int updateEmp(Employee emp) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
int res = sqlSession.update("e.updateEmpById",emp);
sqlSession.commit();
sqlSession.close();
return res;
}
修改Servlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EmployeeService empService = new EmployeeServiceImpl();
Employee employee = new Employee();
employee.setEmpId(2020001);
employee.setEmpName("云过梦无痕");
employee.setEmail("xxxxxx@163.com");
int res = empService.updateEmp(employee);
if (res > 0) {
System.out.println("id为2020001的记录更新成功!");
} else {
System.out.println("更新记录失败!");
}
}
访问Servlet测试:
查看表:
记录更新成功。
6.10.6、foreach元素
<foreach>
是一个迭代功能的动态sql元素,通常在条件包含in或not in时使用其填充条件。MyBatis会自动将有序集合及数组包装成一个Map处理,并使用它的名称作为key。List实例将使用"list"作为键,数组实例以"array"作为键。
示例:
<!-- 使用foreach迭代元素迭代参数集合-->
<select id="queryEmpListForAddress" resultType="emp" statementType="PREPARED">
select * from emp where address in
<!-- 使用数组array
<foreach collection="array" item="ele" index="index" separator="," open="(" close=")">
-->
<!-- 使用集合list -->
<foreach collection="list" item="ele" index="index" separator="," open="(" close=")">
#{ele}
</foreach>
</select>
以下进行测试。
sql映射文件:
<!-- foreach动态语句 -->
<select id="findEmpByAddress" resultType="emp" parameterType="map">
select * from employee where gender=#{gender} and dep_id in
<foreach collection="depList" item="dept" separator="," open="(" close=")">
#{dept}
</foreach>
</select>
service层:
List<Employee> findEmpByAddress(Map<String, Object> map);
@Override
public List<Employee> findEmpByAddress(Map<String, Object> map) {
return new EmployeeDaoImpl().findEmpByAddress(map);
}
dao层:
// 按照地址和性别查询
List<Employee> findEmpByAddress(Map<String, Object> map);
@Override
public List<Employee> findEmpByAddress(Map<String, Object> map) {
SqlSession sqlSession = SqlSessionManager.getSqlSession();
List<Employee> list = sqlSession.selectList("e.findEmpByAddress",map);
return list;
}
修改Servlet:
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
EmployeeService empService = new EmployeeServiceImpl();
Map<String,Object> map = new HashMap<>();
map.put("gender", 0);
List<Integer> depList = Arrays.asList(1001,1002);
map.put("depList",depList);
List<Employee> employees = empService.findEmpByAddress(map);
if(employees!=null && employees.size()>0) {
System.out.println("满足条件的记录一共有" + employees.size() + "条:");
for(Employee emp:employees) {
System.out.println(emp.getEmpName() + "===>"
+ emp.getGender() + "===>"
+ emp.getDepId());
}
}
}
查询结果:
只查询部门编号1001或1002性别0的记录,结果是正确的。
动态SQL在实际开发中应用较为广泛,需要十分熟练。