本文所用代码下载地址:spring-mybatis
1.MyBatis基础
1.JDBC连接数据库
- 注册驱动,获取连接
- 创建Statement对象
- execute()方法执行sql
- 把结果集转成POJO对象
- 关闭资源
下面以一个简单的例子来介绍:
以本地数据库来测试:
@Test
public void testJdbc() throws IOException {
Connection conn = null;
Statement stmt = null;
Blog blog = new Blog();
try {
// 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 打开连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatisdemo", "mybatistest", "123456");
// 执行查询
stmt = conn.createStatement();
String sql = "SELECT bid, name, author_id FROM blog where bid = 1";
ResultSet rs = stmt.executeQuery(sql);
// 获取结果集
while (rs.next()) {
Integer bid = rs.getInt("bid");
String name = rs.getString("name");
Integer authorId = rs.getInt("author_id");
blog.setAuthorId(authorId);
blog.setBid(bid);
blog.setName(name);
}
System.out.println(blog);
rs.close();
stmt.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (stmt != null) stmt.close();
} catch (SQLException se2) {
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
运行结果:
这里只是查询了表中的一条数据,但是在实际业务中可能有各种的select /update/delete/insert等操作,每次都需要 完成 注册驱动,获取连接,创建Statement对象这些重复的动作。特别是忘记写close的代码,很有可能会造成数据库连接资源耗尽。并且sql语句是直接耦合在业务代码中,如果业务逻辑有修改或数据库环境变化,又需要对业务代码进行修改,维护起来就非常的困难了。综合以上,我们很少用JDBC去直接访问数据库。
2.Spring JDBC
- 实现RowMapper接口,mapRow()方法
- 转换结果集返回Object
public List<Employee> query(String sql){
new JdbcTemplate(new DruidDataSource());
return jdbcTemplate.query(sql,new EmployeeRowMapper())
}
EmployeeRowMapper:
public class EmployeeRowMapper implements RowMapper {
@Override
public Object mapRow(ResultSet resultSet, int i) throws SQLException {
Employee employee = new Employee();
employee.setEmpId(resultSet.getInt("emp_id"));
employee.setEmpName(resultSet.getString("emp_name"));
employee.setGender(resultSet.getString("gender"));
employee.setEmail(resultSet.getString("email"));
return employee;
}
}
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class EmployeeRowMapperTest {
@Autowired
JdbcTemplate jdbcTemplate;
List<Employee> list;
@Test
public void EmployeeTest() {
list = jdbcTemplate.query("select * from tbl_emp", new EmployeeRowMapper());
System.out.println(list);
}
}
但是这种方法每一个实体类都需要一个Mapper类,也显得有些繁琐。那么,有没有一种方法可以使数据库的字段跟实体类的字段自动转换呢?
这里定义了一个通用的BaseRowMapper类,通过反射来实现
public class BaseRowMapper<T> implements RowMapper<T> {
private Class<?> targetClazz;
private HashMap<String, Field> fieldMap;
public BaseRowMapper(Class<?> targetClazz) {
this.targetClazz = targetClazz;
fieldMap = new HashMap<>();
Field[] fields = targetClazz.getDeclaredFields();
for (Field field : fields) {
fieldMap.put(field.getName(), field);
}
}
@Override
public T mapRow(ResultSet rs, int arg1) throws SQLException {
T obj = null;
try {
obj = (T) targetClazz.newInstance();
final ResultSetMetaData metaData = rs.getMetaData();
int columnLength = metaData.getColumnCount();
String columnName = null;
for (int i = 1; i <= columnLength; i++) {
columnName = metaData.getColumnName(i);
Class fieldClazz = fieldMap.get(camel(columnName)).getType();
Field field = fieldMap.get(camel(columnName));
field.setAccessible(true);
// fieldClazz == Character.class || fieldClazz == char.class
if (fieldClazz == int.class || fieldClazz == Integer.class) { // int
field.set(obj, rs.getInt(columnName));
} else if (fieldClazz == boolean.class || fieldClazz == Boolean.class) { // boolean
field.set(obj, rs.getBoolean(columnName));
} else if (fieldClazz == String.class) { // string
field.set(obj, rs.getString(columnName));
} else if (fieldClazz == float.class) { // float
field.set(obj, rs.getFloat(columnName));
} else if (fieldClazz == double.class || fieldClazz == Double.class) { // double
field.set(obj, rs.getDouble(columnName));
} else if (fieldClazz == BigDecimal.class) { // bigdecimal
field.set(obj, rs.getBigDecimal(columnName));
} else if (fieldClazz == short.class || fieldClazz == Short.class) { // short
field.set(obj, rs.getShort(columnName));
} else if (fieldClazz == Date.class) { // date
field.set(obj, rs.getDate(columnName));
} else if (fieldClazz == Timestamp.class) { // timestamp
field.set(obj, rs.getTimestamp(columnName));
} else if (fieldClazz == Long.class || fieldClazz == long.class) { // long
field.set(obj, rs.getLong(columnName));
}
field.setAccessible(false);
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
/**
* 下划线转驼峰
* @param str
* @return
*/
public static String camel(String str) {
Pattern pattern = Pattern.compile("_(\\w)");
Matcher matcher = pattern.matcher(str);
StringBuffer sb = new StringBuffer(str);
if(matcher.find()) {
sb = new StringBuffer();
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
matcher.appendTail(sb);
}else {
return sb.toString();
}
return camel(sb.toString());
}
}
测试代码:
@Test
public void EmployeeTest() {
list = jdbcTemplate.query(" select * from tbl_emp", new BaseRowMapper(Employee.class));
System.out.println(list);
}
可以看到,效果是一样的
总结:
工具类解决了以下问题:
-
方法封装
-
支持数据源,不用手动创建和关闭
// 支持传入DataSource
public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
this.setDataSource(dataSource);
this.setLazyInit(lazyInit);
this.afterPropertiesSet();
}
- 映射结果集
未解决问题:
- SQL语句硬编码
- 参数只能按顺序传入(占位符)
- 没有实现实体类到数据库记录的映射
- 没有提供缓存等功能。
2. ORM
mybatis特性:
- 使用连接池对连接进行管理
- SQL和代码分离,集中管理(mapper.xml)
- 参数映射和动态SQL
- 结果集映射
- 缓存管理
- 重复SQL提取<sql>
- 插件机制
如何选择ORM框架??
- 业务简单的项目可以用Hibernate
- 需要灵活的SQL,可以用MyBatis
- 对性能要求高,可以使用JDBC
- Spring JDBC可以和ORM框架混用
3. MyBatis应用
3.1 MyBatis工作原理
-
读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
-
加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
-
构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
-
创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
-
Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
-
MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
-
输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
-
输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
3.2 Spring 集成Mybatis
3.2.1 引入相关的jar(pom.xml)
<!--mybatis 和Spring整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
3.2.2 创建sqlSessionFactory (applicationContext.xml)
<!-- 在Spring启动时创建 sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- mybatis配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<!-- 映射器文件 -->
<property name="mapperLocations" value="classpath:/*.xml"></property>
<!-- Spring 连接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
Spring的连接池
<!-- Spring的连接池 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${driverClassName}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>
mybatis配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--全局配置文件-->
<configuration>
<!-- 参数设置 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />
</settings>
<!-- 类型别名 -->
<!-- <typeAliases>
<package name="com.liulin.mybatis.crud.bean"/>
<!–
<typeAlias alias="Department" type="Department" />
–>
</typeAliases>-->
<!--分页插件的注册-->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不设置该参数 ,可以自动识别
<property name="dialect" value="mysql"/> -->
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=start;pageSize=limit;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="true"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="check"/>
</plugin>
</plugins>
</configuration>
映射器文件
<?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="com.liulin.mybatis.dao.DepartmentMapper">
<resultMap id="BaseResultMap" type="com.liulin.mybatis.bean.Department">
<id column="dept_id" jdbcType="INTEGER" property="deptId"/>
<result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
</resultMap>
<sql id="Base_Column_List">
dept_id, dept_name
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from tbl_dept
where dept_id = #{deptId,jdbcType=INTEGER}
</select>
<select id="selectByMap" parameterType="java.util.Map" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
FROM tbl_dept e
where 1=1
<if test="did != null">
and d_id = #{did,jdbcType=INTEGER}
</if>
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
delete from tbl_dept
where dept_id = #{deptId,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="com.liulin.mybatis.bean.Department">
insert into tbl_dept (dept_id, dept_name)
values (#{deptId,jdbcType=INTEGER}, #{deptName,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" parameterType="com.liulin.mybatis.bean.Department">
insert into tbl_dept
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="deptId != null">
dept_id,
</if>
<if test="deptName != null">
dept_name,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="deptId != null">
#{deptId,jdbcType=INTEGER},
</if>
<if test="deptName != null">
#{deptName,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.liulin.mybatis.bean.Department">
update tbl_dept
<set>
<if test="deptName != null">
dept_name = #{deptName,jdbcType=VARCHAR},
</if>
</set>
where dept_id = #{deptId,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="com.liulin.mybatis.bean.Department">
update tbl_dept
set dept_name = #{deptName,jdbcType=VARCHAR}
where dept_id = #{deptId,jdbcType=INTEGER}
</update>
</mapper>
3.2.3 配置扫描器,将mybatis的接口实现加入到 IOC容器中(applicationContext.xml)
方法1:
<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.liulin.mybatis.dao"/>
</bean>
方法2:
<mybatis-spring:scan base-package="com.liulin.mybatis.dao"/>
启动项目,就可以对员工信息进行增删改查操作了