解决持久层数据处理的问题
传统的jdbc:
//1.声明
String sql = "";
Connection conn = null;
PreparedStatement pstmt = null;
ResultResult rs = null;
//2.获取连接
try{
conn = dataSource.getConnection();
//3.预编译
pstmt = conn.xxx(sql);
pstmt.set???(1,xx);
//4.执行
pstmt.execute???();
//5.处理结果
...
}catch (SqlException e){
...
}finally{
//6.释放资源
rs.close();
pstmt.close();
conn.close();
}
主要是基于JDBC技术的原生代码比较繁琐,没有经过任何优化,开发甚至执行效率低下!
使用MyBatis框架时,不必关心JDBC技术如何实现,只需要编写需要执行的操作的抽象方法,例如User findById(Integer id)
,然后,为这个方法映射所需执行的SQL语句即可。
添加依赖:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>4.3.9.RELEASEversion>
dependency>
在resources根目录下添加spring-mvc.xml配置文件,同时添加相关的通用配置
<context:component-scan
base-package="cn.tedu.spring" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/" />
<property name="suffix" value=".jsp" />
bean>
<mvc:annotation-driven />
在web.xml中添加相关配置:
<servlet>
<servlet-name>SpringMVCservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:spring-mvc.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>SpringMVCservlet-name>
<url-pattern>*.dourl-pattern>
servlet-mapping>
<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>utf-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
此时一个spring的web项目建好了
在pom.xml中添加mybatis相关依赖
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.4.6version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>4.3.9.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.13version>
dependency>
<dependency>
<groupId>commons-dbcpgroupId>
<artifactId>commons-dbcpartifactId>
<version>1.4version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.9version>
dependency>
# 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
在resources根目录下添加spring-dao.xml配置文件,同时添加相关的通用配置
<util:properties id="dbConfig"
location="classpath:db.properties" />
<bean id="dataSource"
class="org.apache.commons.dbcp.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="maxActive"
value="#{dbConfig.maxActive}" />
bean>
测试:
@Test
public void t1() throws SQLException {
AbstractApplicationContext ac = new ClassPathXmlApplicationContext("spring-dao.xml");
DataSource dataSource = ac.getBean("dataSource", DataSource.class);
System.out.println(dataSource.getConnection());
ac.close();
}
使用MyBatis时,无需自行编写JDBC相关代码,只需要创建Java接口文件,并将需要执行的数据操作的抽象方法添加在接口中即可!
通常,建议按照“增 > 查 > 删 > 改”的顺序开发相关功能。
目标:向数据表中插入新的用户数据。
则创建cn.tedu.mybatis.mapper.UserMapper
接口,然后,添加“插入新的用户数据”的抽象方法:
Integer insert(User user);
使用MyBatis时,执行的增、删、改操作均返回
Integer
,表示受影响的行数。
使用MyBatis时,还需要与接口的抽象方法对应的SQL语句,该SQL语句是在XML文件中配置的!(UserMapper.xml
)
注:通常,接口文件的数量与XML映射文件的数量是相同的,是一一对应的!
映射的XML文件应该存放到src\main\resources
下,但是,项目中可能存在多个映射文件,为了便于管理,会在resources
下创建mappers
文件夹,然后把映射的XML文件放在这个文件夹中。
然后,配置该XML映射文件:
INSERT INTO t_user (
username, password,
age, phone, email
) VALUES (
#{username}, #{password},
#{age}, #{phone}, #{email}
)
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("[email protected]");
Integer rows = userMapper.insert(user);
System.out.println("rows=" + rows);
ac.close();
}
}
MyBatis使用简单,可以简化开发,开发者不必关注数据库编程的细节;
使用MyBatis编程主要做好:(1)设计SQL语句; (2)设计抽象方法; (3)配置映射;
所有的xx.properties
和spring-dao.xml
中的配置,需要理解,需要掌握修改配置值,不需要记住;
关于抽象方法:如果是增删改操作,返回值固定设计为Integer
,如果是查询操作,根据查询结果来决定,例如可能是List
、User
、Integer
……;方法的名称应该尽量对应所执行的数据操作,而不应该是某个业务,例如插入数据的方法名可以是insert
,或者addnew
,但是不应该使用reg
;目前,只允许使用最多1个参数,如果一定要使用多个,请封装为1个参数;
关于映射配置:根据所执行的操作选择
、
、
、节点,无论是哪个节点,必须配置
id
,取值为对应的抽象方法的名称;在SQL语句中的参数使用#{}
框住即可;
关于执行结果:如果是增删改操作,只能获得受影响的行数,可以根据该结果判断操作成功与否;如果是执行查询操作,当结果声明为List
时,无论是否查询到数据,返回有效的List
集合对象,如果没有匹配的数据,则List
的长度为0;当结果声明为某个对象,例如User
时,如果没有匹配的数据,则返回null
。
useGeneratedKeys="true"
keyProperty="id"
以上配置中,useGeneratedKeys
表示获取自增长的键(自增长的字段的值),keyProperty
表示键的属性名,即对应的类中的属性名(即该id
是User
类中的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 protected]]
rows=1
增加后:User [id=24, username=jsd1808, password=1234, age=31, phone=13900139008, [email protected]]
注:通俗的理解,在配置insert是,需要配置4个属性
功能设定:将t_user
表中年龄大于xx的数据的密码修改为xx
分析所执行的SQL语句:
update t_user set password=? where age>?
抽象方法:
Integer updatePasswordByAge(
Integer age, String password);
SQL映射:
update t_user
set
password=#{password}
where
age>#{age}
直接调用,会报告错误:
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
文件中,根本就不存在名为password
或age
的参数,所以,程序会报错,可行的解决方法是把多个参数封装到一个Map
中,这样的话,此前使用的方法名称例如age
或password
就会变成Map
的key
,是一些字符串,并不会因为编译而丢失,最终运行就不会有问题,当然,每次运行时都需要自行将参数封装为Map
,操作麻烦,还存在key
拼写错误的风险,所以,MyBatis提供了@Param
注解以解决这个问题:
解决多个参数的问题:可以将多个参数封装为1个Map,或封装为1个自定义的数据类型,但是,更推荐使用@Param注解。
使用resultMap
可以解决名称不匹配的问题!例如在数据表中存在is_delete
字段,则实体类存在isDelete
属性,则数据表中使用的名称与实体类中的属性名不一致!
<resultMap id="" type="">
<result column="" property=""/>
resultMap>
但是很麻烦,mybatis可以使用别名的方式
<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的要求是“查询结果中的列名与返回值类型的属性名必须一致”,通过自定义别名就可以满足这个要求,并不需要通过
来实现!
例如:
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 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个对象中,就必须依靠
来设计封装的规则:
配置方式可参考:
最后,应用时,与普通的数据操作相同,先添加接口与抽象方法:
public interface DepartmentMapper {
DepartmentVO findById(Integer id);
}
然后配置映射:
最终,执行查询获取的结果例如:
DepartmentVO [
depId=2,
depName=RD,
users=[
User [id=13, username=spring, password=1234, age=23, phone=13800138003, [email protected], isDelete=1, department=null],
User [id=14, username=mybatis, password=12cxv34, age=24, phone=13800138004, [email protected], isDelete=0, department=null],
User [id=17, username=jdbc, password=88888888, age=27, phone=13800138007, [email protected], isDelete=1, department=null],
User [id=21, username=mapper, password=88888888, age=31, phone=13900139001, [email protected], isDelete=1, department=null],
User [id=23, username=namespace, password=88888888, age=31, phone=13900139002, [email protected], isDelete=1, department=null]
]
]
如果提示错误TooManyResultsException
,则错误多半在于查询结果的列名与
中普通的
或
节点的column
的配置有误!也有可能存在例如2列的名称都是id
,却有多条数据的id
值不同的问题!
Mybatis中,常见的占位符格式: #{参数}
,其中,也可能是参数对象中的属性,如果参数是Map
类型,还可以是Map
中的key。
使用#{}
的占位符可用于替换值,例如:
select * from t_user where username=?
即可替换以上语句中的问号(?
),在实际运行时,MyBatis会将以上SQL语句进行预编译,并后续使用#{}
替换问号(?
)。
假设获取用户列表时,排序规则不确定,可能使用的抽象方法是:
List findAllOrderedList(String orderBy);
配置的映射可能是:
调用时:
mapper.findAllOrderedList("id asc");
mapper.findAllOrderedList("id desc");
以上代码的执行效果是失败的!需要将#{}
修改为${}
,且在抽象方法中,这样的参数必须添加@Param
注解,即:
List findAllOrderedList(
@Param("orderBy") String orderBy);
然后,在调用时,就可以根据参数的不同,实现不同的排序效果!
使用${}
格式的占位符并不具备预编译的效果!它是直接拼接形成的SQL语句,例如:"select * from t_user order by" + orderBy
,如果一定使用${}
格式的占位符来表示某个值,还需要考虑单引号类似的问题,例如:select * from t_user where username='${username}'
,由于只是拼接,所以,还存在SQL注入风险!
小结
使用#{}
是预编译的(没有SQL注入风险,无需关注数据类型),使用${}
不是预编译的;
使用#{}
只能替换某个值,使用${}
可以替换SQL语句中的任何部分;
关于SQL注入,不需要太过于紧张,预编译可以从根源上杜绝,或者,在执行SQL指令之前,判断参数中是否包含单引号也可以杜绝!
通过使用${}
,可以使得SQL更加灵活,更加通用!但是,却不推荐太过于通用的SQL!因为,即使查询条件可以自由更改,但是,不同的查询条件对应不同的需求,所需的字段列表很有可能是不一样的,查询时,获取不必要的字段,就会造成不必要的资源浪费,例如,显示列表时,可能需要用户名、密码、年龄、手机、邮箱,但是,登录的查询就只需要用户名、密码即可,年龄、手机、邮箱这几项数据在登录时是不需要的,如果也查询出来,就是浪费资源!如果变量太多,又会导致不可控因素太多,容易出错!
在MyBatis的映射文件中,配置SQL语句时,可以添加例如
此类的标签,实现SQL语句的动态变化,即:参数不同,最终执行的SQL语句可能是不同的!
在使用动态SQL时,最常用的就是
和
这两种,
是用于判断的,例如:
select
*
from
t_user
where
${where}
order by
${orderBy}
关于
,主要用于循环处理SQL语句中的某个部分,例如:批量删除某些数据!它的SQL语句可能是:
delete from t_user where id in (?,?,?)
其中,in
关键字右侧的括号中的内容是不确定的,应该是由用户操作时决定的!则需要
动态的生成这个部分!
针对这个问题,设计的抽象方法可能是:
Integer deleteByIds(Integer[] ids);
配置的映射为:
delete from
t_user
where
id in (
#{id}
)
以上配置的
中,collection
表示被遍历的集合对象,当抽象方法只有1个参数时,取值为list
或array
,取决于集合对象的数据类型,当抽象方法 有多个参数时,使用@Param
注解中的名称,item
表示遍历过程中的变量名,separator
表示分隔符。
以上配置还可以调整为:
id in
#{id}
即:open
表示由
处理的SQL语句的起始部分的字符串,而close
表示结束部分的字符串。