MyBatis框架

2. MyBatis框架

2.1. 作用

解决持久层数据处理的问题,主要是基于JDBC技术的原生代码比较繁琐,没有经过任何优化,开发甚至执行效率低下!

使用MyBatis框架时,不必关心JDBC技术如何实现,只需要编写需要执行的操作的抽象方法,例如User findById(Integer id),然后,为这个方法映射所需执行的SQL语句即可。

2.2. 第1次使用MyBatis插入数据

前提:在数据库系统中创建tedu_ums数据库,且存在t_user

步骤1:创建项目

创建时,Artifact IdMYBATISGroup Idcn.tedu.mybatis

创建过程与前次课程相同。

步骤2:创建实体类

通常,每张数据表都有一个与之对应的实体类,在实体类中,有相同数量的属性,数据类型应该保持一致,属性名称与字段名应该一一对应(在Java中的属性名称应该采用驼峰命名法,而数据库领域中并不区分大小写),所有的属性都应该是私有的,且都存在公有的SET/GET方法,整个实体类应该是实现了Serializable接口的!

步骤3:添加依赖

<!-- MyBatis -->
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis</artifactId>
	<version>3.4.6</version>
</dependency>

<!-- MyBatis与Spring整合 -->
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-spring</artifactId>
	<version>1.3.2</version>
</dependency>

<!-- Spring JDBC,版本需要与spring-webmvc保持一致 -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>4.3.9.RELEASE</version>
</dependency>

<!-- MySQL连接 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.13</version>
</dependency>

<!-- 数据库连接池 -->
<dependency>
	<groupId>commons-dbcp</groupId>
	<artifactId>commons-dbcp</artifactId>
	<version>1.4</version>
</dependency>

<!-- Junit单元测试 -->
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.9</version>
</dependency>

注意:下载的新的依赖的jar可能是损坏的文件,如果保证代码正确的前提下,无法得到预期的运行效果,应该删除本地仓库中的jar包并重新更新!

步骤4:配置数据库连接

src\main\resources下创建db.properties文件,用于配置数据库连接的相关信息:

# data-source
url=jdbc:mysql://localhost:3306/tedu_ums?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
driver=com.mysql.cj.jdbc.Driver
username=root
password=root
initialSize=2
maxActive=10

然后,在src\main\resources下使用spring-dao.xml配置:

<!-- 读取db.properties -->
<util:properties id="dbConfig"
	location="classpath:db.properties" />

<!-- 配置数据源 -->
<bean class="org.apache.commons.dbcp.BasicDataSource">
	<!-- driverClassName是BasicDataSource中定义的名称 -->
	<property name="driverClassName"
		value="#{dbConfig.driver}" />
	<property name="url"
		value="#{dbConfig.url}" />
	<property name="username"
		value="#{dbConfig.username}" />
	<property name="password"
		value="#{dbConfig.password}" />
	<property name="initialSize"
		value="#{dbConfig.initialSize}" />
	<property name="macActive"
		value="#{dbConfig.maxActive}" />
</bean>

步骤5:编写方法

使用MyBatis时,无需自行编写JDBC相关代码,只需要创建Java接口文件,并将需要执行的数据操作的抽象方法添加在接口中即可!

通常,建议按照“增 > 查 > 删 > 改”的顺序开发相关功能。

目标:向数据表中插入新的用户数据。

则创建cn.tedu.mybatis.mapper.UserMapper接口,然后,添加“插入新的用户数据”的抽象方法:

Integer insert(User user);

使用MyBatis时,执行的增、删、改操作均返回Integer,表示受影响的行数。

步骤6:编写XML映射

使用MyBatis时,还需要与接口的抽象方法对应的SQL语句,该SQL语句是在XML文件中配置的!

UserMapper.xml

通常,接口文件的数量与XML映射文件的数量是相同的,是一一对应的!

映射的XML文件应该存放到src\main\resources下,但是,项目中可能存在多个映射文件,为了便于管理,会在resources下创建mappers文件夹,然后把映射的XML文件放在这个文件夹中。

然后,配置该XML映射文件:

<!-- namespace:映射的接口的全名 -->
<mapper namespace="cn.tedu.mybatis.mapper.UserMapper">

	<!-- 根据执行的SQL种类选择节点 -->
	<!-- id:抽象方法的名称 -->
	<!-- parameterType:抽象方法的参数类型 -->
	<!-- SQL语句中的参数使用#{}框住User类中的属性名 -->
	<!-- SQL语句不需要使用分号表示结束 -->
	<insert id="insert"
		parameterType="cn.tedu.mybatis.entity.User">
		INSERT INTO t_user (
			username, password,
			age, phone, email
		) VALUES (
			#{username}, #{password}, 
			#{age}, #{phone}, #{email}
		)
	</insert>

