# 1. MyBatis

# 1.1 作用

MyBatis是持久层框架,它是支持JDBC的!简化了持久层开发!

使用MyBatis时,只需要通过接口指定数据操作的抽象方法,然后配置与之关联的SQL语句,即可完成!

持久化存储:在程序运行过程中,数据都是在内存(RAM,即内存条)中的,内存中的数据不是永久存储的,例如程序可以对这些数据进行销毁,或者由于断电也会导致内存中所有数据丢失!而把数据存储到硬盘中的某个文件中,会使得这些数据永久的存储下来,常见做法是存储到数据库中,当然,也可以使用其他技术把数据存储到文本文件,XML文件等其它文件中!

# 1.2. 基本使用

# 1.2.1. 创建项目

使用此前相同的创建流程即可!注意:请检查有没有多余的配置,如果有,请删除,例如在Spring.xml是否有拦截器的配置!

此次使用MyBatis框架,所以,需要添加新的依赖:

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis</artifactId>
	<version>3.4.6</version>
</dependency>

如果下载的依赖jar包是损坏的,应该先关闭Eclipse,然后删除对应的jar包文件,再次启动Eclipse,对项目点击右键,选择Maven>Update Project,并且在弹出的对话框中勾选Force Update...选项即可。

MyBatis是一个独立的框架,即只添加该依赖就可以实现持久层编程,但是,开发过程相对比较繁琐,而实际应用中,通常会与Spring,SpringMVC一起使用,整合使用时,可以简化大量的配置,使得开发更加简便!整合时,还需要添加相关依赖:

<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-spring</artifactId>
	<version>1.2.3</version>
</dependency>

整合的SSM框架是基于JDBC的,所以,还需要添加Spring-jdbc的依赖:

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>3.2.8.RELEASE</version>
</dependency>

添加以上依赖时,直接将此前的spring-webmvc的依赖代码复制一份,将spring-webmvc改成spring-jdbc即可!凡是Spring官方(group ID是org.springframework)推出的以spring-作为前缀的依赖,必须使用相同的版本,否则,可能存在不兼容的风险!

在实践过程中,肯定得先建立与数据库的连接,然后再继续编程,所以,还应该添加数据源管理的依赖,即数据库连接池的依赖:

<dependency>
	<groupId>commons-dbcp</groupId>
	<artifactId>commons-dbcp</artifactId>
	<version>1.4</version>
</dependency>

由于本次将使用MySQL数据库,所以,还需要该数据库的连接驱动的依赖:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.8</version>
</dependency>

# 1.2.2. 创建数据库与数据表

创建数据库tedu_mybatis

cerate database tedu_mybatis;

创建数据表t_user:

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(30),
	primary key(id)
)default charset=utf8;

# 1.2.3. 配置数据源

使用db.properties文件配置与数据库连接相关的信息:

url=jdbc:mysql://localhost:3306/tedu_mybatis?useUnicode=true&characterEncoding=utf8
driver=com.mysql.jdbc.Driver
username=root
password=chengyi123
initialSize=2
maxActive=10

以上配置需要被应用到项目中,在Spring的配置文件中通过<util:properties/>可以读取该文件:

最后,需要把这些配置应用到数据源(数据库连接池)中,当前项目使用的是Apache的commons-dbcp,则对应的数据源是BasicDataSource类:

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

<!-- 配置数据源BasicDataSource-->
<bean id="dataSource" 
	class="org.apache.commons.dbcp.BasicDataSource">
	<property name="url" 
		value="#{dbconfig.url}"/>
	<property name="driverClassName" 
		value="#{dbconfig.driver}"/>
	<property name="username" 
		value="#{dbconfig.username}"/>
	<property name="password" 
		value="#{dbconfig.password}"/>
	<property name="initialSize" 
		value="#{dbconfig.initialSize}"/>
	<property name="maxActive" 
		value="#{dbconfig.maxActive}"/>
</bean>

完成以上配置后,可以测试到目前为止的配置是否正确,做法就是:获取BasicDataSource的对象,调用它的getConnection()方法,尝试在Java程序中获取与数据库的连接,如果能够正常连接,则配置无误,如配置有误,将无法获取连接!

@Test
public void getConnection() throws SQLException {
	AbstractApplicationContext ac = 
			new ClassPathXmlApplicationContext("spring.xml");
	BasicDataSource bds = ac.getBean("dataSource",BasicDataSource.class);
	Connection conn = bds.getConnection();
	System.out.println(conn);
}

