目录
1. 单表查询的进阶知识
1.1 参数占位符 #{} 和 ${} 的区别
1.1.1 #{} 和 ${} 的区别一 (#{} 胜一分)
1.1.2 #{} 和 ${} 的区别二 (${} 胜一分)
1.1.3 #{} 和 ${} 的区别三 - 最主要的区别 (${} 惨败)
1.2 like 查询
2. 多表查询的进阶知识
2.1 查询的返回类型: resultMap
2.2 多表查询示例
3. 动态 SQL 的使用 (抽空补完)
1. 单表查询的进阶知识
1.1 参数占位符 #{} 和 ${} 的区别
1.1.1 #{} 和 ${} 的区别一 (#{} 胜一分)
1. #{} 预编译处理
- MyBatis 在处理 #{} 时, 底层还是会使用 JDBC那一套, 将 SQL 中的 #{} 替换成 '?' , 然后使用预编译 PreparedStatement 的 setXXX() 方法进行赋值.
2. ${} 字符串直接替换
- MyBatis 在处理 ${} 时, 会直接把 ${} 替换成变量的值.
【代码示例】 根据 username 查询用户
1. 接口中的方法
根据用户名的全字匹配, 假设数据库中的名字唯一.
public UserInfo selectByName(@Param("username") String username);
2. 对应 XML 的具体实现(对比两种写法)
写法一 #{}:
<select id="selectByName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=#{username}
</select>
写法二 ${}:
<select id="selectByName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=${username}
</select>
3. 对该功能分别使用两种 xml 的写法进行单元测试:
#{} 参数占位符的测试结果: 能够正确查询到我们想要的数据
${} 参数占位符的测试结果:
上述测试结果报的错误其实就等同于以下 SQL 语句:
【结论】: 使用 ${} 参数占位符时, 相当于参数直接替换, 它的问题就是会带来越权查询和操作数据等问题. (当传递的参数是整型时, 两种参数占位符的测试结果就会给我们一个效果相同的假象)
【扩展】既然 ${} 是直接替换, 那我是否可以给所有的参数都先加上引号 '${}', 大力出奇迹, 这样不就解决上述问题吗 ?
- 对于字符串类型, 加上引号确实可以解决
- 但是对于是整型, double 类型的数据等等, 我们也这样去给它们去加引号, 就会存在一个问题.
1. 从结果上来看, 对于整型数据,, 加上引号和不加引号的查询结果一致. 但是对于给整型加引号这种方式存在 "隐式类型转换" 的问题, MySQL 帮我们做了这个事情, 查询的时候一旦有类型转换, 查询的过程就不会走 "索引" 了.此时查询性能就会非常非常低.
2. 此处是因为数据量少, 所以查询的时候看起来效果一样, 如果数据量有几百个t, 那么走索引的查询可能话费 2ms, 而存在类型转换的查询所需要的时间可能就是 20 分钟!!
1.1.2 #{} 和 ${} 的区别二 (${} 胜一分)
>>> 一个程序里面传参, 使用 #{} 已经可以解决 99.99% 的问题了, 那为啥还需要搞出一个 ${} ?
虽然 #{} 已经很完美了, 但是还有一小部分的事情还是需要 ${} 来解决. 正所谓事物的存在, 是有一定的道理的, 它的价值就在于传参时, 能够执行 MySQL 的关键字.
${} 参数占位符接收关键字参数
【代码示例】排序查询
1. 接口中的方法:
public List<UserInfo> selectAllOrder(@Param("order") String order);
2. 对应 xml 中的具体实现:
<select id="selectAllOrder" resultType="com.example.demo.model.UserInfo">
select * from userinfo order by id ${order}
</select>
3. 单元测试
测试结果:
如果使用 #{} 参数占位符, 在预处理的时候就会给排序规则加上引号, 就相当于犯了以下 SQL 语句的错误:
1.1.3 #{} 和 ${} 的区别三 - 最主要的区别 (${} 惨败)
【SQL 注入问题】
什么是 SQL 注入?
SQL 注入就是在不知道你用户名和密码的情况下, 直接就能够查到你的信息. SQL 注入就是一串简单的字符串, 例如 " ' or 1='1 ", 它利用的就是 MySQL 的漏洞搞事情.
【示例】
为了方便演示, 我只留一条数据.
此时我在不知道用户名和密码的情况下, 输入以下 SQL 语句就能轻松拿到用户信息了.
select * from userinfo where username='张三' and password='' or 1='1';
上述 SQL 语句, password 中的 ' or 1= '1 参数, 左边的单引号 ' 和前面的单引号匹配上了, 然后 or 后面的 1='1' 属于隐式转换, 等式恒成立. 于是整个 SQL 就恒成立, 所以在不知道密码和用户名的情况下就可以获取到用户信息了.
在程序中 #{} 可以避免 SQL 的问题, 而使用 ${} 就会出现 SQL 问题.
【代码示例】演示 ${} 的问题
1. 接口中的方法
public UserInfo selectByNameAndPwd(@Param("username") String username,
@Param("password") String password);
2. 对应 xml 中的具体实现
<select id="selectByNameAndPwd" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=${username} and password=${password}
</select>
3. 单元测试
单元测试结果:
1. 由此可见, 如果我们的参数占位符是 ${} , 就一定存在 SQL 注入的问题, #{} 参数占位符在测试的时候, 是查询不到结果的, 会显示 null , 因为它是预编译处理的, 它认为 ' or 1='1 就是一个字符串, 所以就不会出现 SQL 注入的情况. 这里就不验证了, 大可以自己测试一下.
2. 所以我们前面在使用 ${} 接收参数的时候, 务必要在 controller 层验证一下, 如果传递过来的参数是 'asc' 或者 'desc' 才继续往下走, 否则后面的代码就不要去执行了. 这样也能避免 SQL 注入.
1.2 like 查询
模糊查询的时候使用 #{} 就不太合适.
【代码示例】
1. 接口中的方法
public List<UserInfo> selectLike(@Param("username") String username);
2. 对应 xml 的具体实现
<select id="selectLike" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like '%#{username}%'
</select>
3. 单元测试
1. 还是因为 #{} 是预编译处理, 所以会给username 加引号, 最后就会变成 '%'username'%', 这样的 SQL 语句在语法上就有错.
2. 而使用 ${} 确实能解决问题, 但是前面讲了它存在 SQL 注入的风险, 不安全, 需要在 Controller 层去验证参数的, 像前面的排序查询, 是可以穷举的, 要么是 asc , 要么是 desc, 而此处的模糊查询是不可穷举的. 所以也不会使用 ${}
>>> 那么模糊查询需要怎么接收参数呢 ?
模糊查询可以使用 MySQL 的内置函数 concat() 来处理, 具体 xml 代码:
<select id="selectLike" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%')
</select>
下面进行单元测试检验查询结果:
再从数据库进行模糊查询进行对比, 结果正确:
2. 多表查询的进阶知识
2.1 查询的返回类型: resultMap
对于查询 <select> 标签来说至少有两个属性:
- id 属性: 用于标识实现接口中的哪个方法
- 结果映射属性: resultType/resultMap
绝大多数情况都可以 使用 resultType, 它有很明显的优势, 使用方便, 直接定义到某个实体类即可, 在 Mybatis 初阶博客 里面的查询, 以及前面的模糊查询都是使用的 resultType, 此处就不演示了. 而 resultMap 使用起来就相对来说要麻烦一些, 但是它可以解决字段名和属性名不相同的情况.
resultMap 主要应用场景
1. 字段名和属性名不同的场景. (针对查询的影响)
2. 一对一, 一对多关系可以使用 resultMap 映射并查询数据.
>>> 为什么会有字段名和属性名不同的情况 ?
因为在大公司, Java 程序和数据库是有两个岗位的人去开发的, 不同的岗位在代码上的规范也是不同的, 数据库有数据库的规范, Java 有 Java 的一套规范.所以两拨人去写代码的时候, 难免会出现一些属性名和字段名不相同的情况. (例如当一个字段出现多个单词组合额情况, 数据库可以使用下划线组成, 而 Java 是使用小驼峰. 各有各的规范)
例如:
先演示一下当属性名和字段名不相同的时候, 我们再使用 resultType, 此时单元测试的结果是什么:
【现象】
当属性名和字段名不一致时(username/name, id/uid), 单元测试不报错, 并且查询结果对应的属性是没有值的 (默认值)
【解决】
此时需要使用 resultMap 来解决, 对应的 xml 改写 (字典映射):
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
<id column="id" property="uid"></id>
<result column="username" property="name"></result>
<result column="password" property="password"></result>
<result column="photo" property="photo"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="state" property="state"></result>
</resultMap>
1. 首先要加上 <resultMap> 标签, 它有两个属性, 一个是 id , 一个是 type.
- id 是用来标识当前是属于与哪个 xml 文件的, <resultMap> 它的作用域属于当前 xml 文件, 所以其他 xml 文件下是允许重名的.
- type 属性表示要映射的实体类.
2. <result> <id> 标签里面就是用来表示不同字段名和属性名对应的映射关系. column 中写的是字段名, property 中写的是属性名.
- <id> 标签是用来给主键使用的
- <result> 标签是给普通字段使用的.
3. 对应的查询标签中的 resultType 改成 resultMap.
此时再针对普通查询进行单元测试, 此时查询结果, 对应的属性上就有值了.
>>> 虽然我们此处只有两个属性和字段名不匹配, 但是我们最好把所有的属性和字段的映射都写上去, 即使它们相同,在单表查询的时候不会出问题, 但是在一些特殊的情况下如果我们不写还是可能会出错的(例如一对多的多表查询). 总之, 我们遵守规范就一定不会出错.
>>> 另外想要解决字段名和属性名不一致的问题, 除了 resultMap, 其实还可以通过属性重命名的方式来解决, 后面演示示例.
2.2 多表查询示例
前面我们只创建了用户表, 此时我们再添加一个文章表:
【示例】查询文章表, 并显示文章对应的作者名字;
此处的查询涉及到两张表, 但是查询结果并不是想要得到两张表的数据, 而是想要拿到文章表和用户表相关的数据, 所以属于外连接, 且文章表为主体.
第一步, 在对应的文章 model 中添加作者属性:
第二步, 提供文章表使用的接口和查询方法:
@Mapper
public interface ArticleMapper {
// 查询文章表, 并显示文章对应的作者名字
public List<ArticleInfo> selectAll();
}
第三步, 写对应的 xml 实现:
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id column="id" property="id"></id>
<result column="title" property="title"></result>
<result column="content" property="content"></result>
<result column="createtime" property="createTime"></result>
<result column="updatetime" property="updateTime"></result>
<result column="uid" property="uid"></result>
<result column="rcount" property="rCount"></result>
<result column="state" property="state"></result>
<result column="username" property="author"></result>
</resultMap>
<select id="selectAll" resultMap="BaseMap">
select a.*, u.username from articleinfo a
left join userinfo u
on a.uid = u.id
</select>
然后进行单元测试, 验证结果是否正确:
查询结果正确!!
>>> 假如我还想显示用户表的其他字段呢
无论是两张表, 三张表, 你需要什么字段, 直接在 ArticleInfo 实体类中加上相应的属性即可, 但是要注意的是, 要么保持字段属性名一致.要么使用 resultMap 字典映射. 其实还可以使用起别名的方式来解决.
【起别名解决属性字段名不匹配问题】
还是上面那个代码示例, 为了便于区分重命名的字段, 我先将其他属性和数据库字段名保持一致, 文章表中留一个 author 属性和字段名不一致来演示效果.
此时 xml 不使用 resultMap, 依然使用 resultType, 但是 SQL 语句使用起别名的方式来做到 resultMap 的效果. (u.username as author)
<select id="selectAll" resultType="com.example.demo.model.ArticleInfo">
select a.*, u.username as author from articleinfo a
left join userinfo u
on a.uid = u.id
</select>
单元测试结果如下, 依然能够正确的查询储结果, 并且 author 上面也有对应的作者名.
3. 动态 SQL 的使用
什么是动态 SQL ? 来看看官方是怎么说的:
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
简言之: 动态 sql 是Mybatis的强大特性之⼀,能够完成不同条件下不同的 sql 拼接.
3.1 <if> 标签
例如在注册/登录 的时候, 我们会遇到一个这样的问题:
- 如果只有一个非必填字段的时候, 那么我只需要给出两个方法, 有一个有参数, 一个无参数.
- 但是很多时候是有很多选项, 有些是必填项, 有些是非必填项, 这样我们在写代码的时候, 为了考虑用户的各种情况, 就需要对这些选项进行排列组合写出包含各种情况的方法.
这样就会非常痛苦, 如果你想使用简单的 if else来解决, 这样会把你累死, 而且你的代码可维护性将会变得非常糟糕. 此时就需要动态 SQL 的 <if> 标签来解决了.
最近临近考试, 剩下的类容 (几个标签) 抽空再继续讲解!!