</mapper>

步骤7:完成MyBatis的配置

<!-- MyBatis:MapperScannerConfigurer -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<!-- 接口文件在哪里 -->
	<property name="basePackage"
		value="cn.tedu.mybatis.mapper" />
</bean>

<!-- MyBatis:SqlSessionFactoryBean -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
	<!-- 数据源 -->
	<property name="dataSource"
		ref="dataSource" />
	<!-- XML文件在哪里 -->
	<property name="mapperLocations"
		value="classpath:mappers/*.xml" />
</bean>

步骤8:执行单元测试

public class UserMapperTestCase {

	@Test
	public void insert() {
		AbstractApplicationContext ac
			= new ClassPathXmlApplicationContext(
					"spring-dao.xml");
		
		UserMapper userMapper
			= ac.getBean("userMapper", UserMapper.class);
		
		User user = new User();
		user.setUsername("mapper");
		user.setPassword("1234");
		user.setAge(31);
		user.setPhone("13900139001");
		user.setEmail("mapper@tedu.cn");
		
		Integer rows = userMapper.insert(user);
		System.out.println("rows=" + rows);
		
		ac.close();
	}
	
}

2.3. 获取当前表中所有用户数据

分析需要执行的SQL语句:

select * from t_user

设计抽象方法:

List<User> findAll();

在查询时,MyBatis会得到List集合类型的结果,如果存在匹配的数据,则全部封装在该List集合中,如果没有匹配的数据,则返回的List集合是长度为0的集合。

配置映射:

<select id="findAll"
	resultType="xx.xx.xx.User">
	select * from t_user
</select>

任何查询都必须指定resultType属性,当然,也可以替换为resultMap

2.4. 根据用户名查询用户数据

分析需要执行的SQL语句:

select * from t_user where username=?

设计抽象方法:

User findByUsername(String username);

查询时,如果返回值声明为某数据类型,却不是List集合,MyBatis会尝试从List集合中取出第1个元素,如果存在,则返回该元素,如果不存在(没有匹配的结果,List集合的长度为0),则返回null

配置映射:

<select id="findByUsername"
	resultType="xx.xx.xx.User">
	select * from t_user where username=#{username}
</select>

2.5. 获取t_user表中数据的数量

分析需要执行的SQL语句:

select count(id) from t_user

设计抽象方法:

Integer getCount();

在查询时,查询结果与返回值的类型相匹配即可!所以,如果查询的是数据的数量,查询结果是数字,所设计的抽象方法的返回值就可以是Integer。

配置映射:

<select id="getCount"
	resultType="java.lang.Integer">
	select count(id) from t_user
</select>

目前,所设计的抽象方法只允许存在最多1个参数!

2.6. 小结

  1. MyBatis使用简单,可以简化开发,开发者不必关注数据库编程的细节;

  2. 使用MyBatis编程主要做好:(1)设计SQL语句; (2)设计抽象方法; (3)配置映射;

  3. 所有的xx.propertiesspring-dao.xml中的配置,需要理解,需要掌握修改配置值,不需要记住;

  4. 关于抽象方法:如果是增删改操作,返回值固定设计为Integer,如果是查询操作,根据查询结果来决定,例如可能是List<User>UserInteger……;方法的名称应该尽量对应所执行的数据操作,而不应该是某个业务,例如插入数据的方法名可以是insert,或者addnew,但是不应该使用reg;目前,只允许使用最多1个参数,如果一定要使用多个,请封装为1个参数;

  5. 关于映射配置:根据所执行的操作选择<insert><delete><update><select>节点,无论是哪个节点,必须配置id,取值为对应的抽象方法的名称;在SQL语句中的参数使用#{}框住即可;

  6. 关于执行结果:如果是增删改操作,只能获得受影响的行数,可以根据该结果判断操作成功与否;如果是执行查询操作,当结果声明为List时,无论是否查询到数据,返回有效的List集合对象,如果没有匹配的数据,则List的长度为0;当结果声明为某个对象,例如User时,如果没有匹配的数据,则返回null

1. 执行插入数据时获取自增长的id

当需要获取新增的数据的id时,首先,需要在<insert>节点中添加2个属性:

useGeneratedKeys="true"
keyProperty="id"