# 1.2.4. 通过MyBatis插入数据

MyBatis的编码模式是:

  1. 创建接口,并声明数据访问的抽象方法;
  2. 配置与抽象方法对应的XML映射。

首先,创建cn.tedu.mybatis.entity.User实体类,并添加与t_user数据表匹配的属性。

通常每张数据表都有一个与之匹配的实体类!

创建cn.tedu.mybatis.mapper.UserMapper接口,并在接口中声明抽象方法:

Integer insert(User user);

在MyBatis中,执行insert/update/delete操作时,均返回受影响的行数,所以,设计抽象方法时,如果对应的是这几种操作,返回值均设计为Integer类型。

通常,一个完整的项目中会存在许多MyBatis的映射文件,为了便于管理,会在src/main/resources下创建一个名为mappers的文件夹,然后,下载共享的SomeMapper.zip ,将解压得到的XML文件复制到mappers文件夹中。

其实,在mappers下的映射文件的名称并不重要!但是,为了便于管理,通常会使用与接口对应的名称,所以,将SomeMapper.xml重命名为UserMapper.xml

所有映射文件中,根节点都是</mapper>节点,且该节点必须配置名为namespace的属性,属性值是对应的Java接口,例如:

<mapper 
	namespace="cn.tedu.mybatis.mapper.UserMapper">
</mapper>

经过以上配置,指定了XML映射文件与接口文件的对应关系。

然后,在该文件内部,使用各级子节点配置与抽象方法的对应关系,子节点名称的选取,取决于要执行的操作的类型,例如要执行的数据操作是insert类型,则使用<insert>节点,这些节点都必须指定id属性,属性值是与之对应的抽象方法的方法名:

<!-- id:抽象方法的名称 -->
<insert id="insert" >
</insert>

<insert>节点中,添加paramterType属性,用于指定参数的类型,即抽象方法中的参数类型:

<!-- parameterType:抽象方法中的参数的类型 -->
<insert id="insert" 
parameterType="cn.tedu.mybatis.entity.User">
insert into t_user() values()
</insert>

然后,在节点内部,编写需要执行的SQL语句:

<!-- id:抽象方法的名称 -->
<!-- parameterType:抽象方法中的参数的类型 -->
<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>

执行SQL语句时的参数值均使用#{}类似的语法,其中的名称是User类中的属性名,参数名应该与数据库表中的t_user的字段对应。

# 1.2.5. 最后的配置

首先,需要配置SqlSessionFactoryBean,通过它指定数据源与XML映射的文件位置:

<!-- SqlSessionFactoryBean -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
	<!-- 指定数据源,值为以上配置的数据源 -->
	<property name="dataSource" ref="dataSource"/>
	<!-- 指定XML映射文件的位置 -->
	<property name="mapperLocations" value="classpath:mapppers/*.xml"/>
</bean>

