@github
你想不想来学习 mybatis? 学习其使用和源码呢?那么, 在博客园关注我吧!!sql
我本身打算把这个源码系列更新完毕, 同时会更新相应的注释。快去 star 吧!!数据库
MyBatis 使人喜欢的一大特性就是动态 SQL。 在使用 JDBC 的过程当中, 根据条件进行 SQL 的拼接是很麻烦且很容易出错的。 MyBatis 动态 SQL 的出现, 解决了这个麻烦。mybatis
MyBatis经过 OGNL 来进行动态 SQL 的使用的。app
目前, 动态 SQL 支持如下几种标签ide
元素 | 做用 | 备注 |
---|---|---|
if | 判断语句 | 单条件分支 |
choose(when、otherwise) | 至关于 Java 中的 if else | 多条件分支 |
trim(where、set) | 辅助元素 | 用于处理 SQL 拼接问题 |
foreach | 循环语句 | 批量插入, 更新, 查询时常常用到 |
bind | 建立一个变量, 并绑定到上下文中 | 用于兼容不一样的数据库, 防止 SQL 注入等 |
为了后面的演示, 建立了一个 Maven 项目 mybatis-dynamic, 建立了对应的数据库和表函数
DROP TABLE IF EXISTS `student`; CREATE TABLE `student` ( `student_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号', `name` varchar(20) DEFAULT NULL COMMENT '姓名', `phone` varchar(20) DEFAULT NULL COMMENT '电话', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `sex` tinyint(4) DEFAULT NULL COMMENT '性别', `locked` tinyint(4) DEFAULT NULL COMMENT '状态(0:正常,1:锁定)', `gmt_created` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '存入数据库的时间', `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改的时间', `delete` int(11) DEFAULT NULL, PRIMARY KEY (`student_id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='学生表';
对应的项目结构学习
if 标签是咱们最常使用的。 在查询、删除、更新的时候极可能会使用到。 必须结合 test 属性联合使用。
这是常见的一种现象, 咱们在进行按条件查询的时候, 可能会有多种状况。
根据输入的学生信息进行条件检索
接口函数
/** * 根据输入的学生信息进行条件检索 * 1. 当只输入用户名时, 使用用户名进行模糊检索; * 2. 当只输入邮箱时, 使用性别进行彻底匹配 * 3. 当用户名和性别都存在时, 用这两个条件进行查询匹配的用 * @param student * @return */ List<Student> selectByStudentSelective(Student student);
对应的动态 SQL
<select id="selectByStudentSelective" resultMap="BaseResultMap" parameterType="com.homejim.mybatis.entity.Student"> select <include refid="Base_Column_List" /> from student where 1=1 <if test="name != null and name !=''"> and name like concat('%', #{name}, '%') </if> <if test="sex != null"> and sex=#{sex} </if> </select>
在此 SQL 语句中, where 1=1 是多条件拼接时的小技巧, 后面的条件查询就能够都用 and 了。
同时, 咱们添加了 if 标签来处理动态 SQL
<if test="name != null and name !=''"> and name like concat('%', #{name}, '%') </if> <if test="sex != null"> and sex=#{sex} </if>
此 if 标签的 test 属性值是一个符合 OGNL 的表达式, 表达式能够是 true 或 false。 若是表达式返回的是数值, 则0为 false, 非 0 为 true;
@Test public void selectByStudent() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student search = new Student(); search.setName("明"); System.out.println("只有名字时的查询"); List<Student> studentsByName = studentMapper.selectByStudentSelective(search); for (int i = 0; i < studentsByName.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(studentsByName.get(i), ToStringStyle.MULTI_LINE_STYLE)); } search.setName(null); search.setSex((byte) 1); System.out.println("只有性别时的查询"); List<Student> studentsBySex = studentMapper.selectByStudentSelective(search); for (int i = 0; i < studentsBySex.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(studentsBySex.get(i), ToStringStyle.MULTI_LINE_STYLE)); } System.out.println("姓名和性别同时存在的查询"); search.setName("明"); List<Student> studentsByNameAndSex = studentMapper.selectByStudentSelective(search); for (int i = 0; i < studentsByNameAndSex.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(studentsByNameAndSex.get(i), ToStringStyle.MULTI_LINE_STYLE)); } sqlSession.commit(); sqlSession.close(); }
只有名字时的查询, 发送的语句和结果
查询的条件只发送了
where 1=1 and name like concat('%', ?, '%')
只有性别时的查询, 发送的语句和结果
查询的条件只发送了
where 1=1 and sex=?
姓名和性别同时存在的查询, 发送的语句和结果
查询条件
where 1=1 and name like concat('%', ?, '%') and sex=?
有时候咱们不但愿更新全部的字段, 只更新有变化的字段。
只更新有变化的字段, 空值不更新。
接口方法
/** * 更新非空属性 */ int updateByPrimaryKeySelective(Student record);
对应的 SQL
<update id="updateByPrimaryKeySelective" parameterType="com.homejim.mybatis.entity.Student"> update student <set> <if test="name != null"> `name` = #{name,jdbcType=VARCHAR}, </if> <if test="phone != null"> phone = #{phone,jdbcType=VARCHAR}, </if> <if test="email != null"> email = #{email,jdbcType=VARCHAR}, </if> <if test="sex != null"> sex = #{sex,jdbcType=TINYINT}, </if> <if test="locked != null"> locked = #{locked,jdbcType=TINYINT}, </if> <if test="gmtCreated != null"> gmt_created = #{gmtCreated,jdbcType=TIMESTAMP}, </if> <if test="gmtModified != null"> gmt_modified = #{gmtModified,jdbcType=TIMESTAMP}, </if> </set> where student_id = #{studentId,jdbcType=INTEGER}
@Test public void updateByStudentSelective() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); student.setStudentId(1); student.setName("明明"); student.setPhone("13838438888"); System.out.println(studentMapper.updateByPrimaryKeySelective(student)); sqlSession.commit(); sqlSession.close(); }
结果以下
咱们插入数据库中的一条记录, 不是每个字段都有值的, 而是动态变化的。 在这时候使用 if 标签, 可帮咱们解决这个问题。
只有非空属性才插入。
接口方法
/** * 非空字段才进行插入 */ int insertSelective(Student record);
对应的SQL
<insert id="insertSelective" parameterType="com.homejim.mybatis.entity.Student"> insert into student <trim prefix="(" suffix=")" suffixOverrides=","> <if test="studentId != null"> student_id, </if> <if test="name != null"> `name`, </if> <if test="phone != null"> phone, </if> <if test="email != null"> email, </if> <if test="sex != null"> sex, </if> <if test="locked != null"> locked, </if> <if test="gmtCreated != null"> gmt_created, </if> <if test="gmtModified != null"> gmt_modified, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="studentId != null"> #{studentId,jdbcType=INTEGER}, </if> <if test="name != null"> #{name,jdbcType=VARCHAR}, </if> <if test="phone != null"> #{phone,jdbcType=VARCHAR}, </if> <if test="email != null"> #{email,jdbcType=VARCHAR}, </if> <if test="sex != null"> #{sex,jdbcType=TINYINT}, </if> <if test="locked != null"> #{locked,jdbcType=TINYINT}, </if> <if test="gmtCreated != null"> #{gmtCreated,jdbcType=TIMESTAMP}, </if> <if test="gmtModified != null"> #{gmtModified,jdbcType=TIMESTAMP}, </if> </trim> </insert>
这个 SQL 你们应该很熟悉, 毕竟是自动生成的。
@Test public void insertByStudentSelective() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); student.setName("小飞机"); student.setPhone("13838438899"); student.setEmail("xiaofeiji@qq.com"); student.setLocked((byte) 0); System.out.println(studentMapper.insertSelective(student)); sqlSession.commit(); sqlSession.close(); }
对应的结果
SQL 中, 只有非空的字段才进行了插入。
choose when otherwise 标签能够帮咱们实现 if else 的逻辑。
一个 choose 标签至少有一个 when, 最多一个otherwise
下面是一个查询的例子。
假设 name 具备惟一性, 查询一个学生
接口方法
/** * - 当 studen_id 有值时, 使用 studen_id 进行查询; * - 当 studen_id 没有值时, 使用 name 进行查询; * - 不然返回空 */ Student selectByIdOrName(Student record);
对应的SQL
<select id="selectByIdOrName" resultMap="BaseResultMap" parameterType="com.homejim.mybatis.entity.Student"> select <include refid="Base_Column_List" /> from student where 1=1 <choose> <when test="studentId != null"> and student_id=#{studentId} </when> <when test="name != null and name != ''"> and name=#{name} </when> <otherwise> and 1=2 </otherwise> </choose> </select>
@Test public void selectByIdOrName() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); student.setName("小飞机"); student.setStudentId(1); Student studentById = studentMapper.selectByIdOrName(student); System.out.println("有 ID 则根据 ID 获取"); System.out.println(ToStringBuilder.reflectionToString(studentById, ToStringStyle.MULTI_LINE_STYLE)); student.setStudentId(null); Student studentByName = studentMapper.selectByIdOrName(student); System.out.println("没有 ID 则根据 name 获取"); System.out.println(ToStringBuilder.reflectionToString(studentByName, ToStringStyle.MULTI_LINE_STYLE)); student.setName(null); Student studentNull = studentMapper.selectByIdOrName(student); System.out.println("没有 ID 和 name, 返回 null"); Assert.assertNull(studentNull); sqlSession.commit(); sqlSession.close(); }
有 ID 则根据 ID 获取, 结果
没有 ID 则根据 name 获取
没有 ID 和 name, 返回 null
这三个其实解决的是相似的问题。 如咱们在写前面的[在 WHERE 条件中使用 if 标签] SQL 的时候, where 1=1 这个条件咱们是不但愿存在的。
根据输入的学生信息进行条件检索。
不使用 where 1=1。
很显然, 咱们要解决这几个问题
这时候, 咱们可使用 where 标签。
接口方法
/** * 根据输入的学生信息进行条件检索 * 1. 当只输入用户名时, 使用用户名进行模糊检索; * 2. 当只输入邮箱时, 使用性别进行彻底匹配 * 3. 当用户名和性别都存在时, 用这两个条件进行查询匹配的用 */ List<Student> selectByStudentSelectiveWhereTag(Student student);
对应的 SQL
<select id="selectByStudentSelectiveWhereTag" resultMap="BaseResultMap" parameterType="com.homejim.mybatis.entity.Student"> select <include refid="Base_Column_List" /> from student <where> <if test="name != null and name !=''"> and name like concat('%', #{name}, '%') </if> <if test="sex != null"> and sex=#{sex} </if> </where> </select>
@Test public void selectByStudentWhereTag() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student search = new Student(); search.setName("明"); System.out.println("只有名字时的查询"); List<Student> studentsByName = studentMapper.selectByStudentSelectiveWhereTag(search); for (int i = 0; i < studentsByName.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(studentsByName.get(i), ToStringStyle.MULTI_LINE_STYLE)); } search.setSex((byte) 1); System.out.println("姓名和性别同时存在的查询"); List<Student> studentsBySex = studentMapper.selectByStudentSelectiveWhereTag(search); for (int i = 0; i < studentsBySex.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(studentsBySex.get(i), ToStringStyle.MULTI_LINE_STYLE)); } System.out.println("姓名和性别都不存在时查询"); search.setName(null); search.setSex(null); List<Student> studentsByNameAndSex = studentMapper.selectByStudentSelectiveWhereTag(search); for (int i = 0; i < studentsByNameAndSex.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(studentsByNameAndSex.get(i), ToStringStyle.MULTI_LINE_STYLE)); } sqlSession.commit(); sqlSession.close(); }
只有名字时的查询, 有 where
姓名和性别同时存在的查询, 有 where
姓名和性别都不存在时查询, 此时, where 不会再出现了。
set 标签也相似, 在 [2.2 在 UPDATE 更新列中使用 if 标签] 中, 若是咱们的方法 updateByPrimaryKeySelective
没有使用
set 和 where 其实都是 trim 标签的一种类型, 该两种功能均可以使用 trim 标签进行实现。
如以上的 where 标签, 咱们也能够写成
<trim prefix="where" prefixOverrides="AND |OR"> </trim>
表示当 trim 中含有内容时, 添加 where, 且第一个为 and 或 or 时, 会将其去掉。 而若是没有内容, 则不添加 where。
相应的, set 标签能够以下表示
<trim prefix="SET" suffixOverrides=","> </trim>
表示当 trim 中含有内容时, 添加 set, 且最后的内容为 , 时, 会将其去掉。 而没有内容, 不添加 set
foreach 标签能够对数组, Map 或实现 Iterable 接口。
foreach 中有如下几个属性
其余的比较好理解, collection 中的值应该怎么设定呢?
跟接口方法中的参数相关。
1. 只有一个数组参数或集合参数
默认状况: 集合collection=list, 数组是collection=array
推荐: 使用 @Param 来指定参数的名称, 如咱们在参数前@Param("ids"), 则就填写 collection=ids
2. 多参数
多参数请使用 @Param 来指定, 不然SQL中会很不方便
3. 参数是Map
指定为 Map 中的对应的 Key 便可。 其实上面的 @Param 最后也是转化为 Map 的。
4. 参数是对象
使用属性.属性便可。
在 where条件中使用, 如按id集合查询, 按id集合删除等。
咱们但愿查询用户 id 集合中的全部用户信息。
函数接口
/** * 获取 id 集合中的用户信息 * @param ids * @return */ List<Student> selectByStudentIdList(List<Integer> ids);
对应 SQL
<select id="selectByStudentIdList" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from student where student_id in <foreach collection="list" item="id" open="(" close=")" separator="," index="i"> #{id} </foreach> </select>
@Test public void selectByStudentIdList() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); List<Integer> ids = new LinkedList<>(); ids.add(1); ids.add(3); List<Student> students = studentMapper.selectByStudentIdList(ids); for (int i = 0; i < students.size(); i++) { System.out.println(ToStringBuilder.reflectionToString(students.get(i), ToStringStyle.MULTI_LINE_STYLE)); } sqlSession.commit(); sqlSession.close(); }
结果
能够经过foreach来实现批量插入。
接口方法
/** * 批量插入学生 */ int insertList(List<Student> students);
对应的SQL
<insert id="insertList"> insert into student(name, phone, email, sex, locked) values <foreach collection="list" item="student" separator=","> ( #{student.name}, #{student.phone},#{student.email}, #{student.sex},#{student.locked} ) </foreach> </insert>
@Test public void insertList() { SqlSession sqlSession = null; sqlSession = sqlSessionFactory.openSession(); StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = new LinkedList<>(); Student stu1 = new Student(); stu1.setName("批量01"); stu1.setPhone("13888888881"); stu1.setLocked((byte) 0); stu1.setEmail("13888888881@138.com"); stu1.setSex((byte) 1); students.add(stu1); Student stu2 = new Student(); stu2.setName("批量02"); stu2.setPhone("13888888882"); stu2.setLocked((byte) 0); stu2.setEmail("13888888882@138.com"); stu2.setSex((byte) 0); students.add(stu2); System.out.println(studentMapper.insertList(students)); sqlSession.commit(); sqlSession.close(); }
结果
bind 标签是经过 OGNL 表达式去定义一个上下文的变量, 这样方便咱们使用。
如在 selectByStudentSelective
方法中, 有以下
<if test="name != null and name !=''"> and name like concat('%', #{name}, '%') </if>
在 MySQL 中, 该函数支持多参数, 但在 Oracle 中只支持两个参数。 那么咱们可使用 bind 来让该 SQL 达到支持两个数据库的做用
<if test="name != null and name !=''"> <bind name="nameLike" value="'%'+name+'%'"/> and name like #{nameLike} </if>
更改后的查询结果以下