MyBatis
- MyBatis 是一款优秀的持久层框架,用于简化 JDBC 开发,它封装了JDBC大部分的操作。
- MyBatis 本是 Apache 的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
- 官网:https://mybatis.org/mybatis-3/zh/index.html
Mybatis
就是对jdbc
代码进行了封装。
首先新建maven
项目,在pom.xml
中添加依赖,其中mysql-connector-java
是mysql
驱动,mybatis需要用到,如果是用数据库连接池Druid则不用,因为其内置了mysql驱动。
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
</dependencies>
配置文件开发
文件结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2OvjElE0-1661335163347)(assets/image-20220824170421795.png)]
Brand类如下
public class Brand {
// id 主键
private Integer id;
// 品牌名称
private String brandName;
// 企业名称
private String companyName;
// 排序字段
private Integer ordered;
// 描述信息
private String description;
// 状态:0:禁用 1:启用
private Integer status;
}
有两个文件一个是BrandMapper.java
(接口文件),另一个是BrandMapper.xml
(Sql配置文件,可以在这里写sql语句)
// BrandMapper.java
public interface BrandMapper {
//查询所有
List<Brand> selectAll();
}
<!-- BrandMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test"> // 这里的namespace可以随意
<select id="selectAll" resultType="org.mybatis.pojo.Brand">
select * from tb_brand; // 在这里写sql语句,结果类型要写全。
</select>
</mapper>
然后最重要的就是mybatis配置文件mybatis-config.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<!-- 配置多环境,默认为development-->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!--链接本地数据库-->
<property name="url" value="jdbc:mysql:///db_test?useSSL=false&useServerPrepStmts=true"/>
<property name="username" value="root"/>
<property name="password" value="xxxx"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 单独加载sql映射文件-->
<mapper resource="BrandMapper.xml"/>
</mappers>
</configuration>
接下来就可以执行sql语句了
public class MybatisDemo {
public static void main(String[] args) throws IOException {
// 加载配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 由工厂构建器构建一个SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象,用它来执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 执行sql
List<Brand> brands = sqlSession.selectList("test.selectAll");
for(Brand b:brands){
System.out.println(b);
}
}
}
这里用的是SqlSession来执行的
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Brand> brands = sqlSession.selectList("test.selectAll");
参数是一个字符串,该字符串必须是映射配置文件的namespace.id
。 此时的namespace
可以随便起名,因为这里是准确传入namespace
的,java能够找到。
mapper代理开发
基于上面的条件,第二种执行方式(推荐)
public class MybatisDemo {
public static void main(String[] args) throws IOException {
// 加载配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 由工厂构建器构建一个SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象,用它来执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
// sqlSession执行
// List<Brand> brands = sqlSession.selectList("test.selectAll");
// mapper代理开发
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = mapper.selectAll();
for(Brand b:brands){
System.out.println(b);
}
}
}
关键在这两句
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);//把mapper类传入
List<Brand> brands = brandMapper.selectAll();//然后就可以直接调用方法
但是这种方式要求比较高:需要
-
定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下
-
设置SQL映射文件的namespace属性为Mapper接口全限定名
-
在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致
因此我们需要对项目做出相应的修改,针对第一条:在resource下新建目录: com/example/mapper
把BrandMapper.xml
文件放到该目录下,并将BrandMapper.xml
的namespace
修改为BrandMapper
接口的全限定名。
<mapper namespace="com.example.mapper.BrandMapper">
<select id="selectAll" resultType="com.example.pojo.Brand">
select * from tb_brand
</select>
</mapper>
另外,因为BrandMapper.xml
的位置发生了改变,还需要对mybatis-config.xml
进行修改:
<mappers>
<mapper resource="com/example/mapper/BrandMapper.xml"/>
</mappers>
注意这里的路径分隔符是
/
而不是.
,因为他是处于resources下的,不是package,而是目录。
经过上述修改之后,文件结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q3Un3zfO-1661335163348)(assets/image-20220824172840626.png)]
可以看到,在target目录下,BrandMapper接口和BrandMapper.xml文件在同一个目录下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dGp52km-1661335163349)(assets/image-20220719211135635.png)]
然后就可以通过mapper代理的方式执行sql语句了
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);//把mapper类传入
List<Brand> brands = brandMapper.selectAll();//然后就可以直接调用方法
小结
- 首先要保证Sql映射文件(
BrandMapper.xml
)和接口(BrandMapper.java
)是同名的,且在同一个目录下。- 例如接口位置:
com.example.mapper.BrandMapper
- 映射文件位置:
com/example/mapper/BrandMapper.xml
- 例如接口位置:
- 其次就是接口映射文件中的
namespace
必须是接口的全限定名com.example.mapper.BrandMapper
- 之前通过
session
执行sql的时候是用的namespace.id
的方式,这种方式namespace
可以随便写都能找到。 - 而
mapper
代理开发的方式寻找sql执行方法的时候默认的namespace是接口全限定名。 - 因此我们要把
namespace
写成接口全限定名mybatis才能找到执行sql的方法在哪个接口。
- 之前通过
- 注意要在
mybatis-config.xml
中导入mapper
映射文件com/example/mapper/BrandMapper.xml
- 可以使用包扫描的方法,这样就不用挨个导入了。
- 这里是目录,分隔符为
/
mapper代理开发改进
resultType别名
表示org.mybatis.pojo
下的所有实体类都可以起别名。
<typeAliases>
<package name="com.example.pojo"/>
</typeAliases>
而这个包下放的是Brand类。因此这个包下的类都可以使用别名。
这样起别名的好处是在接口映射文件的resultType中可以使用简单的名称:resultType="Brand"
,而不用写全名。
<mapper namespace="com.example.mapper.BrandMapper">
<select id="selectAll" resultType="Brand">
select * from tb_brand
</select>
</mapper>
包扫描加载sql映射文件
如果Mapper接口名称和SQL映射文件名称相同,并在同一目录下,则可以使用包扫描的方式简化SQL映射文件的加载。也就是将核心配置文件的加载映射配置文件的配置修改为:
<mappers>
<!-- <mapper resource="com/example/mapper/BrandMapper.xml"/>-->
<package name="com/example/mapper"/>
</mappers>
resultMap
这个是用来解决实体类的属性名和数据库中列名不一致的问题的。
例如实体类中喜欢用驼峰: brandCompany
,而数据库中喜欢用下划线: brand_company
,这样会导致查询的结果无法封装到实体类中去,或者实体类中的数据无法更新到数据库中去。
Brand{id=1, brandName='null', companyName='null', ordered=50, description='好吃不上火 ', status=1}
在 BrandMapper.xml中:
<mapper namespace="com.example.mapper.BrandMapper">
<resultMap id="brandResult" type="brand">
<result property="companyName" column="company_name"/>
<result property="brandName" column="brand_name"/>
</resultMap>
<select id="selectAll" resultMap="brandResult">
select * from tb_brand
</select>
</mapper>
mybatis传参数
没有参数的情况
List<Brand> selectAll();
<select id="selectAll" resultType="Brand">
select * from tb_brand
</select>
只有一个参数的情况
Brand selectById(int id); //接口中的定义
映射文件中的定义
<select id="selectById" resultType="Brand">
select * from tb_brand where id = #{id};
</select>
使用:
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
Brand brand = brandMapper.selectById(1);
多个参数的情况
方法一:通过构造一个POJO对象传入多个参数
Brand selectByIdAndBrandNameByObject(Brand brand);
<!-- 用Brand对象最为参数来传递多参数,要求{}的名字和Brand对象的属性名相同-->
<select id="selectByIdAndBrandNameByObject" resultMap="brandResult">
select * from tb_brand where id = #{id} and brand_name = #{brandName};
</select>
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
Brand b = new Brand();
b.setId(10);
b.setBrandName("中国好人");
Brand brand = brandMapper.selectByIdAndBrandNameByObject(b);
方法二:通过注解传参数
Brand selectByIdAndBrandNameByAnnotation(@Param("ID") int ID, @Param("bName") String bName);
<!-- 通过注解的方式来传递多参数,要求{}的名字就是注解内的名字paramName @Param(paramName)-->
<select id="selectByIdAndBrandNameByAnnotation" resultMap="brandResult">
select * from tb_brand where id = #{ID} and brand_name = #{bName};
</select>
Brand brand = brandMapper.selectByIdAndBrandNameByAnnotation(10,"中国好人");
方法三:通过传入一个map传参数
Brand selectByIdAndBrandNameByMap(Map map);
<!-- 通过传入一个map的方式来传递多参数,要求{}的名字是map的key-->
<select id="selectByIdAndBrandNameByMap" resultMap="brandResult">
select * from tb_brand where id = #{key_id} and brand_name = #{key_brandName};
</select>
Map map = new HashMap<String,String>();
map.put("key_id",10);
map.put("key_brandName","中国好人");
Brand brand = brandMapper.selectByIdAndBrandNameByMap(map);
以上三种方式,最不推荐第三种,如果参数较少(两三个)那么用注解比较方便;如果参数多达四五个,那么构造一个POJO对象比较方便。
mybatis传递参数的原理
mybatis传递参数,可以传递单个参数或者多个参数,而单个参数可以是:
- POJO对象,例如传入一个Brand b;
- Map集合类型
- Collection集合类型(Map和Collection是两个派系)
- List集合类型
- Array类型
- 其他类型(例如int String 之类的)
而对于多个参数传递,mybatis底层对于多个参数的处理是将这些参数封装成Map集合对象,值就是参数值。而键在没有使用@Param
注解的时候命名为arg0,arg1…或者是 param1,param2…以此类推。
我们作以下实验:
Brand selectByIdAndStatus(int id, int status); //接口定义,没有使用注解,也没有使用map传入多个参数
<select id="selectByIdAndStatus" resultMap="brandResult">
select * from tb_brand where id = #{arg0} and status = #{arg1};
</select>
这里的参数使用arg0, arg1表示两个参数,能够返回正确的结果:
Preparing: select * from tb_brand where id = ? and status = ?; //arg0 arg1被替换成了占位符
Parameters: 1(Integer), 1(Integer)
Brand{id=1, brandName='三只松鼠', companyName='三只松鼠股份有限公司', ordered=5, description='好吃不上火', status=1} //查询结果封装成的Brand对象
我们进行更改同样能够得到上述的结果:
<select id="selectByIdAndStatus" resultMap="brandResult">
select * from tb_brand where id = #{param1} and status = #{param2}; //参数替换为param1,param2...以此类推
</select>
实际上就算替换成param1,arg1这样的组合也是可以的。即arg0和param1对应的是同一个参数。以此类推。
在接口方法参数上使用@Param注解,mybatis会将arg开头的键名替换为对应注解的属性值。
注意:只是arg参数进行了替换,param参数没有替换!
进行如下更改
Brand selectByIdAndStatus(@Param("id") int id, @Param("status") int status); //接口参数使用注解
发现param参数还能继续使用不报错,而如果替换成arg0,arg1就会报错,因为在map中这两个key已经被替换成了id和status.
<select id="selectByIdAndStatus" resultMap="brandResult">
select * from tb_brand where id = #{param1} and status = #{param2}; //参数替换为param1,param2...以此类推
</select>
查询结果名称处理
由于数据库中的名称和POJO对象的属性名不同:例如数据库中为company_name,POJO对象为companyName
这样导致查询结果无法对应,那么mybatis就不能自动封装为POJO对象。
方法一:查询的时候使用别名,即在sql语句中使用as
select company_name as companyName, ...
这样写比较麻烦
方法二:在映射文件中使用sql片段
也就是把sql语句中 替换的这部分提取出来,以后的查询语句可以复用。这样不方便,因为不是所有字段都用得上。
方法三:使用resultMap
<resultMap id="brandResult" type="brand">
<result column="brand_name" property="brandName"/>
<result column="company_name" property="companyName"/>
</resultMap>
column是数据库中的列标签,property是给定的属性,即别名。
<select id="selectById" resultType="Brand">
select * from tb_brand where id = #{id};
</select>
将resultType替换成resultMap即可
<select id="selectById" resultMap="brandResult">
select * from tb_brand where id = #{id};
</select>
最后,如果是使用resultType的话,需要写全名,即POJO类所在包名+类名
例如 resultType = org.mybatis.pojo.Brand
但是如果在核心配置文件中加上这个片段
<typeAliases>
<package name="org.mybatis.pojo"/>
</typeAliases>
则表明这个包下的类名都可以使用别名,且不区分大小写。resultType = Brand
或者resultType = brand
都是可以的。
resultType适用于查询结果中没有名称不一致的问题的情况。
另外在核心配置文件中需要导入映射文件
<mappers>
<!-- <mapper resource="org/mybatis/mapper/BrandMapper.xml"/>-->
<package name="org/mybatis/mapper"/>
</mappers>
使用<package name="org/mybatis/mapper"/>
可以一次将这个目录下的所有映射文件导入。
最后mybatis的核心配置文件是有顺序的,例如typeAliases就必须在environments的前面,否则无法生效。
动态SQL语句
if 和 where标签
多条件组合。
<select id="selectByCondition" resultMap="brandResult">
select * from tb_brand
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="companyName!=null and companyName != '' ">
and company_name like #{companyName}
</if>
<if test="brandName!=null and brandName != '' ">
and brand_name like #{brandName}
</if>
</where>
</select>
- 不用再写where,标签会自动加上
- 每一句都要写and,mybatis会自动把第一个and 去掉。
mapper接口如下
List<Brand> selectByCondition(Brand brand);
使用过程如下:此时传入的参数为一个POJO对象是最好的
Brand b = new Brand();
b.setStatus(1);
List<Brand> brands = brandMapper.selectByCondition(b);
打印mybatis执行日志可以看到实际执行sql语句为:(使用了预编译,即?作为占位符然后传入参数的方法,而不是拼接字符串的方法)
Preparing: select * from tb_brand WHERE status = ?
Parameters: 1(Integer)
如果更改为
Brand b = new Brand();
b.setBrandName("三只松鼠");
List<Brand> brands = brandMapper.selectByCondition(b);
Preparing: select * from tb_brand WHERE brand_name like ?
Parameters: 三只松鼠(String)
可见where和if的组合能够动态选择条件。
choose和when标签
单个条件选择
- 不用写where,标签会自动加上
- 不用写and,因为这是单个条件选择,只能多选一,而不是条件的组合。
<select id="selectByConditionSingle" resultMap="brandResult">
select * from tb_brand
<where>
<choose>
<when test="status!=null">
status = #{status}
</when>
<when test="companyName != null and companyName != '' ">
company_name like #{companyName}
</when>
<when test="brandName != null and brandName != '' ">
brand_name like #{brandName}
</when>
</choose>
</where>
</select>
Brand selectByConditionSingle(Brand brand);
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
Brand b = new Brand();
b.setBrandName("三只松鼠");
Brand brand = brandMapper.selectByConditionSingle(b);
Preparing: select * from tb_brand WHERE brand_name like ?
Parameters: 三只松鼠(String)
主键返回
多用于插入一条记录之后,返回主键。这里主键是自动增长的。(此处存疑,我理解的主键返回是:主键自增,插入数据之后返回主键的值,但是经过实验,好像都是插入成功返回1)
<insert id="add" useGeneratedKeys="true" keyColumn="id">
insert into tb_brand(brand_name, company_name, ordered, description, status) VALUES
(#{brandName},#{companyName},#{ordered},#{description},#{status} );
</insert>
Brand b = new Brand();
b.setBrandName("中国好人aaa");
b.setCompanyName("中国好人有限公司");
b.setStatus(1);
b.setOrdered(99);
b.setDescription("中国好人,做中国好产品!");
brandMapper.add(b);
sqlSession.commit();
尤其注意:需要手动提交事务,否则数据没有真正插入到数据库中
另外:id自增不一定是连续的。用户如果设置了已经存在的主键也不会影响插入,mysql会自动赋值一个主键。
或者可以设置自动提交事务
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//设置自动提交事务,这种情况不需要手动提交事务了
update和set
<update id="update">
update tb_brand
<set>
<if test="status != null">
status = #{status},
</if>
<if test="companyName!=null and companyName != '' ">
company_name = #{companyName},
</if>
<if test="brandName!=null and brandName != '' ">
brand_name = #{brandName},
</if>
<if test="description != null and description != '' ">
description = #{description},
</if>
<if test="ordered != null">
ordered = #{ordered}
</if>
</set>
where id = #{id};
</update>
- 不用写set,标签会自动加上
- 除了最后一个if之外,其余的都要加上逗号
,
因为set语句就是分开设置的,而不是用and连接的。
update `tb_brand` set brand_name="新产品",ordered=-1 WHERE id = 24;
批量删除 delete foreach
<delete id="deleteById">
delete from tb_brand where id in
<foreach collection="array" item="id" separator="," open="(" close=")">
#{id}
</foreach>
;
</delete>
- array是参数默认的名字,如果使用注解传参,则会更改参数名字,此处也要做对应的修改
void deleteById(int[] ids);
int[] ids = {23,24,25};
brandMapper.deleteById(ids);
sqlSession.commit();//如果没有设置自动提交事务,涉及到对数据库的修改需要手动提交事务。
Preparing: delete from tb_brand where id in ( ? , ? , ? ) ;
Parameters: 23(Integer), 24(Integer), 25(Integer)
设置自动提交事务: SqlSession sqlSession = sqlSessionFactory.openSession(true);
注解实现CURD
使用注解开发会比配置文件开发更加方便,但是仅仅适用于sql语句比较简单的情况,例如
@Select(value="select * from tb_brand where id = #{id}")
Brand select(int id);//mapper接口
此外还有
- @Insert
- @Update
- @Delete
@Select("select * from tb_brand where id = #{id}")
List<Brand> selectByIdByAnnotation(int id);
使用注解就实现了sql查询,这样就不用在映射文件中写sql语句了。
final List<Brand> brands = brandMapper.selectByIdByAnnotation(1);
System.out.println(brands);
brandMapper.deleteById(ids);
sqlSession.commit();//如果没有设置自动提交事务,涉及到对数据库的修改需要手动提交事务。
设置自动提交事务:
SqlSession sqlSession = sqlSessionFactory.openSession(true);