MyBatis
1.框架的作用
在Java实现数据库编程时,主要通过【JDBC】来实现,而JDBC相关代码在实现过程中【流程相对固定】,不论哪种数据操作,代码方面差距不大,所以,就出现了各种简化开发的持久层框架,常见的有【Hibernate】和【MyBatis】。MyBatis是持久层框架,使用MyBatis框架时,无需编写JDBC相关代码,只需要为某些抽象方法配置其对应的需要执行的SQL语句即可。
2.使用MyBatis框架实现数据访问
2.1 创建数据库与数据表
CREATE TABLE t_user (
id INT AUTO_INCREMENT,
username VARCHAR(20) UNIQUE NOT NULL,
password VARCHAR(20) NOT NULL,
age INT,
phone VARCHAR(20),
email VARCHAR(20),
PRIMARY KEY(id)
) DEFAULT CHARSET=UTF8;
2.2 创建项目
生成部署描述文件【web.xml】,添加【Tomcat环境】,【添加依赖】,配置【web.xml】文件,配置文件【spring-dao.xml】,创建数据库连接配置文件【db.properties】
添加相关依赖
<dependencies>
<!-- 添加Spring相关依赖 -->
<!-- 在同一个项目中,如果需要使用多个以spring-作为前缀的依赖,则应该使用相同的版本 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.10.RELEASE</version>
</dependency>
<!-- 添加MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.0</version>
</dependency>
<!-- 将Spring与MyBatis整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 添加数据库连接池依赖 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<!-- 添加单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- 添加依赖便于操作数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
</dependencies>
配置【web.xml】中的【DispatcherServlet】和【CharacterEncodingFilter】
<!-- 配置前端处理器 -->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<!-- 配置SpringMVC框架自带的字符编码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
在source文件夹下新建配置文件【spring-dao.xml】
<?xml version="1.0" encoding="UTF-8"?>
<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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd">
<!-- 读取db.properties中的配置 -->
<util:properties id="db" location="classpath:db.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="url" value="#{db.url}" />
<property name="driverClassName" value="#{db.driver}" />
<property name="username" value="#{db.username}" />
<property name="password" value="#{db.password}" />
<property name="initialSize" value="#{db.initialSize}" />
<property name="maxActive" value="#{db.maxActive}" />
</bean>
<!-- 配置会话工厂SqlSessionFactory -->
<bean class="org.mybatis.spring.mapper.SqlSessionFactory">
<!-- 指定XML文件的位置 -->
<property name="mapperLocations" value="classpath:mappers/*.xml" />
<!-- 使用哪个数据源 ref与上边数据源配置id一致-->
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
数据库连接配置文件【db.properties】
url=jdbc:mysql://localhost:3306/hzxh_ums?userUnicode=true&characterEncoding=utf-8
driver=com.mysql.jdbc.Driver
username=root
password=root
initialSize=3
maxActive=5
单元测试,获取连接对象
public class ConnectionTest {
BasicDataSource dataSource;
@Test
public void getConnection() throws SQLException {
//加载Spring配置文件,获取Spring容器
AbstractApplicationContext ac = new ClassPathXmlApplicationContext("spring-dao.xml");
//从Spring容器中获取对象
dataSource = ac.getBean("dataSource",BasicDataSource.class);
//获取连接对象
Connection conn = dataSource.getConnection();
//测试输出
System.out.println(conn);
//释放资源
ac.close();
}
}
2.3 接口与抽象方法
创建User实体类,在类中声明属性,与数据库中属性对应,数据库中每张表都要有一实体类与之对应
public class User implements Serializable{
private Integer id;
private String username;
private String password;
private Integer age;
private String phone;
private String email;
//添加get/set/toString方法
//根据id区分不同User对象,针对id重写hashCode及equals方法
}
创建【UserMapper】接口,并在接口中声明方法
在使用MyBatis时,如果抽象方法对应需要执行的数据操作是增、删、改,则抽象方法的返回值应该是【Integer】,表示受影响的行数;抽象方法的名称可以自由定义,符合语法规范并能表达语义即可;抽象方法的参数应该根据执行SQL语句时所需要的参数来决定。
public interface UserMapper {
/**
* 插入用户数据
* @param user 用户数据
* @return 受影响行数
*/
Integer addNew(User user);
/**
* 根据id删除数据
* @param id 用户的id
* @return 受影响的行数
*/
Integer deleteById(Integer id) ;
/**
* 修改用户密码
* @param id 用户id
* @param password 要修改的密码
* @return 受影响的行数
*/
Integer updatePassword(Integer id,String password);
}
在Spring配置文件中配置接口扫描【MapperScannerConfigurer】,用于扫描接口文件
<!-- MapperScannerConfigurer -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定接口文件所在的根包 -->
<property name="basePackage" value="cn.hzxh.mybatis" />
</bean>
配置映射
在【resource】下创建名为【mappers】的文件夹,并在该文件夹下创建映射配置文件【UserMapper.xml】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- namespace:匹配java的接口 -->
<mapper namespace="cn.hzxh.mybatis.mapper.UserMapper">
<!-- 根据要执行的SQL语句的类型选择节点 -->
<!-- id:对应抽象方法的名称 -->
<!-- 在SQL语句中的?应该使用#{ }来占位 -->
<!-- 在#{ }中的名称是参数User类中的属性名 -->
<insert id="addNew">
INSERT INTO t_user(
username,password,age,phone,email
)VALUES(
#{username},#{password},#{age},#{phone},#{email}
)
</insert>
<!-- insert节点可以自动配置自增主键,需要加入以下配置 -->
<!-- useGenerateKeys表示要不要获取自动编号的值 -->
<!-- keyProperties表示获取以后的值封装在试题类中的哪个属性中 -->
<insert id="insert" useGenerateKeys="true" keyProperty="id" >
<delete id="deleteById">
DELETE FROM
t_user
WHERE
id = #{id}
</delete>
<update id="updatePassword">
UPDATE
t_user
SET
password = #{password}
WHERE
id = #{id}
</update>
</mapper>
为了使得MyBatis框架能找到这些XML的映射文件并执行其中的SQL语句,还需要在Spring-dao.xml文件中添加配置
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 映射XML文件的位置 -->
<property name="mapperLocations" value="mappers/*.xml" />
<!-- 指定数据源,取值将引用(ref)以上配置的数据源的ID -->
<property name="dataSource" ref="dataSource"/>
</bean>
可以编写测试类了
@Test
public void addnew() {
AbstractApplicationContext ac = new ClassPathXmlApplicationContext("spring-dao.xml");
UserMapper userMapper = ac.getBean("userMapper", UserMapper.class);
User user = new User();
user.setUsername("root");
user.setPassword("1234");
user.setAge(18);
user.setPhone("13800138001");
user.setEmail("root@tedu.cn");
Integer rows = userMapper.addnew(user);
System.out.println("rows=" + rows);
ac.close();
}
2.4.多参数的处理
MyBatis框架只识别抽象方法中的1个参数,即:抽象方法应该最多只有1个参数!当抽象方法只有一个参数时,如:
public void deleteById(Integer id);
此时,映射文件中占位符号可以写任意值,如:
<delete id="deleteById">
DELETE FROM t_user WHERE id=#{helloworld}
</delete>
因为参数只有1个,框架会自动的读取对应位置的参数并应用于执行SQL语句,所以,参数的名称根本就不重要,另外,每个【.java源文件】在被编译成为【.class文件】之后,也都会丢失参数名称!当有多个参数时,一般的处理方式可以将抽象方法的参数设计为1个Map,则可以满足【参数只有1个】的需求,根据参数名称向该Map中put对应的属性值,但这种方式使用时非常不方便,因为方法的调用者并不明确向Map中封装数据时应该使用什么名称的key!
一般使用以下的方法:
在设计抽象方法时,允许使用多个参数,但是,每个参数之前都应该添加该注解,并在注解中设置参数的名称,MyBatis框架在处理时,会将这些参数封装为一个Map,也符合框架只有一个参数的约束,同时代码也能变的易于理解。
/**
* 修改用户密码
* @param id 用户id
* @param password 要修改的密码
* @return 受影响的行数
*/
Integer updatePassword(
@Param("id") Integer id,
@Param("password") String password);
//注解中的值要与配置文件中占位符号的参数名称保持一致
最终测试类
public class UserMapperTestCase {
public UserMapper userMapper;
public AbstractApplicationContext ac;
//@Before注解:执行@Test方法之前执行
@Before
public void doBefore() {
ac = new ClassPathXmlApplicationContext("spring-dao.xml");
userMapper = ac.getBean("userMapper",UserMapper.class);
}
@Test
public void testUpdate() {
AbstractApplicationContext ac = new ClassPathXmlApplicationContext("spring-dao.xml");
UserMapper userMapper = ac.getBean("userMapper",UserMapper.class);
//两个参数,数据正常修改
Integer row = userMapper.updatePassword(1, "123123");
System.out.println(row);
}
//@After注解:执行@Test方法之后执行
@After
public void doAfter() {
ac.close();
}
}
2.5 查询方法
查询方法在配置文件【spring-dao.xml】中配置的节点为【select】节点
<!-- 查询结果为单一对象 -->
<select id="findById" resultType="cn.hzxh.mybatis.User">
SELECT
id,username,password,age,phone,email
FROM
t_user
WHERE
id = #{id}
</select>
<!-- 查询结果为集合 -->
<select id="findAll" resultType="cn.hzxh.mybatis.User">
SELECT
id,username,password,age,phone,email
FROM
t_user
</select>
需注意的是:无论多么简单的返回值类型,都必须配置【resultType】或【resultMap】。当抽象方法的返回值类型为【List】时,MyBatis框架底层会调用【selectOne】方法,查询结果依然是实体类的对象,而且会将查询结果封装为一个集合,因此查询结果为多条数据时,返回值类型也不需要改变。
【Mapper】接口中配置的抽象方法为
/**
* 通过id查询用户
* @param id 用户id
* @return 有数据返回该用户,没有数据返回null值
*/
User findById(Integer id);
/**
* 查询所有用户
* @return 所有数据的集合
*/
List<User> findAll();
在配置抽象方法时,返回值的类型应该根据查询需求来决定,即:调用该方法后希望得到什么的数据。如:
获取当前表中的数据量:
Integer conutById();
<select id="countById"
resultType="java.lang.Integer">
SELECT COUNT(id) FROM t_user
</select>
3.动态SQL
在配置映射时,可以添加例如【if】【foreach】等节点,可以根据参数的不同,从而生成不同的SQL语句。
3.1 关于【foreach】
如有下列需求:批量删除用户数据,需执行SQL语句为 DELETE FROM t_user WHERE id IN (3,4,5);
在MyBatis中,如果实现以上功能,首先还是要设计【抽象方法】
/**
* 根据多个用户id删除数据
* @param ids 用户id数组
* @return受影响行数
*/
Integer deleteByIds(Integer[] ids);
【UserMapper.xml】文件配置
<delete id="deleteByIds">
DELETE FROM
t_user
WHERE
id IN (
<foreach collection="array" item="id" separator=",">
#{id}
</foreach>
)
</delete>
【foreach】中个节点中各属性的配置
--【collection】:被遍历的集合,当抽象方法只有1个参数时,取值根据参数类型选择【array】或【list】,如上文的文件配置,但有多个参数时,取值应该为【@Param】注解中的名称。如下:
/**
* 批量修改用户密码
* @param ids 用户数组id
* @return 受影响行数
*/
Integer updataPasswordByIds(
@Param("ids")Integer[] ids,
@Param("newPassword")String newPassword) ;
<update id="updataPasswordByIds">
UPDATE
t_user
SET
password = #{newPassword}
WHERE
id IN (
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</update>
--【item】:遍历过程中元素的名称;
--【seperater】:分隔符,在IN语句中,分隔符为逗号",";
--【open】及【close】:遍历生成的代码区域左侧与右侧的值,如IN语句两端应为括号【 open="(" close=")"】;
3.2 关于【if】
常用于单表查询,可以动态添加多个查询条件,如:
<select id="findUserList" resultType="cn.hzxh.mybatis.User">
SELECT
*
FROM
t_user
<if test="where != null">
WHERE
#{where}
</if>
<if test="orderBy != null">
ORDER BY
#{orderBy}
</if>
<if test="offset != null">
LIMIT
#{offset},#{count}
</if>
</select>
需注意的是,当需要检测的条件中,含有【?】占位符时,可以使用【#{ }】表达式,但当不含【?】占位符时,获取参数需要使用【${ }】符号,即,使用【#{ }】在执行时是【预编译】的,而【${ }】只是单纯的通过字符串拼接形成的最终的SQL语句,可能存在SQL注入风险!
归纳:在SQL语句中,查询条件为值的,要使用【#{ }】占位,为表达式的,使用【${ }】占位,如:
SELECT uname ,age FROM t_user WHERE age = 18 LIMIT 20,10;
//WHERE 后的条件为【${where}】,LIMIT后的条件为【#{offset},#{count}】
因此,上述配置中,【where】及【orderBy】前的符号应该改为【$】,以上查询方式几乎可以实现任何形式的单表查询,并不含聚合函数的查询需求,但此种方式不建议使用,主要原因为:
(1)执行效率低,会查询多余的部分数据,造成性能、内存的浪费,如【SELECT *】;
(2)使用【${ }】较多,均为字符串拼接,为防止SQL注入,会增加更多的判断,如【x' or ' a ' = ' a '】;
对应抽象方法
/**
* 多条件查询
* @param where 增加查询条件
* @param orderBy 增加排序条件
* @param offset 增加分页信息
* @param count 增加分页信息
* @return 对应用户列表
*/
List<User> findUserList(
@Param("where")String where,
@Param("orderBy")String orderBy,
@Param("offset")Integer offset,
@Param("count")Integer count);
3.3 关于【resultMap】
在设计查询时,【select】节点必须使用【resultType】或【resultMap】中的一项,通常多表查询,使用【resultMap】。
创建对应的【UserVO】类:
public class UserVO {
private Integer id;
private String username;
private String password;
private Integer age;
private String phone;
private String email;
private String departmentName;
// GET/SET/toString方法
}
设计抽象方法,返回值类型为【UserVO】:
/**
* 根据id查找用户
* @param id 用户id
* @return 符合条件的用户
*/
UserVO findUserById(Integer id);
配置文件【userMapper.xml】
<select id="findUserById" resultType="cn.hzxh.mybatis.UserVO">
SELECT
id,username,password,age,phone,email,name AS departmentName
FROM
t_user
LEFT JOIN
t_department
ON
t_user.did = t_department.did
WHERE
t_user.id = #{id}
</select>
创建测试类:
@Test
public void testUserVO() {
Integer id = 6;
UserVO uservo = userMapper.findUserById(id);
System.out.println(uservo);
}
如果查询某一个部门的详情信息,应该只返回1个结果,但通过SQL查询语句,一般都会得到2个以上的数据条数,无法赋值给一个对象,【resultMap】可以将多个查询结果封装到同一个对象中。下面介绍【resultMap】的用法。
创建数据库表【t_user】及【t_department】并完成数据初始化
创建【DepartmentVO】类
public class DepartmentVO {
private Integer did;
private String name;
private List<User> users;
//GET/SET/toString方法
}
创建【DeptMapper.xml】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- namespace:匹配java的接口 -->
<mapper namespace="cn.hzxh.mybatis.mapper.DeptMapper">
<!-- 当前resultMap用于指导mybatis将多条查询对象结果封装到同一个对象中 -->
<resultMap id="DepartmentVOMap" type="cn.hzxh.mybatis.DepartmentVO">
<!-- id节点:用于配置主键 -->
<!-- column属性:查询结果中的列名 -->
<!-- property属性:resultMap中type对应的数据类型中属性的名称 -->
<id column="did" property="did" />
<!-- result节点用于配置主键以外的其他属性 -->
<result column="name" property="name"/>
<!-- collection节点:用于配置1对多关系的数据 -->
<!-- property属性:集合名称, ofType属性:集合中所存元素的类型 -->
<collection property="users" ofType="cn.tedu.mybatis.User" >
<id column="id" property="id" />
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="age" property="age"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
</collection>
</resultMap>
<select id="findDepartmentById" resultMap="DepartmentVOMap">
SELECT
t_department.did,id,username,password,age,phone,email,name
FROM
t_user
LEFT JOIN
t_department
ON
t_user.did = t_department.did
WHERE
t_department.did = #{did}
</select>
</mapper>
在配置【resultMap】时,关于唯一字段的配置,应该使用【id】节点进行配置,虽然【result】节点也能实现相同功能,但时MyBatis是天生自带缓存的,使用【id】节点配置的数据,会作为缓存数据的标识,而使用【result】节点配置的数据则不会。
创建【DeptMapper】接口并声明抽象方法
public interface DeptMapper {
/**
* 根据id查找数据
* @param did depart数据
* @return 查询结果
*/
DepartmentVO findDepartmentById(Integer did);
}
创建测试类并测试
public class DeptMapperTestCase {
public DeptMapper deptMapper;
public AbstractApplicationContext ac;
@Before
public void doBefore() {
ac = new ClassPathXmlApplicationContext("spring-dao.xml");
deptMapper = ac.getBean("deptMapper",DeptMapper.class);
}
@Test
public void testFindDeptById() {
Integer did = 3;
DepartmentVO dept = deptMapper.findDepartmentById(did);
System.out.println(dept.getUsers().size());
}
@After
public void doAfter() {
ac.close();
}
}
【resultMap】另外一个作用是,当查询结果出现字段名与实体类属性名称无法对应的情况,如【SELECT * FROM t_user】,c此时,查询的结果全部为数据库字段名,不一定与实体类完全相匹配,此时需要在实体类对应的【mapper.xml】文件中配置相关节点属性。
<!-- resultMap节点:指导MyBatis如何封装查询结果 -->
<!-- id:自定义名称 -->
<!-- column:查询结果中的列名 property:实体类中的属性名 -->
<resultMap id="UserEntityMap" type="cn.hzxh.mybatis.User">
<result column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="age" property="age" />
<result column="phone" property="phone" />
<result column="email" property="email" />
<result column="is_delete" property="isDelete" />
</resultMap>
至此,MyBatis框架已可以满足大多数开发需求。