以上配置中,useGeneratedKeys表示获取自增长的键(自增长的字段的值),keyProperty表示键的属性名,即对应的类中的属性名(即该idUser类中的id,并非t_user表中的id)!

添加以上配置之后,插入数据操作的返回值依然表示“受影响的行数”,但是,用于执行插入操作的参数对象中就会包含自动生成的id值,例如,调用时的代码:

System.out.println("增加前:" + user);
Integer rows = userMapper.insert(user);
System.out.println("rows=" + rows);
System.out.println("增加后:" + user);

结果例如:

增加前:User [id=null, username=jsd1808, password=1234, age=31, phone=13900139008, email=jsd1808@tedu.cn]
rows=1
增加后:User [id=24, username=jsd1808, password=1234, age=31, phone=13900139008, email=jsd1808@tedu.cn]

可以记住:在配置时,需要配置4个属性。

2. 使用多个参数

功能设定:将t_user表中年龄大于xx的数据的密码修改为xx

分析所执行的SQL语句:

update t_user set password=? where age>?

抽象方法:

Integer updatePasswordByAge(
	Integer age, String password);

SQL映射:

<update id="updatePasswordByAge">
	update t_user 
	set 
		password=#{password} 
	where 
		age>#{age}
</update>

直接调用,会报告错误:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'password' not found. Available parameters are [arg1, arg0, param1, param2]

Caused by: org.apache.ibatis.binding.BindingException: Parameter 'password' not found. Available parameters are [arg1, arg0, param1, param2]

因为.java文件在编译后变成.class文件就会丢失参数名称,在最终执行的.class文件中,根本就不存在名为passwordage的参数,所以,程序会报错,可行的解决方法是把多个参数封装到一个Map中,这样的话,此前使用的方法名称例如agepassword就会变成Mapkey,是一些字符串,并不会因为编译而丢失,最终运行就不会有问题,当然,每次运行时都需要自行将参数封装为Map,操作麻烦,还存在key拼写错误的风险,所以,MyBatis提供了@Param注解以解决这个问题:

解决多个参数的问题:可以将多个参数封装为1个Map,或封装为1个自定义的数据类型,但是,更推荐使用@Param注解。

3. 关于resultMap

使用resultMap可以解决名称不匹配的问题!例如在数据表中存在is_delete字段,则实体类存在isDelete属性,则数据表中使用的名称与实体类中的属性名不一致!

前置操作:

alter table t_user add column is_delete int default 0;

update t_user set is_delete=1 where id in (11,13,15);

然后,在User类中添加private Integer isDelete;及SET/GET方法,重新生成toString()

名称不匹配的问题可以通过自定义别名来解决,例如:

<select id="findAll"
	resultType="cn.tedu.mybatis.entity.User">
	select 
		id, username, password,
		age, phone, email, 
		is_delete AS isDelete
	from t_user
</select>

也就是:MyBatis的要求是“查询结果中的列名与返回值类型的属性名必须一致”,通过自定义别名就可以满足这个要求,并不需要通过<resultMap>来实现!

通常,需要自定义<resultMap>时,主要用于解决多表数据关联查询的问题。

例如:

create table t_department (
	id int auto_increment,
	name varchar(20) not null,
	primary key(id)
);

insert into t_department (name) values ('UI'), ('RD'), ('TEST');

alter table t_user add column department int;

通常实体类都是与数据表一一对应的,符合设计规范,但不适用于多表的关联查询,例如当需要“查询某个部门信息的同时需要获取该部门的所有员工的信息”,则没有任何数据类型可以匹配这样的信息,为了解决这样的问题,通常会定义VO类,即Value Object类,这种类型是专用于解决实体类不满足使用需求而存在的,类的设计结构与实体类非常相似,但是,属性的设计是完全根据使用需求来决定的,例如:

public class DepartmentVO {
	private Integer depId;
	private String depName;
	private List<User> users;
	// SET/GET方法,toString(),序列化接口
}

普通的查询无法得到以上结果,查询语句可能是:

select
	t_user.id, username, password, age, phone, email, is_delete,
	t_department.id AS dep_id, name
from 
	t_user, t_department
where
	t_user.department=t_department.id
	and t_department.id=?

以上查询易于理解,通俗易懂,但是,不推荐使用,更推荐使用JOIN系列的查询语法:

SELECT
	t_user.id, username, password, age, phone, email, is_delete,

	t_department.id AS dep_id, name
FROM
	t_user
INNER JOIN
	t_department
ON
	t_user.department=t_department.id
WHERE
	t_department.id=?

这样的查询结果可能有好几行,

需要有效的封装到1个对象中,就必须依靠<resultMap>来设计封装的规则:

<!-- id:节点的唯一标识 -->
<!-- type:数据类型 -->
<resultMap id="DepartmentVoMap" 
	type="cn.tedu.mybatis.vo.DepartmentVO">
	<!-- id节点:主键的配置 -->
	<!-- column:查询结果的列名 -->
	<!-- property:数据类型的属性名 -->
	<id column="dep_id" property="depId"/>
	<!-- 其它结果的配置 -->
	<result column="name" property="depName"/>
	<!-- 配置List集合 -->
	<!-- ofType:集合中放的哪种类型的数据 -->
	<collection property="users"
		ofType="cn.tedu.mybatis.entity.User">
		<!-- 自此开始,property表示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"/>
		<result column="is_delete" property="isDelete"/>
	</collection>
</resultMap>

最后,应用时,与普通的数据操作相同,先添加接口与抽象方法:

public interface DepartmentMapper {

	DepartmentVO findById(Integer id);

}

然后配置映射:

<select id="findById"
	resultMap="DepartmentVoMap">
	SELECT
		t_department.id AS dep_id, name,
		
		t_user.id, username, password, 
		age, phone, email, 
		is_delete
	FROM
		t_user
	INNER JOIN
		t_department
	ON
		t_user.department=t_department.id
	WHERE
		t_department.id=#{id}
</select>

最终,执行查询获取的结果例如:

DepartmentVO [
	depId=2, 
	depName=RD, 
	users=[
		User [id=13, username=spring, password=1234, age=23, phone=13800138003, email=spring@tedu.cn, isDelete=1, department=null], 
		User [id=14, username=mybatis, password=12cxv34, age=24, phone=13800138004, email=mybatis@tedu.cn, isDelete=0, department=null], 
		User [id=17, username=jdbc, password=88888888, age=27, phone=13800138007, email=jdbc@tedu.cn, isDelete=1, department=null], 
		User [id=21, username=mapper, password=88888888, age=31, phone=13900139001, email=mapper@tedu.cn, isDelete=1, department=null], 
		User [id=23, username=namespace, password=88888888, age=31, phone=13900139002, email=namespace@tedu.cn, isDelete=1, department=null]
	]
]

如果提示错误TooManyResultsException,则错误多半在于查询结果的列名与<resultMap>中普通的<id/><result/>节点的column的配置有误!也有可能存在例如2列的名称都是id,却有多条数据的id值不同的问题!

练习:存在学生表和班级表,学生表t_student中包括id, name, age, class_id,班级表t_class中包括id, name,最终,查询时,查某班级数据时将显示该班级所有学生的信息。涉及的类为Student、Clazz。

练习步骤1:创建2张数据表,插入一定量的数据,创建对应的实体类,创建班级的VO类:

create table t_class (
	id int auto_increment,
	name varchar(20),
	primary key (id)
);

insert into t_class (name) values ('JSD1806'),('JSD1807'),('JSD1808');

create table t_student (
	id int auto_increment,
	name varchar(20),
	age int,
	class_id int,
	primary key (id)
);

insert into t_student (name,age,class_id) values ('Mike', 20, 1), ('Tom', 21, 2), ('Terry', 21, 3), ('Jerry', 22, 2), ('Lucy', 22, 1), ('Kitty', 22, 2), ('Lily', 21, 3), ('Lilei', 20, 3), ('HanMM', 23, 3), ('XiaoMing', 21, 2);

练习步骤2:创建cn.tedu.mybatis.mapper.ClazzMapper接口,复制得到src\main\resources\ClazzMapper.xml映射文件,这2个文件都是空文件即可。

练习步骤3:设计SQL语句:

SELECT 
	t_class.id AS cls_id, 
	t_class.name AS cls_name,

	t_student.id AS stu_id, 
	t_student.name AS stu_name,
	age, class_id
FROM
	t_class
INNER JOIN
	t_student
ON
	t_class.id=t_student.class_id
WHERE
	t_class.id=?;

练习步骤4:抽象方法

ClazzVO findById(Integer id);

练习步骤5:配置<select>

<select id="findById"
	resultMap="ClazzVOMapper">
	SELECT 
		t_class.id AS cls_id, 
		t_class.name AS cls_name,

		t_student.id AS stu_id, 
		t_student.name AS stu_name,
		age, class_id
	FROM
		t_class
	INNER JOIN
		t_student
	ON
		t_class.id=t_student.class_id
	WHERE
		t_class.id=#{id};
</select>

练习步骤6:配置<resultMap>