以上配置中,XML映射文件的位置使用了mappers/*.xml,即:在mappers文件夹下的所有XML文件都应该是MyBatis的映射文件,所以,后续使用时,不可以在这个文件夹中存放其他XML文件。

然后,还需要配置MapperScannerConfigurer,用于指定接口文件在哪里。

<!-- MapperScannerConfigurer -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
	<!-- 指定接口文件的位置 -->
	<property name="basePackage" value="cn.tedu.mybatis.mapper"/>
</bean>

至此配置完成

# 1.2.6. 执行单元测试

@Test
public void insert() {
	//加载Spring配置文件,获取Spring容器
	AbstractApplicationContext ac = 
			new ClassPathXmlApplicationContext("spring.xml");
	//从Spring容器中获取对象
	//bean-id与接口名一致,首字母为小写
	UserMapper userMapper = 
			ac.getBean("userMapper", UserMapper.class);
	//测试功能
	User user = new User();
	user.setUsername("chengyi");
	user.setPassword("123456");
	Integer rows = userMapper.insert(user);
	System.out.println("rows:"+rows);
	//释放资源
	ac.close();
}

# 1.2.7. 小结

关于配置 使用MyBatis添加了一些新的依赖,及一些配置,这些属于固定的一次性操作,在后续的项目中,每个项目只需要做1次即可。

关于这些配置,需要记住配置的作用,及可能需要修改的位置,至少包括:

  1. db.properties的文件名,因为<util:properties需要使用它;
  2. 在db.properties中配置的值,重点的是数据库名称,访问数据库的密码,在更换项目或更换计算机后都可能需要调整;
  3. 在配置SqlSessionFactoryBean时指定的映射文件的位置;
  4. 在配置MapperScannerConfigurer时指定的接口文件所在的位置。

关于开发

  1. 每张数据表,都应该有1个与之对应的实体类;(值对象)
  2. 每种数据(视为每个实体类)的处理都应该有对应的接口文件,例如项目中User实体类,则应该有UserMapper接口;
  3. 在接口中声明抽象方法时,如果最终执行insert/delete/update操作,返回值类型应该是Integer
  4. 每个持久层接口,都应该有对应的XML映射文件,例如有UserMapper.java接口,就应该有UserMapper.xml文件;
  5. 在配置XML文件内部,根据执行操作选择节点,如果执行的是insert操作,则通过<insert>节点进行配置;
  6. 在XML映射中,每个节点都必须配置id属性,取值是接口中抽象方法的名称,由于id具有唯一的特性,所以,在接口中声明抽象方法时,不要使用重载;
  7. 在配置SQL语句时,使用#{}表示预编译时的?对应的值;括号中的名称是参数名称,或参数对象中的属性名称。
  8. 使用${}表示的变量,是通过字符串的拼接形成的SQL语句,所以,如果使用它来表示字符串或其他例如时间等格式,可能存在两端的符号问题。
  9. 当执行delete/update时,配置的节点不需要指定parameterType;

# 1.2.7. 删除指定的数据

设置目标为根据id删除指定的数据

现在接口中声明抽象方法:

Integer deleteUserById(Integer id);

然后,在XML映射中配置以上方法对应的节点:

<delete id="deleteUserById">
	delete from t_user where id=#{id}
</delete>

# -----------------------------

  1. 什么是MyBatis MyBatis是持久层框架,它是支持JDBC的!简化了持久层开发!
  2. 通过MyBatis插入数据
    1. 创建接口,并声明数据访问的抽象方法;
    2. 配置与抽象方法对应的XML映射。

# 1. 单元测试

由于在单元测试中,每个测试方法都需要执行相同都前置代码和后置代码,则可以自定义2个方法,分别在这2个方法中执行前置代码和后置代码,为这2个方法添加@Before@After,然后,在每个测试方法中,就不必再编写这些代码,并且,在执行测试方法之前,会自动调用添加了@Before方法,在执行测试方法之后,会自动调用添加了@After方法:

private UserMapper userMapper;
private AbstractApplicationContext ac;

@Before
public void doBefore() {
	ac = new ClassPathXmlApplicationContext("spring.xml");
	//从Spring容器中获取对象
	//bean-id与接口名一致,首字母为小写
	userMapper = ac.getBean("userMapper", UserMapper.class);
}
@After
public void doAfter() {
	//释放资源
	ac.close();
}

# 2. 在MyBatis中查询数据

使用MyBatis执行查询操作时,抽象方法的设计中,返回值应该根据查询需求来决定,例如查询用户列表时,可以使用List<User>作为返回值类型,查询某个用户时,可以使用User作为返回值类型,查询的是计数等操作时,可以使用Integer作为返回值类型。

其实,无论执行什么样的查询,MyBatis的查询结果,都是List集合,只不过,如果抽象方法声明的不是集合,而是具体的某个类型,例如User时,MyBatis*会尝试从集合中去第1个元素作为返回值!

查询是可能失败的,即没有匹配的数据,在这种情况下,如果返回值是List集合,则返回的是空集合(集合对象是存在的,但是集合中没有任何元素),如果返回值是某个类型,则返回null。

在配置的XML映射中,每个查询对应的都是<select>节点,该节点必须配置<resultType><resultMap>属性,用于表示查询的结果的类型(即使返回值类型是Integer也必须配置)!

如果返回结果是User对象,则resultType的值是User类的全名,如果返回结果是List<User>,其resultType也是User类的全名,而不是List的全名!

关于resultMap后续再介绍。

目标:查询所有用户的数据

在接口中添加抽象方法:

List<User> findAll();

配置以上方法的映射:

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

目标:查询指定id的用户数据 在接口中添加抽象方法:

User findUserById(Integer id);

配置以上方法的映射:

<select id="findAll" resultType="cn.tedu.mybatis.entity.User">
	select 
		id,username,password,age,phone,email
	from 
		t_user 
	where
		id=#{id}
</select>

练习:根据用户名查询用户数据。

# 2. 设计多参数的数据访问操作

在许多操作中,也许1个参数并不能满足需求,可能需要2个或多个参数,例如修改某个用户的密码,设计的抽象方法是:

Integer updatePasswordById(
		@Param("id")Integer id,
		@Param("password")String password);

然后,配置的映射:

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

在MyBatis中,其实只有1个参数,并且,Java源文件编译为class字节码文件后,也会丢失参数名称,所以,需要为每一个参数添加@Param注解,则MyBatis会把这些参数全部封装在一个Map中,注解中的名称就是每个参数的key,后续,在配置映射时,在#{}中填入的也就是注解的名称,即各参数在Map中的Key。

在设计MyBatis中的抽象方法时,如果参数超过1个,则每个参数之前都必须添加@Param注解,在映射的SQL语句中,#{}中填入也是注解中使用的名称!

# 3. 插入数据时获取数据自增长的id

在配置映射时,为<insert>节点添加2个属性:useGeneratedKeys="true"表示需要获取自动生成的键,通常就是数据表中的主键字段,即id字段,然后配置keyProperty="id",表示获取到的键的值(即自增长的id值)将要封装到那个属性中(即实体类的属性名):

<insert id="insert" 
	parameterType="cn.tedu.mybatis.entity.User"
	useGeneratedKeys="true" 
	keyProperty="id">
	insert into t_user(
		username, password, age, phone, email
	) values(
		#{username},#{password},#{age},#{phone},#{email}
	)
</insert>

经过以上配置后,当成功插入数据时,就会获取到该数据的自增长的id值,并且,会将值封装到参数对象中,即:调用Integer insert(User user)方法时,假设使用user1作为调用时的参数,当方法执行结果后,参数user1中就已经包含了id值!

# 4. 关联表查询操作

问题描述

存在班级学生数据,要求显示某个班级信息时,同时显示出该班级的所有学生。

准备工作

创建班级(1)表:

create table t_class(
	id int auto_increment,
	name varchar(20),
	primary key(id)
) default charset=utf8;

创建学生(N)表:

create table t_student(
	id int auto_increment,
	name varchar(20),
	class_id int, 
	primary key(id)
)default charset=utf8;

当数据表之间存在1对多关系时,在“多”的表中需要存储“1”的表的唯一表识,即存储“1”的表的id

为了保证后续的数据查询,还应该添加一部分的测试数据。

insert t_class (name) values('jsd1807'),('jsd1808'),('jsd1809');

insert into t_student(name,class_id) values
("Jack",1),
("Rose",1),
("Lilei",2),
("HanMM",2),
("Lucy",1),
("LiLi",3),
("Liming",1),
("Bob",3),
("Kitty",1),
("Tom",2);

基于以上数据表,如果要获取班级信息的同时,还获取该班级的所有学生的列表,则执行的SQL查询应该是:

select 
	c.id as class_id,
	c.name as class_name,
	s.id as student_id,
	s.name as student_name
from t_class as c 
inner join t_student as s
 on c.id=s.class_id
 where c.name="jsd1807";

基于每张数据表都应该有1个与之对应的实体类的原则,则在项目中应该有:

public class Clazz {
	private Integer id;
	private String name;
}
public class Student{
	private Integer id;
	private String name;
	private Integer classId;
}

即使有了以上两个类,却都无法满足查询结果的需求,即:

??? getClassInfo(String className);

无法确定返回值类型!

针对这种情况,通常会在项目中创建VO类,即ValueObject类

public class ClazzVO{	
	private Integer classId;
	private String className;
	private List<Student> students;
}

通常,实体类与数据表是对应的,而VO类是与实际使用需求对应的!

当设计好了VO类,则查询的抽象方法是:

ClazzVO getClassInfo(String className);

可以发现,即使使用了VO类,查询的字段与ClazzVO类中的属性名称等都无法直接对应,则,在映射文件中,需要使用到<resultMap>!

配置映射文件:

<resultMap  id="classMap" type="cn.tedu.mybatis.vo.ClazzVO">
	<!-- id节点:专用于配置自增长字段的节点 -->
	<!-- column:查询结果的列名/字段名 -->
	<!-- property:返回结果类型中的属性名 -->
	<id column="class_id" property="classId"/>
	<!-- result节点:配置非主键的节点 -->
	<result column="class_name" property="className"/>
	<!-- collection节点:配置1对多关系的数据 -->
	<!-- ofType:集合中的数据类型 -->
	<collection property="students" 
	ofType="cn.tedu.mybatis.entity.Student">
		<id column="student_id" property="id"/>
		<result column="student_name" property="name"/>
	</collection>
</resultMap>

<select id="getClassInfo" resultMap="classMap">
	select 
		c.id as class_id,
		c.name as class_name,
		s.id as student_id,
		s.name as student_name
	from t_class as c 
	inner join t_student as s
 	on c.id=s.class_id
 	where c.name=#{className}
</select>

映射文件的变量关系图解: 在这里插入图片描述

执行结果为!

clazzVo:ClazzVO [
	classId=2, className=jsd1808, 
	students=[
		Studen [id=3, name=Lilei, classId=null], 
		Studen [id=4, name=HanMM, classId=null], 
		Studen [id=10, name=Tom, classId=null]
	]
]

小结 如果某个查询设计多张表,存在关联查询,通常是没有匹配的实体类可以直接使用的,在这种情况下,就需要自定义VO类。

VO类与实体类的代码表现基本相似,只是定位不同,实体类是与数据表对应的,而VO类是为了满足编码需求,更方便的获取查询结果而存在的!

VO类的属性的设计原则完全取决于所需要执行的查询的结果。

产生了关联后,可以直接用VO类作为resultType,但是,如果查询结果中存在数据之间的1对多等关系,则需要配置<resultMap>

# 5. 动态SQL

在MyBatis中配置映射时,允许使用例如<if>此类的标签,使得每次执行的SQL语句可以产生动态调整,则称之为动态SQL。

目标:实现根据id修改用户信息,可修改的字段有:密码、年龄、手机号码、电子邮件、如果执行的参数中,某项数据为null,则不修改原有值,例如修改时,参数中没有年龄值,则不修改原有的年龄,其他字段也是相同的处理方式。

首先,在接口中声明抽象方法:

Integer changeInfo(
		@Param("id")Integer id,
		@Param("password")String password,
		@Param("age")Integer age,
		@Param("phone")String phone,
		@Param("email")String email);

然后配置以上方法的映射:

<update id="changeInfo">
	update 
		t_user
	set 
		password = #{password},
		<if test="age != null">
			age = #{age},
		</if>
		<if test="phone != null">
			phone = #{phone},
		</if>
		email = #{email}
	where
		id = #{id};
</update>

以上配置的执行效果会是:如果没有提供某个值,将会把对应的字段的值设置为null,而并非不修改原有值。

此类问题可以通过动态SQL的<if>标签来解决:

执行以上代码时,如果提供了有效的age值(非null),则SQL语句为

update t_user set password=?,age=?,phone=?email=?where id=?

如果没有提供age值,则SQL语句为

update t_user set password=?,phone=?email=?where id=?

注意:在编写动态SQL时,参数直接写名字即可,例如test="age != null"中的age就是参数的名称,不需要使用#{}这类的语法!

目标:一次删除多条数据,这些数据的id是作为参数体现的,但是,是没有规律的。

在接口中声明抽象方法:

Integer deleteUserByIds(List<Integer> ids);

以上方法的设计,参数可以是List<Integer>,也可以是Integer[]

然后,配置映射:

<!-- collection: 使用哪个集合,取值使用list或array -->
<!-- item:每次遍历到的元素名称 -->
<!-- separator:IN内部的各个值之间使用的分隔符 -->
<delete id="deleteUserByIds">
	delete from 
	t_user
	where id in(
		<foreach collection="list" item="id" 
		separator=",">
			#{id}
		</foreach>
	)
</delete>

注意:在<foreach>中的collection属性,抽象方法只有1个参数时,取值为list和array(首字母不能大写),根据参数类型决定,当抽象方法的参数超过1个时,该属性的值为参数的名称(参数的注解中使用的名称)!

关于动态SQL,主要掌握<if><foreach>的使用!

# -------------------------------------

Last Updated: 10/10/2021, 12:03:13 PM