目录
一、方式1:使用代理DAO
------>项目源码
本文接着Mybatis快速入门的案例,进行其他CRUD操作,使用的是Mapper接口代理DAO的开发方式。
重点其实就是映射文件的配置,在写SQL 语句传参时,使用标签的 parameterType 属性来设定参数类型。
该属性的取值可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。
案例的目标表:
实体类:
目录结构:
插入操作
首先在dao的接口中添加:
/**
* 保存用户
*
* @param user
*/
void saveUser(User user);
在映射配置中:
<!-- 保存用户 -->
<insert id="saveUser" parameterType="com.zhu.damain.User">
<!-- 配置插入操作后,获取插入数据的id -->
insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
</insert>
测试类:
/**
*
* 测试mybatis的crud操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;
@Before
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象!!!
userDao = sqlSession.getMapper(IUserDao.class);
}
@After
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}
/**
* 测试查询所有
*/
@Test
public void testFindAll(){
//5.执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
}
/**
* 测试保存操作
*/
@Test
public void testSave(){
User user = new User();
user.setUserName("modify User property");
user.setUserAddress("北京市顺义区");
user.setUserSex("男");
user.setUserBirthday(new Date());
System.out.println("保存操作之前:"+user);
//5.执行保存方法
userDao.saveUser(user);
System.out.println("保存操作之后:"+user);
}
}
注意: 需要在执行SQL后,通过sqlSession.commit()手动提交事务。
特别的,factory.openSession(true)可以将事务类型改为自动提交,这样就不需要手动提交了。
获取插入数据的id
这个功能可以通过这条sql语句实现: select last_insert_id();
如果通过Mybatis实现呢?
可以在映射配置XML中,在原来的基础上添加:
<!-- 保存用户 -->
<insert id="saveUser" parameterType="com.zhu.domain.User">
<!-- 配置插入操作后,获取插入数据的id -->
<selectKey keyProperty="userId" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
</insert>
keyProperty为实体类的属性名, keyColumn为表的列名,order为语句执行的时机(在insert之前或之后)。
那么测试类中测试一下:
/**
* 测试保存操作
*/
@Test
public void testSave(){
User user = new User();
user.setUserName("modify User property");
user.setUserAddress("北京市顺义区");
user.setUserSex("男");
user.setUserBirthday(new Date());
System.out.println("保存操作之前:"+user);
//5.执行保存方法
userDao.saveUser(user);
System.out.println("保存操作之后:"+user);
}
可以看到,实体类的id属性在保存后被赋值了。
更新操作
在dao的接口中添加:
/**
* 更新用户
*
* @param user
*/
void updateUser(User user);
在映射配置中添加:
<!-- 更新用户 -->
<update id="updateUser" parameterType="com.zhu.damain.User">
update user set username=#{userName},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id}
</update>
在测试类中添加:
/**
* 测试更新操作
*/
@Test
public void testUpdate(){
User user = new User();
user.setUserId(50);
user.setUserName("mybastis update user");
user.setUserAddress("北京市顺义区");
user.setUserSex("女");
user.setUserBirthday(new Date());
//5.执行更新方法
userDao.updateUser(user);
}
删除操作
在dao的接口中添加:
/**
* 根据Id删除用户
*
* @param userId
*/
void deleteUser(Integer userId);
在映射配置中添加:
<!-- 删除用户-->
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id = #{uid}
</delete>
这里需要注意的是由于SQL语句中只需要一个参数,并且是基本类型或其包装类,所以#{ }中的内容可以任意写。
测试类:
/**
* 测试删除操作
*/
@Test
public void testDelete(){
//5.执行删除方法
userDao.deleteUser(48);
}
查询单个
dao接口添加:
/**
* 根据id查询用户信息
*
* @param userId
* @return
*/
User findById(Integer userId);
映射配置:
<!-- 根据id查询用户 -->
<select id="findById" parameterType="INT" resultType="com.zhu.domain.User">
select * from user where id = #{uid}
</select>
测试类:
/**
* 测试查询操作
*/
@Test
public void testFindOne(){
//5.执行查询一个方法
User user = userDao.findById(50);
System.out.println(user);
}
模糊查询
dao接口添加:
/**
* 根据名称模糊查询用户信息
*
* @param username
* @return
*/
List<User> findByName(String username);
映射配置:
<!-- 根据名称模糊查询 -->
<select id="findByName" parameterType="string" resultMap="com.zhu.domain.User">
select * from user where username like #{name}
</select>
测试类添加:
/**
* 测试模糊查询操作
*/
@Test
public void testFindByName(){
//5.执行查询一个方法
List<User> users = userDao.findByName("%王%");
for(User user : users){
System.out.println(user);
}
}
我们在配置文件中没有加入%来作为模糊查询的条件,所以在传入字符串实参时,就需要给定模糊查询的标识%。
聚合查询
dao接口添加:
/**
* 查询总用户数
*
* @return
*/
int findTotal();
映射配置:
<!-- 获取用户的总记录条数 -->
<select id="findTotal" resultType="int">
select count(id) from user;
</select>
测试类:
/**
* 测试查询总记录条数
*/
@Test
public void testFindTotal(){
//5.执行查询一个方法
int count = userDao.findTotal();
System.out.println(count);
}
二、查询条件封装
在上面的CRUD中,可以明白parameterType属性的取值可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。不仅如此,该取值还可以是实体类的封装类。这是通过OGNL(Object-Graph Navigation Language,对象-图导航语言)表达式来实现的。它通过对象的getter方法来获取数据,在写法上把get省略了。
例如我们要获取用户名称:
类中的写法:user.gerUsername();
OGNL表达式的写法:user.username
若查询条件是综合的查询条件,不仅包括用户查询条件还包括其它的查询条件(比如将用户购买商品信息也作为查询条件),这时可以使用封装对象传递输入参数。
例如,现在需要根据用户名查询用户信息,查询条件放到 QueryVo 的 user 属性中。QueryVo 就是一个封装对象。
/**
* 封装对象
*/
public class QueryVo {
private User user;
private Product product;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Product setProduct() {
this.product = product;
}
public void getProduct() {
return product;
}
}
dao接口定义查询方法:
/**
* 根据queryVo中的条件查询用户
*
* @param vo
* @return
*/
List<User> findUserByVo(QueryVo vo);
映射配置:
语句中使用了OGNL表达式——
<!-- 根据queryVo的条件查询用户 -->
<select id="findUserByVo" parameterType="com.zhu.domain.QueryVo" resultType="com.zhu.domain.User">
select * from user where username like #{user.username}
</select>
测试类:
/**
* 测试使用QueryVo作为查询条件
*/
@Test
public void testFindByVo(){
QueryVo vo = new QueryVo();
User user = new User();
user.setUserName("%王%");
vo.setUser(user);
//5.执行查询一个方法
List<User> users = userDao.findUserByVo(vo);
for(User u : users){
System.out.println(u);
}
}
三、查询结果封装
之前案例中都强调实体类的属性要与表的列名保持一致,万一由于某种原因改动了实体类属性(id -> userId),造成了不一致的情况,那么查询到的数据就无法封装进实体对象了,那么应该如何解决呢?
- 别名:
<!-- 查询所有 -->
<select id="findAll" parameterType="com.zhu.damain.User">
select id as userId,username as userName,address as userAddress,sex as userSex,birthday as userBirthday from user;
</select>
- 在映射配置中使用resultMap标签:
<!-- 配置 查询结果的列名和实体类的属性名的对应关系 -->
<resultMap id="userMap" type="com.zhu.domain.User">
<!-- 主键字段的对应 -->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userAddress" column="address"></result>
<result property="userSex" column="sex"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>
id 属性:给定一个唯一标识,提供给 select 标签引用用的
type 属性:指定实体类的全限定类名
接着在select标签中引入:
<!-- 查询所有 -->
<select id="findAll" resultMap="userMap">
select * from user;
</select>
虽然比别名的方式多配了一段xml,但是好处是一次配置就能多处使用。
四、主配置文件中的两个技巧
propertie标签的使用及细节
之前,在主配置文件中,我们是这么写的:
下面介绍propertie标签的另一种用法。
1.先在 classpath 下定义 db.properties 文件:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root
- 在主配置xml中配置properties标签:
<properties resource="jdbcConfig.properties">
</properties>
resource 属性:用于指定 properties 配置文件的位置,要求配置文件必须在类路径下
或
<properties url="file:///E:/mybatis_learning/mybatis_4_CRUD/src/main/resources/jdbcConfig.properties">
</properties>
- 修改dataSource标签:
<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
显而易见,这样的方式可以更灵活的进行配置。
typeAliases(类型别名)与 package
- Mybatis 支持默认别名,我们还可以使用typeAliases标签自定义别名。
<!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
<typeAliases>
<!--typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就不再区分大小写 -->
<typeAlias type="com.zhu.domain.User" alias="user"></typeAlias>-->
</typeAliases>
定义别名后,在映射配置中就可以这么简写了,而不再需要写全限定类名:
- 批量自定义别名:package标签
<typeAliases>
<!-- 用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写-->
<package name="com.zhu.domain"></package>
</typeAliases>
package标签也能用于主配置的标签中:
<!-- 配置映射文件的位置 -->
<mappers>
<!--<mapper resource="com/zhu/dao/IUserDao.xml"></mapper>-->
<!-- package标签是用于指定dao接口所在的包,当指定了之后就不需要在写mapper以及resource或者class了 -->
<package name="com.zhu.dao"></package>
</mappers>
此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一个目录中。
五、方式2:编写 Dao 实现类
------->项目源码
使用 Mybatis 开发 Dao,通常有两个方法,即原始 Dao 开发方式和 Mapper 接口代理开发方式。 而现在主流的开发方式是接口代理开发方式,这种方式总体上更加简便。以上的案例就是接口代理开发的方式,现在通过案例,学习基于传统编写 Dao 实现类的开发方式。
各项配置还是沿用之前的。
查询列表
由于现在需要自己实现dao接口,那么先写一个实现类:
先实现findAll方法:
/**
* Dao实现类
*/
public class UserDaoImpl implements IUserDao {
private SqlSessionFactory factory;
public UserDaoImpl(SqlSessionFactory factory){
this.factory = factory;
}
@Override
public List<User> findAll() {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用SqlSession中的方法,实现查询列表
List<User> users = session.selectList("com.zhu.dao.IUserDao.findAll");//参数就是能获取配置信息的key
//3.释放资源
session.close();
return users;
}
测试类与之前最主要的区别就是,SqlSession对象此时是在自己的dao实现类中使用了,因此测试类中不再需要获取:
/**
*
* 测试mybatis的crud操作
*/
public class MybatisTest {
private InputStream in;
private IUserDao userDao;
@Before
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.使用工厂对象,创建dao对象
userDao = new UserDaoImpl(factory);
}
@After
public void destroy()throws Exception{
//6.释放资源
in.close();
}
/**
* 测试查询所有
*/
@Test
public void testFindAll(){
//5.执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users){
System.out.println(user);
}
}
测试结果:
插入操作
实现类中添加:
@Override
public void saveUser(User user) {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用方法实现保存
session.insert("com.zhu.dao.IUserDao.saveUser",user);
//3.提交事务
session.commit();
//4.释放资源
session.close();
}
别忘了提交事务,以及insert方法需要传一个来源对象。
测试类添加:
/**
* 测试保存操作
*/
@Test
public void testSave(){
User user = new User();
user.setUsername("dao impl user");
user.setAddress("广州市天河区");
user.setSex("男");
user.setBirthday(new Date());
System.out.println("保存操作之前:"+user);
//5.执行保存方法
userDao.saveUser(user);
System.out.println("保存操作之后:"+user);
}
测试结果:
更新、删除、查询单个、聚合查询
代码结构都与之前的类似,注意查询单个使用的是session.selectOne
实现类:
@Override
public void updateUser(User user) {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用方法实现更新
session.update("com.zhu.dao.IUserDao.updateUser",user);
//3.提交事务
session.commit();
//4.释放资源
session.close();
}
@Override
public void deleteUser(Integer userId) {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用方法实现更新
session.update("com.zhu.dao.IUserDao.deleteUser",userId);
//3.提交事务
session.commit();
//4.释放资源
session.close();
}
@Override
public User findById(Integer userId) {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用SqlSession中的方法,实现查询一个
User user = session.selectOne("com.zhu.dao.IUserDao.findById",userId);
//3.释放资源
session.close();
return user;
}
@Override
public List<User> findByName(String username) {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用SqlSession中的方法,实现查询列表
List<User> users = session.selectList("com.zhu.dao.IUserDao.findByName",username);
//3.释放资源
session.close();
return users;
}
@Override
public int findTotal() {
//1.根据factory获取SqlSession对象
SqlSession session = factory.openSession();
//2.调用SqlSession中的方法,实现查询一个
Integer count = session.selectOne("com.zhu.dao.IUserDao.findTotal");
//3.释放资源
session.close();
return count;
}
测试类:
/**
* 测试更新操作
*/
@Test
public void testUpdate(){
User user = new User();
user.setId(8);
user.setUsername("userdaoimpl update user");
user.setAddress("上海市黄浦区");
user.setSex("女");
user.setBirthday(new Date());
//5.执行保存方法
userDao.updateUser(user);
}
/**
* 测试删除操作
*/
@Test
public void testDelete(){
//5.执行删除方法
userDao.deleteUser(8);
}
/**
* 测试查询单个操作
*/
@Test
public void testFindOne(){
//5.执行查询一个方法
User user = userDao.findById(5);
System.out.println(user);
}
/**
* 测试模糊查询操作
*/
@Test
public void testFindByName(){
//5.执行查询一个方法
List<User> users = userDao.findByName("%东%");
for(User user : users){
System.out.println(user);
}
}
/**
* 测试查询总记录条数
*/
@Test
public void testFindTotal(){
//5.执行查询一个方法
int count = userDao.findTotal();
System.out.println(count);
}
分别测试——
更新:
删除:
查询单个:
模糊查询:
查询总记录:
小结
通过对比可以看出来,通过DAO实现类的开发方式,需要多写许多代码,因此这种方式用得并不多。
深究其原因,可以通过下文的分析执行过程来得出结论。
六、从源码分析Mybatis的CRUD执行过程
下面分析对于自己写DAO实现类的这种开发方式,mybatis内部的执行细节。
以分析查询列表方法为例,从我们的实现类开始,点击session.selectList()查看源码,当进入一个接口且它有多个实现类时,可以通过断点调试的方法来判断执行过程中调用的是哪一个实现类,这样就能继续深入追踪了。
分析selectList()方法
从session.selectList()开始,一路追踪,最终到 PreparedStatementHandler这个类为止,可以整理出从selectList()方法直到ps.execute()所调用的所有的类及其方法(点击查看大图更清晰):
PreparedStatementHandler.query的代码如下,最终执行sql的,不就是我们在JDBC中熟悉的 preparedStatement.execute()吗?
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
读代码可知,执行完execute()之后,再通过handleResultSets进行结果集的封装,追踪到这里,mybatis内部如何执行selectList()方法,已经不再神秘!
附上涉及到的部分类图:
分析其他方法
insert()、update()、delete()
为了方便归纳,实体类进行删除操作时,原先的update改为通过delete方法完成:
接下来如法炮制,分析session.insert()、session.update()、session.delete()三个方法。为什么要把这三个放在一起呢?因为它们在DefaultSqlSession中,其实调用的都是同一个方法:update(),看下图就明白了(点击查看大图更清晰):
同样,一路追踪,最终到 PreparedStatementHandler这个类,执行update方法:
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
综上,尽管mybatis为我们提供了session.insert()、session.update()、session.delete()这些方法,但是其底层都是通过preparedStatement.execute()来实现最终对数据库的SQL操作的。
selectOne()
同上,可以很快发现,session.selectOne()方法,会在DefaultSqlSession中调用selectList()方法,其后的流程上面已经分析过了。
七、从源码分析Mybatis的CRUD执行过程(代理DAO方式)
上面分析的是DAO实现类的方式,现在对更为常用的代理DAO方式进行分析(点击查看大图更清晰)。
从测试类开始,一路追踪到MapperMethod对象中,最终execute()方法执行的是session.insert()、session.update()、session.delete()三个熟悉的方法,看了上文的分析,我们已经很清楚,至此之后的执行过程和自己写DAO实现类的实现方式是一模一样的。稍有不同的sqlSession.selectList()方法也在同一个类中的executeForMany()方法中被调用,同样,又与上文的分析连接到了一起。
小结
为什么Mybatis能让我们无需写DAO实现类?
因为它已经帮我们调用了,当我们自己写DAO实现类时所调用的方法!
既然Mybatis已经帮我们做了这些事情,那我们就没有理由再做一遍——这就是为什么大多使用DAO代理的方式来进行开发的理由。