<resultMap id="ClazzVOMapper"
	type="cn.tedu.mybatis.vo.ClazzVO">
	<id column="cls_id" property="clsId"/>
	<result column="cls_name" property="clsName"/>
	<collection property="student"
		ofType="cn.tedu.mybatis.entity.Student">
		<id column="stu_id" property="id"/>
		<result column="stu_name" property="name"/>
		<result column="age" property="age"/>
		<result column="class_id" property="classId"/>
	</collection>
</resultMap>

1. MyBatis中的占位符

在MyBatis中,常见的占位符格式是#{参数},其中,也可能是参数对象中的属性,如果参数是Map类型,还可以是Map中的key。

使用#{}的占位符可用于替换,例如:

select * from t_user where username=?

即可替换以上语句中的问号(?),在实际运行时,MyBatis会将以上SQL语句进行预编译,并后续使用#{}替换问号(?)。

假设获取用户列表时,排序规则不确定,可能使用的抽象方法是:

List<User> findAllOrderedList(String orderBy);

配置的映射可能是:

<select id="xx" resultType="xx">
	select * from t_user
	order by #{orderBy}
</select>

调用时:

mapper.findAllOrderedList("id asc");
mapper.findAllOrderedList("id desc");

以上代码的执行效果是失败的!需要将#{}修改为${},且在抽象方法中,这样的参数必须添加@Param注解,即:

List<User> findAllOrderedList(
	@Param("orderBy") String orderBy);

<select id="xx" resultType="xx">
	select * from t_user
	order by ${orderBy}
</select>

然后,在调用时,就可以根据参数的不同,实现不同的排序效果!

使用${}格式的占位符并不具备预编译的效果!它是直接拼接形成的SQL语句,例如:"select * from t_user order by" + orderBy,如果一定使用${}格式的占位符来表示某个值,还需要考虑单引号类似的问题,例如:select * from t_user where username='${username}',由于只是拼接,所以,还存在SQL注入风险!

小结

使用#{}是预编译的(没有SQL注入风险,无需关注数据类型),使用${}不是预编译的;

使用#{}只能替换某个值,使用${}可以替换SQL语句中的任何部分;

关于SQL注入,不需要太过于紧张,预编译可以从根源上杜绝,或者,在执行SQL指令之前,判断参数中是否包含单引号也可以杜绝!

通过使用${},可以使得SQL更加灵活,更加通用!但是,却不推荐太过于通用的SQL!因为,即使查询条件可以自由更改,但是,不同的查询条件对应不同的需求,所需的字段列表很有可能是不一样的,查询时,获取不必要的字段,就会造成不必要的资源浪费,例如,显示列表时,可能需要用户名、密码、年龄、手机、邮箱,但是,登录的查询就只需要用户名、密码即可,年龄、手机、邮箱这几项数据在登录时是不需要的,如果也查询出来,就是浪费资源!如果变量太多,又会导致不可控因素太多,容易出错!

2. 动态SQL

在MyBatis的映射文件中,配置SQL语句时,可以添加例如<if>此类的标签,实现SQL语句的动态变化,即:参数不同,最终执行的SQL语句可能是不同的!

在使用动态SQL时,最常用的就是<if><foreach>这两种,<if>是用于判断的,例如:

select 
	* 
from 
	t_user
	
<if test="where != null">
where 
	${where}
</if>
	
<if test="orderBy != null">
order by 
	${orderBy}
</if>

关于<foreach>,主要用于循环处理SQL语句中的某个部分,例如:批量删除某些数据!它的SQL语句可能是:

delete from t_user where id in (?,?,?)

其中,in关键字右侧的括号中的内容是不确定的,应该是由用户操作时决定的!则需要<foreach>动态的生成这个部分!

针对这个问题,设计的抽象方法可能是:

Integer deleteByIds(Integer[] ids);

配置的映射为:

<delete id="deleteByIds">
	delete from 
		t_user
	where
		id in (
		<foreach collection="array"
			item="id" separator=",">
			#{id}
		</foreach>
		)
</delete>

以上配置的<foreach>中,collection表示被遍历的集合对象,当抽象方法只有1个参数时,取值为listarray,取决于集合对象的数据类型,当抽象方法 有多个参数时,使用@Param注解中的名称,item表示遍历过程中的变量名,separator表示分隔符。

以上配置还可以调整为:

id in
<foreach collection="array"
	item="id" separator=","
	open="(" close=")">
	#{id}
</foreach>

即:open表示由<foreach>处理的SQL语句的起始部分的字符串,而close表示结束部分的字符串。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值