首先我们先取一个项目:
数据库表(tb_brand)及数据准备:
-- 删除tb_brand表
drop table if exists tb_brand;
-- 创建tb_brand表
create table tb_brand (
-- id 主键
id int primary key auto_increment,
-- 品牌名称
brand_name varchar(20),
-- 企业名称
company_name varchar(20),
-- 排序字段
ordered int,
-- 描述信息
description varchar(100),
--
状态:0:禁用 1:启用
status int );
-- 添加数据
insert into tb_brand (brand_name, company_name, ordered, description, status) values
('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联 的智能世界', 1),
('小米', '小米科技有限公司', 50, 'are you ok', 1);
实体类 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;
}
安装 MyBatisX 插件:
MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。
主要功能:
XML映射配置文件 和 接口方法 间相互跳转。
根据接口方法生成 statement。
在没有MybatisX前,因为接口方法要跟XML文件中的select标签中的id要对应,因此会在XML文件和接口中来回切换,观察是否对应。安装了MybatisX后就解决了这一问题。
插件效果:
红色头绳的表示映射配置文件,蓝色头绳的表示mapper接口。在mapper接口点击红色头绳的小鸟图标会自动跳转到对应的映射配置文件,在映射配置文件中点击蓝色头绳的小鸟图标会自动跳转到对应的mapper接口。也可以在mapper接口中定义方法,自动生成映射配置文件中的 statement ,如图所示:
要完成Mybatis的操作步骤:
1.创建接口,编写接口方法
2.创建对应的XML文件,XML文件要与对应的接口在同一目录下,再在XML中编写SQL
3.创建一个普通类,执行对应的SQL方法,获得返回值。
具体步骤可以参考上一篇mybatis(1)。
但是这样有一个小问题,当我们去打印获取到的数据的时候,有:
但是数据库中的对应的值并不是null,我们发现此时Brand实体类中brandName是驼峰命名,而数据库中对应的属性名字是brand_name,此时名字不相同就不能自动的给相应的Brand对象赋值。
为了解决这个问题,可以在查询的时候给查询的字段用as 关键字设置别名,就可以做到数据库中查询出来的字段跟Brand里面属性的字段对应起来。如:
此时打印出来的结果就不为null了。
但是这么做有缺点,每次查询的时候都要起别名。为此,我们可以引入sql片段:
sql片段可以对我们查询的语句做一个记录,当用到这条语句的时候,就采用include标签引入sql片段即可。
但是sql片段也有缺点:他很不灵活。如:我们只需要brand_name的字段时,我们要设置一个sql片段;当我们需要brand_name和其它字段时,又要设置sql片段,就很麻烦。
此时就引入一个resultMap:这个resultMap可以做到映射的作用。
在映射配置文件中使用resultMap定义 字段 和 属性 的映射关系:
resultMap后的id,是resultMap的唯一标识。type是实体类的名字,支持别名。
result中的column对应的是数据库中列名的名字。property是映射到实体类中的属性名。
注:在上面只需要定义 字段名 和 属性名 不一样的映射,而一样的则不需要专门定义出来。
在ResultMap中映射好关系后,XML文件中的SQL语句就可以正常写了:
<select id="selectAll" resultMap="brandResultMap">
<!--此处的resultType改为resultMap-->
select
*
from tb_brand ;
</select>
注:select标签中,在没有使用resultMap时,是resultType属性。而现在需要改为resultMap属性,后面紧跟的是resultMap的id。
查看详情,无非就是根据数据库中,他在某个表中的id字段来获取。
那么要在传入id字段,在BrandMapper中有:Brand selectById(int id);
而对应地,在BrandMapper.xml中设置:
<select id="selectById" resultMap="brandResultMap">
select * from tb_brand where id = #{id};
</select>
编写测试方法,在测试方法中执行SQL:
public class MybatisTest {
public static void main(String[] args) throws IOException {
int id = 1 ;
//加载mybatis的核心配置文件,获取sqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.获取sqlSessionFactory,执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.1 获取UserMapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
Brand brand = brandMapper.selectById(id);
System.out.println(brand);
sqlSession.close();
}
}
执行测试方法结果如下:
可以看到id后面的#{id} 被替换成了?,就等同于JDBC的sql代码了。
下面就讲到关于#的参数占位符。
mybatis提供了两种参数占位符:
#{} :执行SQL时,会将 #{} 占位符替换为?,将来自动设置参数值。从上述例子可以看出使用#{} 底层使用的是PreparedStatement。
${} :拼接SQL。底层使用的是 Statement ,会存在SQL注入问题。
如下图将 映射配置文件中的 #{} 替换成 ${} 来看效果:
可以发现,此时id后面直接就跟着1,是传入的参数。就引入了SQL注入的问题。
因此我们最好用#{} 。
以后肯定会在SQL语句中写一下特殊字符,比如某一个字段大于某个值,如下图:
可以看出报错了,因为映射配置文件是xml类型的问题,而 > < 等这些字符在xml中有特殊含义,所以此时我们需要将这些符号进行转义,可以使用以下两种方式进行转义。
1.转义字符
2.CDATA
在XML中打入CD,就有提示,如下图输入一个< 即可。
对于多条件的查询,可以有三种方法:1.用@Param注释来关联参数。2.实例化实体类传参。3.用Map装入SQL语句对应的字段名和值。
如下图:如果想在多个数据中查看某一类数据,就需要进行多条件的查询来限定:
而我们做这个功能需要分析最终的SQL语句应该是什么样,思考两个问题:1.条件表达式。2.如何连接。
条件字段 企业名称 和 品牌名称 需要进行模糊查询,所以条件应该是:
简单的分析后,我们来看功能实现的步骤:
1.编写接口方法:
参数:所有查询条件
结果:List
2.在映射配置文件中编写SQL语句
3.编写测试方法并执行
1.编写接口方法:
//使用 @Param("参数名称") 标记每一个参数,在映射配置文件中就需要使用 #{参数名称} 进行占位
List<Brand> selectByCondition(@Param("status")int status,@Param("companyName")String companyName,@Param("brandName")String brandName);
//将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内 容} 时,里面的内容必须和实体类属性名保持一致。
List<Brand> selectByCondition(Brand brand);
//将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容}时,里面的内容必须和map集合中键的名称一致。
List<Brand> selectByCondition(Map map);
2.编写SQL语句
在 BrandMapper.xml 映射配置文件中编写 statement ,使用 resultMap 而不是使用 resultType
<select id="selectByCondition" resultMap="brandResultMap">
select * from tb_brand where
status = #{status} and company_name like #{companyName}
and brand_name like #{brandName};
</select>
3.编写测试方法
在 test/java 下的 com.itheima.mapper 包下的 MybatisTest类中 定义测试方法:
@Test
public void testSelectByCondition() throws IOException {
int status = 1;
String companyName = "%"+"华为"+"%";
String brandName = "%"+"华为"+"%";
//加载mybatis的核心配置文件,获取sqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.获取sqlSessionFactory,执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.1 获取UserMapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = brandMapper.selectByCondition(status,companyName,brandName);
System.out.println(brands);
sqlSession.close();
}
注:这里的多条件查询要用到模糊匹配,此处设置的company和brandName要设置为模糊匹配的条件。这里传入的就对应到接口方法中要传入的参数。
上述功能实现存在很大的问题。用户在输入条件时,肯定不会所有的条件都填写,这个时候我们的SQL语句就不能那样写的。SQL语句会根据用户输入的条件自己地变动和适应。
针对上述的需要,Mybatis对动态SQL有很强大的支撑:
if
choose (when, otherwise)
trim (where, set)
foreach
实现动态SQL,只需要在xml文件下修改对应的SQL语句即可。
如:用户如果输入了 当前状态 和 企业名称 时,SQL语句是select * from tb_brand where status = #{status} and company_name like #{companName}
,那么如何在原来SQL语句的基础上变动呢?
可以用到Mybatis提供的if标签:if标签中的test属性写的是条件。
<select id="selectByCondition" resultMap="brandResultMap">
select * from tb_brand where
<if test="status!=null">
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>
</select>
此时我们只传入status和companyName时,是可以进行查询的,没有报错。
但是我们只传入companyName时,却报错了。
原因很简单:我们发现上面的SQL语句中,如果只有companyName属性,SQL语句就变为:select * from tb_brand where and company_name like ?
,此时只有一个条件时,有and 是错误的。此时删除and当有两个条件以上时却又行不通,因此删and又不是,不删and又不是。
为此,Mybatis中有where标签帮助我们解决这个问题:
注:在status的条件语句前也要加上and,当只有一个条件时,Mybatis的where标签会帮我们自动地删掉and。
<select id="selectByCondition" resultMap="brandResultMap">
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>
如上图所示,在查询时只能选择 品牌名称 、 当前状态 、 企业名称 这三个条件中的一个,但是用户到底选择哪儿一个,我们并不能确定。这种就属于单个条件的动态SQL语句。
这种需求需要使用到 choose(when,otherwise)标签 实现, 而 choose 标签类似于Java 中的switch语句。
通过一个案例来使用这些标签:
1.编写接口方法
List<Brand> selectByConditionSingle(Brand brand);
2.编写SQL语句
<select id="selectByCondition" resultMap="brandResultMap">
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>
3.编写测试类
此处跟多条件查询的几乎类似,只是设置实例化brand的时候,只设置一个参数。
结果:
当然,用户可能什么也不输入,此时如果是按照上面的SQL语句进行查询,会报错。因此要处理什么都不查询的时候错误。(只需要加上otherwise即可,条件为1=1):
<select id="selectByConditionSingle" resultMap="brandResultMap">
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>
<otherwise>
1==1
</otherwise>
</choose>
</where>
</select>
当然,我们要灵活的使用where标签,就可以不再使用otherwise了,Mybatis就可以把上面的otherwise规避的问题自动解决了。
编写接口方法:
void add(Brand brand);
编写SQL语句:
<insert id="add">
insert into tb_brand(brand_name, company_name, ordered, description, status)
values (#{brandName},#{companyName},#{ordered},#{description},#{status});
</insert>
编写测试代码:
@Test
public void addTest() throws IOException {
int status = 1;
String companyName = "波导手机";
String brandName = "波导";
String description = "手机中的战斗机";
int ordered = 100;
Brand brand = new Brand();
brand.setCompanyName(companyName);
brand.setBrandName(brandName);
brand.setDescription(description);
brand.setOrdered(ordered);
//加载mybatis的核心配置文件,获取sqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.获取sqlSessionFactory,执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.1 获取UserMapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
brandMapper.add(brand);
sqlSession.close();
}
我们运行测试的代码,可以发现运行没有报错。但是从数据库中查看这条数据是否已经插入时,却没有查到。
我们查看运行的结果:
看到了自动提交事务的操作是没有开启的,并且帮我们回滚了事务。
为了解决这个问题,有两个解决办法:
1.手动提交事务:在测试代码中调用了上面的add方法后,就sqlSession.commit();
来手动提交事务。
2.自动提交事务:在获取sqlSession时,传入一个true参数:SqlSession sqlSession = sqlSessionFactory.openSession(true);
添加——主键返回:
在数据添加成功后,有时候需要获取插入数据库数据的主键(主键是自增长)。
比如:添加订单和订单项,如下图就是京东上的订单:
订单数据存储在订单表中,订单项存储在订单项表中。假设我们添加了一个订单后,此时要添加订单项,订单项的id要与订单对应,但是此时拿不到订单的id就非常麻烦。
我们可以简单地演示一下:在上面的测试代码中,等到调用add方法后,去获取我们实例化的brand的id后,打印的结果是null。
解决方法:
在statement中添加两个属性:
useGenerateKeys:是够获取自动增长的主键值。true表示获取
keyProperty :指定将获取到的主键值封装到哪个属性里
此时再去测试代码中调用add方法后,打印实例化好的brand的id时,就可以打印出来了。
1.编写接口
int update(Brand brand);
2.编写SQL语句
<update id="update">
update tb_brand set brand_name = #{brandName},
company_name = #{companyName},
ordered = #{ordered},
description = #{description},
status = #{status}
where id = #{id};
</update>
3.编写测试代码:
@Test
public void testUpdate() throws IOException {
int status = 1;
String companyName = "波导手机";
String brandName = "波导";
String description = "波导手机,手机中的战斗机";
int ordered = 200;
int id = 5;
Brand brand = new Brand();
brand.setCompanyName(companyName);
brand.setBrandName(brandName);
brand.setDescription(description);
brand.setOrdered(ordered);
brand.setId(id);
//加载mybatis的核心配置文件,获取sqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.获取sqlSessionFactory,执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.1 获取UserMapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
int ret = brandMapper.update(brand);
System.out.println(ret);
sqlSession.close();
}
如图所示是修改页面,用户在该页面书写需要修改的数据,点击 提交 按钮,就会将数据库中对应的数据进行修改。注意一点,如果哪儿个输入框没有输入内容,我们是将表中数据对应字段值替换为空白还是保留字段之前的值?答案肯定是保留之
前的数据。
编写的接口不变,只需要修改对应的SQL语句即可。
如下代码:但是会发现两个问题:
1.如果status没有传入,则description就是最后一个条件语句,但是它的后面有逗号,SQL语句就报错了。
2.如果传入的东西都为空,则SQL语句还是会报错,set后面就没有东西了。
<update id="update">
update tb_brand set
<if test="brandName!=null and brandName!=''">
brand_name = #{brandName},
</if>
<if test="companyName!=null and companyName!=''">
company_name = #{companyName},
</if>
<if test="ordered!=null and ordered!=''">
ordered = #{ordered},
</if>
<if test="description!=null and description!=''">
description = #{description},
</if>
<if test="status!=null">
status = #{status}
</if>
where id = #{id};
</update>
我们可以使用set标签来解决上面的问题:
<update id="update">
update tb_brand
<set>
<if test="brandName!=null and brandName!=''">
brand_name = #{brandName},
</if>
<if test="companyName!=null and companyName!=''">
company_name = #{companyName},
</if>
<if test="ordered!=null and ordered!=''">
ordered = #{ordered},
</if>
<if test="description!=null and description!=''">
description = #{description},
</if>
<if test="status!=null">
status = #{status}
</if>
</set>
where id = #{id};
</update>
如上图所示,每行数据后面都有一个 删除 按钮,当用户点击了该按钮,就会将改行数据删除掉。那我们就需要思考,这种删除是根据什么进行删除呢?是通过主键id删除,因为id是表中数据的唯一标识。
1.编写接口:
void deleteByIds(@Param("ids") int[] ids);
2.编写SQL语句:
<delete id="delete">
delete from tb_brand where id = #{id};
</delete>
3.编写测试代码:
@Test
public void testDelete() throws IOException {
int id = 5;
Brand brand = new Brand();
brand.setId(id);
//加载mybatis的核心配置文件,获取sqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.获取sqlSessionFactory,执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.1 获取UserMapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
int ret = brandMapper.delete(brand);
System.out.println(ret);
sqlSession.close();
}
对于批量删除,因为删除是根据id来删除的,因此就要设置id数组来进行遍历地删除。
在 BrandMapper.xml 映射配置文件中编写删除多条数据的 statement 。
编写SQL时需要遍历数组来拼接SQL语句。Mybatis 提供了 foreach 标签供我们使用
编写SQL语句。
foreach 标签:
用来迭代任何可迭代的对象(如数组,集合)。
collection 属性:
mybatis会将数组参数,封装为一个Map集合。
默认:array = 数组,即map的key为array,map的value为数组。
使用@Param注解改变map集合的默认key的名称。当没有用@Param注解时,collection填的是array,否则运行测试代码时会报错。
item 属性:本次迭代获取到的元素。
separator 属性:集合项迭代之间的分隔符。 foreach 标签不会错误地添加多余的分隔符。也就是最后一次迭代不会加分隔符。
open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
<delete id="deleteByIds">
delete from tb_brand where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
;
</delete>
假如数组中的id数据是{1,2,3},那么拼接后的sql语句就是:
delete from tb_brand where id in (1,2,3);
可以看到是比较繁杂的。
编写测试代码:
@Test
public void testDeleteByIds() throws IOException {
int[] ids = {5,7,8};
//加载mybatis的核心配置文件,获取sqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.获取sqlSessionFactory,执行sql
SqlSession sqlSession = sqlSessionFactory.openSession();
//3.1 获取UserMapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
brandMapper.deleteByIds(ids);
sqlSession.close();
}
Mybatis 接口方法中可以接收各种各样的参数,如下:
1.多个参数
2.单个参数:单个参数又可以是如下类型:
POJO 类型
Map 集合类型
Collection 集合类型
List 集合类型
Array 类型
其他类型
在本篇博客中就提到,多个参数的传递方式有:用@Param注解、传入参数为实例化的实例类、传入参数为Map。
为了了解为什么要加入该注释,就需要了解Mybatis实现的源码。
如:
User select(@Param("username") String username,@Param("password") String password);
和
<select id="select" resultType="user">
select * from tb_user whereusername=#{username} and password=#{password}
</select>
我们在接口方法中定义多个参数,Mybatis 会将这些参数封装成 Map 集合对象,值就是参数值,而键在没有使用 @Param注解时有以下命名规则:
1.以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:
map.put("arg0",参数值1);
map.put("arg1",参数值2);
2.以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:
map.put("param1",参数值1);
map.put("param2",参数值2);
我们可以验证一下:
在 UserMapper 接口中定义如下方法:
User select(String username,String password);
在 UserMapper.xml 映射配置文件中定义SQL:
<select id="select" resultType="user">
select * from tb_user whereusername=#{arg0} and password=#{arg1} </select>
或者
<select id="select" resultType="user">
select * from tb_user whereusername=#{param1} and password=#{param2} </select>
运行代码结果如下:
在映射配合文件的SQL语句中使用用 arg 开头的和 param 书写,代码的可读性会变的特别差,此时可以使用 @Param 注解。
在接口方法参数上使用 @Param 注解,Mybatis 会将 arg 开头的键名替换为对应注解的属性值。
代码验证:
在 UserMapper 接口中定义如下方法,在 username 参数前加上 @Param 注解
User select(@Param("username") String username, String password);
Mybatis 在封装 Map 集合时,键名就会变成如下:
map.put(“username”,参数值1);
map.put(“arg1”,参数值2);
map.put(“param1”,参数值1);
map.put(“param2”,参数值2);
在 UserMapper.xml 映射配置文件中定义SQL:
<select id="select" resultType="user">
select * from tb_user whereusername=#{username} and password=#{param2}
</select>
运行程序结果没有报错。而如果将 #{} 中的 username 还是写成 arg0:
<select id="select" resultType="user">
select * from tb_user whereusername=#{arg0} and password=#{param2} </select>
运行程序则可以看到错误:
1.POJO 类型
直接使用。要求 属性名 和 参数占位符名称 一致
2.Map 集合类型
直接使用。要求 map集合的键名 和 参数占位符名称 一致
3.Collection 集合类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,collection集合);
map.put(“collection”,collection集合);
4.List 集合类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,list集合);
map.put(“collection”,list集合);
map.put(“list”,list集合);
可以使用 @Param 注解替换map集合中默认的 arg 键名。
5.Array 类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,数组);
map.put(“array”,数组);
可以使用 @Param 注解替换map集合中默认的 arg 键名。
6.其他类型
比如int类型, 参数占位符名称 叫什么都可以。尽量做到见名知意。
对于单个参数为什么可以用param和arg,这个需要查看ParamNameResolver这个类中的getNamedParams方法。
使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发:
@Select(value = "select * from tb_user where id = #{id}") public User select(int id);
注意:注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的 statement。
Mybatis 针对 CURD 操作都提供了对应的注解,已经做到见名知意。如下:
查询 :@Select
添加 :@Insert
修改 :@Update
删除 :@Delete
注意:在官方文档中 入门 中有这样的一段话:
所以,注解完成简单功能,配置文件完成复杂功能。