是一个基于Java的持久层框架,支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。
MyBatis可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJO映射成数据库中的记录。 每个MyBatis应用程序主要都是使用SqlSessionFactory实例的,一个SqlSessionFactory实例可以通过SqlSessionFactoryBuilder获得。SqlSessionFactoryBuilder可以从一个xml配置文件或者一个预定义的配置类的实例获得。
用xml文件构建SqlSessionFactory实例是非常简单的事情。推荐在这配置中使用类路径资源(classpath、resource),但你可以使用任何reader实例(就是说读取的流),包括用文件路径或file://开头的url创建的实例。MyBatis有一个实用类—Resources,它有很多方法,可以方面地从类路径及其它位置加载资源。
简单易学,没有任何第三方依赖。只需要两个jar跟映射的sql配置文件即可。
解除sql与程序代码的耦合,通过提供DAL层(数据访问层,主要用于数据库的访问),将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。
提供映射标签,支持对象与数据库orm字段关系映射(即数据库字段与JavaBean属性名不匹配的情况)
内部封装了JDBC,使开发者只需要关注sql语句本身,而无须花费精力去处理加载驱动、创建链接、创建statement等繁杂的过程。
通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中的sql的动态参数进行映射生成最终的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。
1、创建maven工程
2、打开pom.xml,导入相关的依赖jar包,我们导入三个,分别是mybatis的核心jar包、mysql数据库的包(用于连接数据库)、junit单元测试的包(用于方便单元测试)
3、编写User.java实体类,重写toString,并提供相应的get和set方法
package cn.levi.domain;
import java.io.Serializable;
import java.util.Date;
/**
* User实体类
*
* @author Levi
*
*/
public class User implements Serializable {
private Integer id; // 用户ID
private String username;// 用户名
private Date birthday;// 生日
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", birthday=" + birthday + "]";
}
}
4、创建IUserDao.java 用户持久层接口,并提供查询所有用户的抽象方法
package cn.levi.dao;
import java.util.List;
import cn.levi.domain.User;
/**
* 用户持久层接口
*
* @author Levi
*
*/
public interface IUserDao {
// 查询所有用户
List findAllUsers();
}
5、在配置文件目录创建SqlMapConfig.xml配置文件(名字可以改成其他的),并导入相关dtd约束
config的dtd约束
添加约束文件后的空白SqlMapConfig.xml
6、通过约束文件的提示,我们要对主配置文件相关项进行编写
<configuration>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC">transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="root" />
<property name="password" value="admin" />
dataSource>
environment>
environments>
configuration>
configuration:
就像是Mybatis的总管(根标签),Mybatis的所有配置信息都存放在这里
environments:
environments里可以配置多个environment,每个environment对应一个数据库环境
default属性表示默认的数据库环境,与某个environment的id相对应
environment:
environment通过id属性与其他数据库环境区别。
它有两个子节点:transactionManager(事务管理器)和dataSource(数据源)
transactionManager:
在MyBatis中有两种事务管理器类型
第一种:JDBC
这个配置直接简单使用了JDBC的提交和回滚设置。它依赖于从数据源得到的连接来管理事务范围。
第二种:MANAGED
这个配置几乎没做什么。它从来不提交或回滚一个连接。而它会让容器来管理事务的整个生命周期(比如Spring或JEE应用服务器的上下文)
dataSource:
在MyBatis中有三种数据源类型
第一种:UNPOOLED
这个数据源的实现是每次被请求时简单打开和关闭连接
第二种:POOLED
mybatis实现的简单的数据库连接池类型,它使得数据库连接可被复用,不必在每次请求时都去创建一个物理的连接
第三种:JNDI
通过jndi从tomcat之类的容器里获取数据源
需要配置的属性
driver:这是JDBC驱动的Java类的完全限定名
例如:com.mysql.jdbc.Driver
url:这是数据库的JDBC URL地址
例如:jdbc:mysql://localhost:3306/mybatis
username:登录数据库的用户名
例如:root
password:登录数据库的密码
例如:admin
defaultTransactionIsolationLevel:默认的连接事务隔离级别
7、在配置文件目录,创建与IUserDao相同目录结构的IUserDao.xml映射配置文件,并导入相关dtd约束
mapper的dtd约束
添加约束文件后的空白IUserDao.xml
8、通过约束文件的提示,在映射配置文件中编写用于执行IUserDao时所需要的sql语句
<mapper namespace="cn.levi.dao.IUserDao">
<select id="findAllUsers" resultType="cn.levi.domain.User">
select * from user;
select>
mapper>
什么是映射配置文件?
它是Java实体类与数据库对象之间的桥梁。
在实际的使用过程中,一般一个映射配置文件对应一个数据库操作Dao接口
mapper:
映射配置文件的根节点
在根节点中支持9个元素insert、update、delete、select、cache、cache-ref、resultMap、parameterMap、sql
namespace属性:
填写对应接口的全限定类名。因实际开发中,可能会有很多个mapper,同时很多个mapper中也会存在名字相同的方法,所以为了加以区分,这里必须填写接口的全限定类名
select:
配置查询使用的标签
id属性:(必须配置项)
命名空间中的唯一标识,mapper中的namespace指定到某个dao接口,此处的id用于指定到接口中的具体方法,故这里应与方法名一致
parameterType属性:(可选配置项,默认mybatis会自动选择处理)
将要传入语句的参数的完全限定类名或别名,如果不配置,mybatis会通过ParameterHandler根据参数类型默认选择合适的typeHandler进行处理 parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象)
resultType属性:(与resultMap二选一配置)
用以指定返回类型,指定的类型可以是基本类型,可以是java容器,也可以是javabean
resultMap属性:(与resultType二选一配置)
resultMap用于引用我们通过resultMap标签定义的映射类型,这也是mybatis组件高级复杂映射的关键(多用于数据库名称与实体类属性名不一致时),后面遇到的地方我们在进行详解
9、现在主配置文件与映射配置文件都编写好了,但是两者并没有任何关系,下面我们要在主配置文件中配置映射文件的位置
在根标签下添加下面标签
<mappers>
<mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>
配置后的主配置文件
<configuration>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC">transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="root" />
<property name="password" value="admin" />
dataSource>
environment>
environments>
<mappers>
<mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>
configuration>
mappers:(必须配置项)
用于指定在mybatis初始化时,需要引入哪些mapper映射配置文件。有两种配置方法,分别是配置package元素和配置mapper元素
package:
只需要配置一个name属性用于设置包名,将会自动扫描包下所有的映射配置文件
<mappers>
<package name="cn.levi.dao"/>
mappers>
mapper:
通过resource指定具体的配置文件位置,上文我们使用的就是这种方式
<mappers>
<mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>
通过class指定接口,然后将接口与对应的xml文件形成映射关系,需要注意的是,这种方式必须保证xml与接口在相同包结构下,且名字必须完全相同。即接口如果为IUserDao.java,那么xml配置文件则必须叫IUserDao.xml
<mappers>
<mapper class="cn.levi.dao.IUserDao" />
mappers>
通过url指定配置文件的位置(url地址获取方式:将计算机中的文件拖入浏览器,复制地址栏的地址即可)
<mappers>
<mapper url="file:///E:/Learn/Java/Workspace/MyBatis01/src/main/resources/cn/levi/dao/IUserDao.xml" />
mappers>
10、因为MyBatis通过动态代理的方式,对接口的方法进行增强,实现对应的功能,所以我们不需要编写接口的实现类,直接编写测试类MyBatisTest.java进行测试即可
package cn.levi.test;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import cn.levi.dao.IUserDao;
import cn.levi.domain.User;
/**
* 测试MyBatis入门案例
*
* @author Levi
*
*/
public class MyBatisTest {
// 测试查找所有用户方法
@Test
public void findAllUsers() throws Exception {
// 1、获取主配置文件的流对象
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2、使用构建者模式,创建SqlSession工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 3、使用工厂生产一个SqlSession对象
SqlSession session = factory.openSession();
// 4、获取持久层接口的代理对象
IUserDao mapper = session.getMapper(IUserDao.class);
// 5、执行持久层接口中的查询所有用户的方法
List users = mapper.findAllUsers();
// 6、遍历集合,打印查询到的对象
for (User user : users) {
System.out.println(user);
}
// 7 、释放资源
session.close();
is.close();
}
}
1、在持久层接口中添加CRUD相关的方法
package cn.levi.dao;
import java.util.List;
import cn.levi.domain.User;
/**
* 用户持久层接口
*
* @author Levi
*
*/
public interface IUserDao {
// 查询所有用户
List findAllUsers();
// 根据名称模糊查询用户(不存在sql注入问题)
List findUserByName1(String username);
// 根据名称模糊查询用户(存在sql注入问题)
List findUserByName2(String username);
// 根据ID查询用户
User findById(Integer id);
// 获取总记录条数
int findUserCount();
// 保存用户
void saveUser(User user);
// 修改用户
void editUser(User user);
// 删除用户
void delUser(Integer id);
}
2、在映射文件中填写持久层接口中的方法要执行的sql语句
<mapper namespace="cn.levi.dao.IUserDao">
<select id="findAllUsers" resultType="cn.levi.domain.User">
select * from user;
select>
<select id="findUserByName1" resultType="cn.levi.domain.User" parameterType="String">
select * from user where username like #{username};
select>
<select id="findUserByName2" resultType="cn.levi.domain.User" parameterType="String">
select * from user where username like '%${value}%';
select>
<select id="findById" resultType="cn.levi.domain.User" parameterType="Integer">
select * from user where id=#{id};
select>
<select id="findUserCount" resultType="int">
select count(*) from user;
select>
<insert id="saveUser" parameterType="cn.levi.domain.User">
insert into user (username,birthday)values(#{username},#{birthday});
insert>
<update id="editUser" parameterType="cn.levi.domain.User">
update user set username=#{username},birthday=#{birthday} where id=#{id};
update>
<delete id="delUser" parameterType="Integer">
delete from user where id=#{id};
delete>
mapper>
在sql语句中,我们通过#{}代表占位符,如果只有一个参数,#{参数名随意};如果为多个或返回实体类中的多个属性,需要跟属性名保持一致#{实体类属性名一致}
{}与${}的区别?
{}表示一个占位符号
#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换
#{}可以有效防止sql注入
#{}可以接收简单类型值或POJO属性值
如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称
${}表示拼接sql串
${}可以将parameterType传入的内容拼接在sql中且不进行jdbc类型转换
${}可以接收简单类型值或POJO属性值
如果parameterType传输单个简单类型值,${}括号中只能是value
3、编写测试类中对应的测试方法进行测试,因为方法比较多,所以我们把公共部分抽取出来,我们分成四个模块
第一部分:初始化模块(@Before:Junit中的初始化方法)
// 初始化方法
@Before
public void init() throws Exception {
// 1、获取主配置文件的流对象
is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2、使用构建者模式,创建SqlSession工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 3、使用工厂生产一个SqlSession对象
session = factory.openSession();
// 4、获取持久层接口的代理对象
mapper = session.getMapper(IUserDao.class);
}
第二部分:释放资源模块(@After:Junit中的释放资源方法)
// 释放资源方法
@After
public void destory() throws Exception {
session.close();
is.close();
}
第三部分:成员变量抽取(因为要在多个方法中使用同一个对象)
private InputStream is;
private SqlSession session;
private IUserDao mapper;
第四部分:单元测试模块(@Test:Junit中的测试方法)
// 测试查找所有用户方法
@Test
public void findAllUsers() throws Exception {
List users = mapper.findAllUsers();
for (User user : users) {
System.out.println(user);
}
}
// 测试根据名称模糊查询用户方法(不存在sql注入问题)
@Test
public void findUserByName1() {
List users = mapper.findUserByName1("%王%");
for (User user : users) {
System.out.println(user);
}
}
// 测试根据名称模糊查询用户方法(存在sql注入问题)
@Test
public void findUserByName2() {
List users = mapper.findUserByName2("王");
for (User user : users) {
System.out.println(user);
}
}
// 测试根据ID查询用户方法
@Test
public void findById() {
User user = mapper.findById(41);
System.out.println(user);
}
// 测试获取总记录条数方法
@Test
public void findUserCount() {
int rows = mapper.findUserCount();
System.out.println(rows);
}
上面四个查询方法中,我们不需要进行提交,但是进行增删改操作后,我们需要手动进行提交才可以,使用session.commit();
// 测试保存用户方法
@Test
public void saveUser() {
User user = new User();
user.setUsername("利威尔");
user.setBirthday(new Date());
mapper.saveUser(user);
session.commit();
}
// 测试修改用户方法
@Test
public void editUser() {
User user = mapper.findById(50);
user.setUsername("阿克曼");
user.setBirthday(new Date());
mapper.editUser(user);
session.commit();
}
// 测试删除用户方法
@Test
public void delUser() {
mapper.delUser(50);
session.commit();
}
完整的测试类代码MyBatisTest.java
package cn.levi.test;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import cn.levi.dao.IUserDao;
import cn.levi.domain.User;
/**
* 测试MyBatis入门案例
*
* @author Levi
*
*/
public class MyBatisTest {
private InputStream is;
private SqlSession session;
private IUserDao mapper;
// 初始化方法
@Before
public void init() throws Exception {
// 1、获取主配置文件的流对象
is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2、使用构建者模式,创建SqlSession工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 3、使用工厂生产一个SqlSession对象
session = factory.openSession();
// 4、获取持久层接口的代理对象
mapper = session.getMapper(IUserDao.class);
}
// 释放资源方法
@After
public void destory() throws Exception {
session.close();
is.close();
}
// 测试查找所有用户方法
@Test
public void findAllUsers() throws Exception {
List users = mapper.findAllUsers();
for (User user : users) {
System.out.println(user);
}
}
// 测试根据名称模糊查询用户方法(不存在sql注入问题)
@Test
public void findUserByName1() {
List users = mapper.findUserByName1("%王%");
for (User user : users) {
System.out.println(user);
}
}
// 测试根据名称模糊查询用户方法(存在sql注入问题)
@Test
public void findUserByName2() {
List users = mapper.findUserByName2("王");
for (User user : users) {
System.out.println(user);
}
}
// 测试根据ID查询用户方法
@Test
public void findById() {
User user = mapper.findById(41);
System.out.println(user);
}
// 测试获取总记录条数方法
@Test
public void findUserCount() {
int rows = mapper.findUserCount();
System.out.println(rows);
}
// 测试保存用户方法
@Test
public void saveUser() {
User user = new User();
user.setUsername("利威尔");
user.setBirthday(new Date());
mapper.saveUser(user);
session.commit();
}
// 测试修改用户方法
@Test
public void editUser() {
User user = mapper.findById(50);
user.setUsername("阿克曼");
user.setBirthday(new Date());
mapper.editUser(user);
session.commit();
}
// 测试删除用户方法
@Test
public void delUser() {
mapper.delUser(50);
session.commit();
}
}
4、如何获取自增长主键的值?
上文中我们并没有设置id,但是却可以看到有相关的字段值,因为我们在数据库中对id进行了自增长的设置。
我们先在测试方法中获取自增长主键ID试试看
// 测试获取自增长主键ID
@Test
public void testId() {
User user = new User();
user.setUsername("利威尔");
user.setBirthday(new Date());
mapper.saveUser(user);
System.out.println(user.getId());
session.commit();
}
从图中我们不难发现,新增的id=52,但是在代码中却打印出null
那么问题又来了,我们如何才能在代码中获取到自增长主键的值呢?
SelectKey在Mybatis中是为了解决Insert数据时不支持主键自动生成的问题,他可以很随意的设置生成主键的方式。
keyColumn:表示查询语句返回结果的列名,简单说就是sql语句中对应的列名
keyProperty:表示将属性设置到某个列中,简单说就是实体类中对应的属性名
resultType:表示返回值的类型
我们在保存用户的方法添加代码如下
<insert id="saveUser" parameterType="cn.levi.domain.User">
<selectKey keyColumn="id" keyProperty="id" resultType="Integer">
select last_insert_id();
selectKey>
insert into user (username,birthday)values(#{username},#{birthday});
insert>
1、编写UserDaoImpl.java并且继承IUserDao接口,实现其中的抽象方法
package cn.levi.dao.impl;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import cn.levi.dao.IUserDao;
import cn.levi.domain.User;
/**
* 用户持久层接口实现类
*
* @author Levi
*
*/
public class UserDaoImpl implements IUserDao {
// 定义factory不是session 是为了防止单例
private SqlSessionFactory factory;
// 只提供有参构造 目的是为了让使用者一定要传个工厂才能创建实现类对象
public UserDaoImpl(SqlSessionFactory factory) {
this.factory = factory;
}
// 查询所有用户
@Override
public List findAllUsers() {
return factory.openSession().selectList("cn.levi.dao.IUserDao.findAllUsers");
}
// 根据名称模糊查询用户(不存在sql注入问题)
@Override
public List findUserByName1(String username) {
return factory.openSession().selectList("cn.levi.dao.IUserDao.findUserByName1", username);
}
// 根据名称模糊查询用户(存在sql注入问题)
@Override
public List findUserByName2(String username) {
return factory.openSession().selectList("cn.levi.dao.IUserDao.findUserByName1", username);
}
// 根据ID查询用户
@Override
public User findById(Integer id) {
return factory.openSession().selectOne("cn.levi.dao.IUserDao.findById", id);
}
// 获取总记录条数
@Override
public int findUserCount() {
return factory.openSession().selectOne("cn.levi.dao.IUserDao.findUserCount");
}
// 保存用户
@Override
public void saveUser(User user) {
SqlSession session = factory.openSession();
session.insert("cn.levi.dao.IUserDao.saveUser", user);
session.commit();
}
// 修改用户
@Override
public void editUser(User user) {
SqlSession session = factory.openSession();
session.update("cn.levi.dao.IUserDao.editUser", user);
session.commit();
}
// 删除用户
@Override
public void delUser(Integer id) {
SqlSession session = factory.openSession();
session.delete("n.levi.dao.IUserDao.delUser", id);
session.commit();
}
}
成员变量为什么定义的是factory而不是session呢?
从代码中我们可以看出,每个方法都会调用factory.opensession()来获取一个session对象,所以很容易就会想到为什么不把这一部分一起抽取出来,而要选择定义factory呢。原因很简单,如果我们的实现类是单例的,那么它只会执行一次初始化操作,那么就只会存在一个session对象,也就是说session也变成单例的了。当我们操作某一个方法调用完session后进行session.close()操作,这个session就会被销毁。其他方法在想调用session,就会报错了。所以我们通过工厂来解决这个问题,即便工厂是单例的,每次调用方法也都会生成一个新的session对象。
2、编写完接口的实现类,接下来我们就可以编写新的测试类了,对通过接口实现类的方式进行测试
package cn.levi.test;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import cn.levi.dao.IUserDao;
import cn.levi.dao.impl.UserDaoImpl;
import cn.levi.domain.User;
/**
* 测试通过Dao接口实现类实现的方式
*
* @author Levi
*
*/
public class MyBatisTest2 {
private InputStream is;
private IUserDao dao;
// 初始化方法
@Before
public void init() throws Exception {
// 1、获取主配置文件的流对象
is = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2、使用构建者模式,创建SqlSession工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 3、使用Dao接口的实现类 传入一个工厂进行初始化
dao = new UserDaoImpl(factory);
}
// 释放资源方法
@After
public void destory() throws Exception {
is.close();
}
// 测试查找所有用户方法
@Test
public void findAllUsers() throws Exception {
List users = dao.findAllUsers();
for (User user : users) {
System.out.println(user);
}
}
// 测试根据名称模糊查询用户方法(不存在sql注入问题)
@Test
public void findUserByName1() {
List users = dao.findUserByName1("%王%");
for (User user : users) {
System.out.println(user);
}
}
// 测试根据名称模糊查询用户方法(存在sql注入问题)
@Test
public void findUserByName2() {
List users = dao.findUserByName2("王");
for (User user : users) {
System.out.println(user);
}
}
// 测试根据ID查询用户方法
@Test
public void findById() {
User user = dao.findById(41);
System.out.println(user);
}
// 测试获取总记录条数方法
@Test
public void findUserCount() {
int rows = dao.findUserCount();
System.out.println(rows);
}
// 测试保存用户方法
@Test
public void saveUser() {
User user = new User();
user.setUsername("利威尔");
user.setBirthday(new Date());
dao.saveUser(user);
}
// 测试修改用户方法
@Test
public void editUser() {
User user = dao.findById(50);
user.setUsername("阿克曼");
user.setBirthday(new Date());
dao.editUser(user);
}
// 测试删除用户方法
@Test
public void delUser() {
dao.delUser(50);
}
// 测试获取自增长主键ID
@Test
public void testId() {
User user = new User();
user.setUsername("利威尔");
user.setBirthday(new Date());
dao.saveUser(user);
System.out.println(user.getId());
}
}
通过测试,我们也实现了单表CRUD的全部操作。但是这种方式相比之前通过代理Dao实现的方式存在不足。
我们将增删改的提交的代码(session.commit())写到了UserDaoImpl中,但实际开发中,事务的提交是业务层的事,不应该放到持久层中,所以还是建议通过代理Dao的方式而不是Dao实现类的方式
1、先来看下之前我们在主配置文件中对数据源的配置部分
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="username" value="root" />
<property name="password" value="admin" />
dataSource>
从上面我们可以看出一个问题,我们把数据库的配置文件写死到了xml配置文件中,这样做的弊端是,每次我们要修改数据库信息,都要去主配置文件中找到对应数据源配置的部分,进行修改,这显然不是我们想要的,此时我们可以通过properties标签进行适当的优化。
<properties>
<property name="jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="jdbc.username" value="root" />
<property name="jdbc.password" value="admin" />
properties>
数据源部分我们通过EL表达式进行调整
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
dataSource>
这时我们已经把数据从原来的数据源标签移到了properties标签中了,但是与配置文件还是绑定在了一起
有没有办法能把这部分数据库连接信息移到properties的配置文件中去呢?
答案是肯定的
在properties标签中,为我们提供了两种读取配置文件的方式的属性
resource属性:
用于指定外部的资源文件(properties文件),读取的位置是类路径下,写法是包名/包名/文件名
<properties resource="jdbcConfig.properties">properties>
jdbcConfig.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=admin
url属性:
用于指定外部资源文件,他可以读取任意能访问的路径下的文件,但写法必须是URL的方式(拖拽到浏览器即可)
<properties
url="file:///E:/Learn/Java/Workspace/MyBatis01/src/main/resources/jdbcConfig.properties">
properties>
如果同时在多处进行配置,读取的优先级是怎样的呢?
1、在properties内部自定义的属性值第一个被读取
<properties>
<property name="jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/mybatis" />
<property name="jdbc.username" value="root" />
<property name="jdbc.password" value="admin" />
properties>
2、然后读取resource路径表示文件中的属性,如果有它会覆盖已经读取的属性;如果resource路径不存在,那么读取url表示路径文件中的属性,如果有它会覆盖第一步读取的属性值
<properties resource="jdbcConfig.properties">properties>
3、最后读取parameterType传递的属性值,它会覆盖已读取的同名的属性
<select id="findUserByName" resultType="cn.levi.domain.User" parameterType="String">
select * from user where username like '%${jdbc.username}%';
select>
我们在编写映射配置文件的时候,总是要写cn.levi.domain.User
当配置文件很多的时候,是比较耗费时间与精力的,当需要修改的时候,也要将所有的位置统一进行修改,严重影响了开发效率与出错的几率
这时候就需要我们使用别名的方式来进行优化,在SqlMapConfig.xml中,我们添加下面的标签
<typeAliases>
<typeAlias type="cn.levi.domain.User" alias="user"/>
typeAliases>
如果有很多实体类的时候,我们就要使用另一种配置方式了
<typeAliases>
<package name="cn.levi.domain"/>
typeAliases>
这种方式默认将包中的类名作为别名使用,以上两种方式的别名都不区分大小写。
可能有的同学会有疑问,为什么有些类似int类型的也可以呢?
这是因为myBatis已经预先注册好了常用的一些类型的别名
那么我们如何查看都有哪些呢?
打开eclipse,Ctrl + Shift + T 输入TypeAliasRegistry
点进TypeAliasRegistry,在源码中我们就可以看到MyBatis中定义的别名了
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);
registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);
registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);
registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);
registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);
registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);
registerAlias("ResultSet", ResultSet.class);
按住Ctrl点击registerAlias,选择Open Implementation,查看这个方法的实现
public void registerAlias(String alias, Class> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
TYPE_ALIASES.put(key, value);
}
在这里能看到,MyBatis将传入的字符串进行了转换,无论传入的是大写还是小写,都会默认转成小写字母,这就是我们使用别名时不区分大小写的原因。
相似的还有一个地方可以进行调整
比如之前我们指定映射配置文件的时候,使用的标签如下
<mappers>
<mapper resource="cn/levi/dao/IUserDao.xml" />
mappers>
如果有很多个接口的话,那么就需要配置很多个mapper标签,现在我们可以同样使用包的方式进行优化
<mappers>
<package name="cn.levi.dao"/>
mappers>
我们可能还会有疑问,mapper到底是什么东西呢?
目前来看,我们在mapper中写的都是关于dao接口的东西,其实很多人喜欢将dao写成mapper,即把我们写的IUserDao叫成IUserMapper,其实说的都是同一个意思,代表的都是持久层的接口。以后在开发中如果遇到别人写的xxxMapper文件时,你就要明白,其实他写的是个持久层接口就可以了。
什么是连接池:
用于存放连接的容器,它里面全都是Connection对象。
容器一般指的就是一个集合(连接池的集合是一个List)
连接池的作用和优势
作用:解决在项目使用过程中获取连接消耗时间的问题
优势:由于在项目一加载时,已经创建好了连接,所以在项目运行过程中,就不会再消耗创建连接的时间了。
连接池实现可能遇到的问题以及解决
close方法不能真正的关闭连接(否则用一个关一个,连接池就没有连接可用了)
解决:
对原有的close方法进行升级,把它增强,让他在执行时不是真正的关闭,而是加回到池中。
可以采用:动态代理技术或者装饰者模式来实现
常用的连接池技术
C3P0、DBCP、基于tomcat服务器(不只是tomcat)的JNDI
JNDI:Java Named and Directory Interface(java命名和目录服务接口)
mybatis中连接池的实现机制
它用了两个集合来维护:
idleConnections:空间的连接池
activeConnections:活动的连接池
mybatis框架:
会先去空闲池中看是否有空闲的连接
有:
直接拿着用
没有:
再看活动池中是否到了最大活动数:
没到:
创建一个新的连接,并加入到活动池中
到了:
取出活动池中最老的一个连接进行重置
mabatis中的连接池配置
在主配置文件的dataSource标签中,有一个type属性,用于指定是否使用连接池
取值:
POOLED:使用连接池
UNPOOLED:不使用连接池
JNDI:使用web服务器中提供命名目录接口来获取数据源
在openSession中,我们能看到好多重载的方法
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
我们选择了比较熟悉的四个进行说明
第一个空参的,是我们最常用的方式。
第二个需要传入是否自动提交,当为true时,我们无需进行手动提交;false时与第一种一样
第三个需要传入一个连接,它可以实现多条语句共用一个Connection,在转账案例中,我们就需要使用同一个连接
第四个可以设置事务的隔离级别
Java程序控制事务的本质
设置connection对象的提交为手动
setAutoCommit(false);
事务控制的要求
事务的控制位置:应该在业务层。
要求业务层方法全部走完再决定事务是否提交/回顾
开发中通过pojo传递查询条件,查询条件是综合的查询条件,不仅包括用户查询条件还包括其他的查询条件,这时我们可以使用包装对象传递输入参数,即POJO类中包含POJO
需求:根据用户名查询用户信息,查询条件放到QueryVo的user属性中
第一步:新建一个QueryVo的实体类,里面提供一个User对象属性
package cn.levi.domain;
import java.io.Serializable;
/**
* 查询的条件对象
*
* @author Levi
*
*/
public class QueryVo implements Serializable {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
第二步:在Dao接口中添加通过对应的抽象方法
// 根据查询条件中的对象 查询用户列表
List findByQueryVo(QueryVo vo);
第三步:在映射配置文件中配置对应的方法
<select id="findByQueryVo" resultType="user parameterType="queryvo">
select * from user where username like #{user.username};
select>
这里我们不能直接使用username like #{username}
因为我们的返回值类型是queryvo,并没有username这个属性值,所以我们要用user.username
这种写法有个专业的叫法是OGNL表达式,它不是MyBatis独有的,是apache的
我们这里使用的仅仅是其中的一种格式
详情可见:https://commons.apache.org/proper/commons-ognl/language-guide.html
第四步:添加测试方法
// 测试查询用户列表方法(QueryVo)
@Test
public void findByQueryVo() {
QueryVo vo = new QueryVo();
User user = new User();
user.setUsername("%王%");
vo.setUser(user);
List users = mapper.findByQueryVo(vo);
for(User u:users){
System.out.println(u);
}
}
在上面的案例中,我们留有一个地方没有讲到。之前的代码中,我们的POJO的属性名称与sql查询的列名是一致的,所以可以直接将指定的查询结果映射为POJO。
但是很多情况下,我们不可能把所有的POJO属性名都与数据库中对应的列名保持一致,那么该怎么办呢?
这时候我们就可以使用resultMap将字段名和属性名做一个对应关系,resultMap实质上还需要将查询结果映射到POJO对象中。
resultMap可以实现将查询结果映射为复杂类型的POJO,比如在查询结果映射对象中包括POJO和list实现一对一查询和一对多查询。
下面我们通过代码来体会resultMap的作用
测试思路分析:
由于原有代码中很多地方都用到了User实体类,改动之后其他的代码也会受到影响。我们这里只是简单的说明下resultMap的作用,所以不准备修改原有的User实体类属性名,但是还要保证与数据库中列的字段名称不一致的需求,这时候我们想到了可以使用别名来达到我们想要的效果
1、我们打开映射配置文件,对刚刚编写的QueryVo的查询方法sql语句进行修改
<select id="findByQueryVo" resultType="user" parameterType="queryvo">
select id newId,username newUsername,birthday newBirthday from user where username =#{user.username};
select>
3、接下来我们使用resultMap标签将字段进行对应
<resultMap type="user" id="userMap">
<id column="newId" property="id" />
<result column="newUsername" property="username" />
<result column="newBirthday" property="birthday" />
resultMap>
resultMap中的type属性:
封装的类型即对应的JavaBean,因为在之前的代码中我们已经通过包的方式配置了别名,所以我们可以直接写user
resultMap中的id属性:
定义唯一的标识,其他标签也是通过该id值对resultMap进行引用
resultMap的子节点id:
定义查询结果集的唯一标识,通常只有一个,代表主键id。需要注意的是,多数情况下只需要一个主键id就可以进行唯一的约束,但如果需要用到多个字段为复合唯一约束条件,则这里就要定义多个了
resultMap的子节点result:
定义除主键id外的其它需要对应的字段
column属性:
sql查询出来后对应的字段名
property属性:
表示JavaBean中对应属性的名称
4、定义完成resultMap后,仍然不能实现我们想要的结果,因为我们使用的仍旧是resultType属性值,并没有与resultMap产生关联,这时候我们需要使用到另外一个属性resultMap,属性值是要使用的resultMap的id
<select id="findByQueryVo" resultMap="userMap" parameterType="queryvo">
select id newId,username newUsername,birthday newBirthday from user
where username =#{user.username};
select>
5、到这里我们就完成了resultMap的对应关系,运行测试方法结果如下
测试方法
// 测试查询用户列表方法(QueryVo)
@Test
public void findByQueryVo() {
QueryVo vo = new QueryVo();
User user = new User();
user.setUsername("老王");
vo.setUser(user);
List users = mapper.findByQueryVo(vo);
for (User u : users) {
System.out.println(u);
}
}
当比较简单的sql语句已经无法满足较为复杂的业务逻辑时,我们就需要sql语句也是可以动态变化的。根据实体类的取值不同,使用不同的sql语句进行查询。比如当id不为空时可以根据id查询,当username不为空时,可以在原有基础上在加入根据username进行查询。
下面我们还是在findByQueryVo的基础上添加if标签进行修改
需求:输入名字时,根据名字查询;不输入名字,查询所有用户
映射配置文件
<select id="findByQueryVo" resultType="user" parameterType="queryvo">
select * from user where 1=1
<if test="user.username!=null and user.username!=''">
and username =#{user.username}
if>
select>
测试方法
// 测试查询用户列表方法(QueryVo)
@Test
public void findByQueryVo() {
QueryVo vo = new QueryVo();
User user = new User();
user.setUsername("老王");
vo.setUser(user);
List users = mapper.findByQueryVo(vo);
for (User u : users) {
System.out.println(u);
}
}
第一次运行测试方法 包含有user.setUsername(“老王”);
第二次运行测试方法 注释掉user.setUsername(“老王”);
注意:
上面我们通过添加了一个where1=1来实现拼接操作,但是这样显得很不专业,我们可以使用where标签来解决这个问题,where标签可以自动处理第一个and
<select id="findByQueryVo" resultType="user" parameterType="queryvo">
select * from user
<where>
<if test="user.username!=null and user.username!=''">
and username =#{user.username}
if>
where>
select>
上面我们只是将一个值动态添加到sql语句中作为查询条件,当我们需要将一个集合中的值,作为参数动态添加进来的时候,就需要使用到foreach标签了
我们先在QueryVo中新增一个成员变量,用于存储所有的id值
package cn.levi.domain;
import java.io.Serializable;
import java.util.List;
/**
* 查询的条件对象
*
* @author Levi
*
*/
public class QueryVo implements Serializable {
private User user;
private List ids;// 所有id的集合
public List getIds() {
return ids;
}
public void setIds(List ids) {
this.ids = ids;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
然后修改映射配置文件
<select id="findByQueryVo" resultType="user" parameterType="queryvo">
select * from user
<where>
<foreach collection="ids" open="id in(" item="id" close=")" separator=",">
#{id}
foreach>
where>
select>
需求:查询id为41、42、43的用户列表
完整sql语句:select * from user where id in(41,42,43);
foreach标签:用户遍历集合
修改测试方法
@Test
public void findByQueryVo() {
QueryVo vo = new QueryVo();
List ids = new ArrayList();
ids.add(41);
ids.add(42);
ids.add(43);
vo.setIds(ids);
List users = mapper.findByQueryVo(vo);
for (User u : users) {
System.out.println(u);
}
}
到这里,看起来已经没什么问题了,但是确实是存在问题的
如果我们把测试方法中setIds方法注释掉,即集合为空时,再次运行查看结果
这时候测试报错了,因为我们的ids现在是null
那么该如何解决呢?
很简单,只需要利用刚刚学过的if标签进行判断即可
<select id="findByQueryVo" resultType="user" parameterType="queryvo">
select * from user
<where>
<if test="ids!=null and ids.size>0">
<foreach collection="ids" open="id in(" item="id" close=")" separator=",">
#{id}
foreach>
if>
where>
select>
当多条sql语句中包含重复的部分时(例如查询所有select * from user),这个时候我们可以将重复的这部分sql语句抽取出来,定义到sql标签中。
<sql id="defaultAll">
select * from user
sql>
然后我们修改原来的查询所有用户的方法为
<select id="findAllUsers" resultType="user">
<include refid="defaultAll">include>
select>
其中include标签的refid属性值,就是sql标签的id属性值
如果需要引用的是其他mapper.xml映射文件中的sql,则要加上namespace
例如:refid=”cn.levi.dao.IAccountDao.defaultAll”
最近精力有限 先写到这
以后抽时间慢慢补
未完待续——