MyBatis入门:CRUD、参数处理与防 SQL 注入

一、代理 Dao 方式的 CRUD 操作

MyBatis 的代理 Dao 方式(Mapper 代理)是最常用的开发方式,无需手动实现 Dao 接口,框架会自动创建接口的代理对象,大幅简化开发流程。

1. 定义 User 实体类

复制代码
package com.qcby.entity;

import java.util.Date;

public class User {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    // 省略getter、setter和toString方法
}

2. UserDao接口代码

定义数据库操作的抽象方法,方法名需与 Mapper.xml 中的 SQL 标签 id 一致:

复制代码
import com.qcby.entity.User;

import java.util.List;

public interface UserDao {
    /**
     * 全部查询
     * @return
     */
    public List<User> findAll();

    /**
     * 根据id进行查询
     * @param id
     */
    public User findById(int id);

    /**
     *  插入数据
     * @param user
     * @return
     */
    public int insert(User user);

    /**
     * 删除
     * @param id
     * @return
     */
    public int delete(int id);

    /**
     * 修改
     * @param user
     * @return
     */
    public int update(User user);

    /**
     * 获取插入的id
     * @param user
     * @return
     */
    public int insertGetId(User user);

    /**
     * 通过姓名模糊查询
     * @return
     */
    public List<User> likeByName(String username);
}

3. mapper层

在 resources 目录下创建与接口路径对应的 Mapper.xml,实现 SQL 映射:

复制代码
<?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="com.qcby.Dao.UserDao">
    <select id="findAll" resultType="com.qcby.entity.User">
        select * from user
    </select>

    <select id="findById" resultType="com.qcby.entity.User" parameterType="java.lang.Integer">
        select * from user where id = #{id}
    </select>

    <insert id="insert" parameterType="com.qcby.entity.User">
        insert into user(username,birthday,sex,address)
        values(#{username},#{birthday},#{sex},#{address})
    </insert>

    <delete id="delete" parameterType="java.lang.Integer">
        delete from user where id = #{id}
    </delete>

    <update id="update" parameterType="com.qcby.entity.User">
        update user set username = #{username},birthday = #{birthday},
                sex = #{sex},address = #{address} where id = #{id}
    </update>

    <!--返回主键 :我们的主键需要设置自动递增 -->
    <insert id="insertGetId" parameterType="com.qcby.entity.User">
        <selectKey keyProperty="id" resultType="int" order="AFTER">
            SELECT LAST_INSERT_ID()
        </selectKey>
        insert into user(username,birthday,sex,address)
        values(#{username},#{birthday},#{sex},#{address})
    </insert>

    <!--${}:拼接 , #{}预编译 -->
    <select id="likeByName" resultType="com.qcby.entity.User" parameterType="java.lang.String">
        select * from user where username like '%${value}%';
    </select>

</mapper>

4. 测试类

使用 JUnit 测试 CRUD 操作,封装 SqlSession 的创建和销毁:

复制代码
import com.qcby.Dao.UserDao;
import com.qcby.entity.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class UserTest {

    private InputStream in = null;
    private SqlSession session = null;
    private UserDao mapper = null;

    @Before
    public void init() throws IOException {
        //加载主配置文件,目的是为了构建SqlSessionFactory对象
        in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //创建SqlSessionFactory对象
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        //通过SqlSessionFactory工厂对象创建SqlSesssion对象
        session = factory.openSession();
        //通过Session创建UserDao接口代理对象
        mapper = session.getMapper(UserDao.class);
    }

    @After
    public void destory() throws IOException {
        //释放资源
        session.close();
        in.close();
    }

    /**
     * 测试查询所有的方法
     */
    @Test
    public void findAll() throws IOException {
        List<User> users = mapper.findAll();
        for (User user:users) {
            System.out.println(user.toString());
        }
    }

    @Test
    public void findById() {
        User user = mapper.findById(2);
        System.out.println(user.toString());
    }


    @Test
    public void insert(){
        User user = new User();
        user.setAddress("保定");
        user.setBirthday(new Date());
        user.setSex("女");
        user.setUsername("张三");


        int code = mapper.insert(user);
        session.commit();  
        System.out.println(code);
    }

    @Test
    public void delete(){
        int code = mapper.delete(2);
        session.commit();
        System.out.println(code);
    }

    @Test
    public void update(){
        User user = new User();
        user.setId(1);
        user.setAddress("保定");
        user.setBirthday(new Date());
        user.setSex("女");
        user.setUsername("shsh");

        int code = mapper.update(user);
        session.commit();
        System.out.println(code);
    }

    @Test
    public void insertGetId(){
        User user = new User();
        user.setAddress("保定");
        user.setBirthday(new Date());
        user.setSex("女");
        user.setUsername("张三");

        mapper.insertGetId(user);
        session.commit();
        System.out.println(user.getId());
    }

    @Test
    public void likeByName(){
        List<User> users = mapper.likeByName("熊");
        for (User user: users) {
            System.out.println(user);
        }
    }
}

二、MyBatis 参数详解

1. parameterType(参数类型)

用于指定传入 SQL 的参数类型,MyBatis 会自动进行类型转换:

简单数据类型 :如intStringdouble等,支持简写(如java.lang.Integer可简写为int/Integer);

POJO 类型 :如User实体类,需指定全类名(可通过配置别名简化);

默认省略 :MyBatis 可自动推断参数类型,实际开发中可省略parameterType

2. resultType(结果类型)

用于指定 SQL 查询结果的映射类型:

简单数据类型 :如返回记录数int、返回用户名String

POJO 类型 :如返回User对象,要求 POJO 属性名与数据库列名一致;

集合类型 :如List<User>,只需指定泛型类型com.qcby.entity.User

3. resultMap(结果映射)

resultType可以指定pojo将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功。

如果sql查询字段名和pojo的属性名不一致,可以通过resultMap将字段名和属性名作一个对应关系 ,resultMap实质上还需要将查询结果映射到pojo对象中。

resultMap可以实现将查询结果映射为复杂类型的pojo,比如在查询结果映射对象中包括pojo和list实现一对一查询和一对多查询。

复制代码
xml
<!-- 定义resultMap -->
<resultMap id="ResultMap" type="com.qcby.entity.User">
    <!-- id:主键映射 -->
    <id column="id" property="id" jdbcType="INTEGER"/>
    <!-- result:普通字段映射 -->
    <result column="username" property="username" jdbcType="VARCHAR"/>
    <result column="birthday" property="birthday" jdbcType="DATE"/>
    <result column="sex" property="sex" jdbcType="VARCHAR"/>
    <result column="address" property="address" jdbcType="VARCHAR"/>
</resultMap>

<!-- 使用resultMap查询 -->
<select id="findAlResultMap" resultMap="ResultMap">
    select * from user
</select>

测试方法:

复制代码
@Test
public void findAlResultMap() {
    List<User> users = mapper.findAlResultMap();
    for (User user : users) {
        System.out.println(user);
    }
}

三、MyBatis 获取参数值的两种方式(#{} 与 ${})

Mybatis获取参数值得两种方式:${}和#{}

${}的本质是字符串拼接,#{}的本质是占位符赋值

${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号;

#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段赋值时,可以自动添加单引号;

1.配置sql输出日志

注意:要在配置文件的第一行进行配置

复制代码
<!-- settings:控制mybatis的全局行为-->
<settings>
    <!--设置mybatis输出日志-->
    <!--logImpl:表示对日志的控制-->
    <!--STDOUT_LOGGING:将日志输出到控制台上-->
    <setting name="logImpl" value="STDOUT_LOGGING" />
</settings>

2.看#{}和${}的输出现象

首先先来看#{}的输出

复制代码
<!--通过#进行查询-->
<select id="select"  parameterType="java.lang.String" resultType="com.qcby.entity.User">
    select * from user where username = #{username}
</select>

我们可以看到当前执行的sql语句的日志执行结果最后的条件值是用 ?代替

其次来看${}的输出

复制代码
<!--通过$进行查询-->
<select id="selectBy$" parameterType="java.lang.String" resultType="com.qcby.entity.User">
    select * from user where username = ${value}
</select>

我们发现和上边用 #号不同,这次输出的sql 直接在上边拼接好了我们要查询的值,而且还出现了错误

我们将这条sql语句放入到mysql当中去执行,发现原因是我们sql语句在值上没有带单引号

所有我们需要重新编辑sql

3.#{}的预编译

复制代码
#{}是占位符:动态解析 -> 预编译 -> 执行
${}是拼接符:动态解析 -> 编译 -> 执行

预编译可以类比java类的编译,java类被编译成class文件,载入虚拟机,载入虚拟机的字节码文件可以先被编译成机器吗,那么在执行某行代码的时候就可以直接执行编译后的机器码,而不用从字节码开始编译再执行,那么执行效率就高了。这也是为啥热机状态比冷机状态可以抗更多负载的原因。

sql的预编译也是一样的道理,在执行前就编译好,等执行时直接取编译结果去执行。省去编译时间。sql预编译后会在参数位置用占位符表示

预编译:数据库驱动在发送sql和参数到DBMS之前,先对sql语句进行编译处理,之后DBMS则可以直接对sql进行处理,不需要再次编译,提高了性能。这一点mybatis 默认情况下,将对所有的 sql 进行预编译处理。

预编译可以将多个操作步骤合并成一个步骤,一般而言,越复杂的sql,编译程度也会复杂,难度大,耗时,费性能,而预编译可以合并这些操作,

预编译之后DBMS可以省去编译直接运行sql预编译语句可以重复利用。把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。

4.sql注入

使用${}做字符串拼接的方式并不安全,可能存在sql注入问题

比如我们要做如下查询

复制代码
select * from user where username = ${value};

但是如果我们的传参是: "张三 or username = 李四"

这种通过传参就能改变SQL语句原本规则的操作就是SQL注入,这个在实际生产中当然是危险的,攻击者可以把SQL命令插入到Web表单的输入域或页面请求的查询字符串中,欺骗服务器执行恶意的SQL命令。

以上的情况就属于sql注入攻击。

5.什么时候使用${}

:可以替换表名或者列名,你能确定数据是安全的,可以使用

6.如何选择使用 #{} 和 ${}

复制代码
1.能用 #{} 的地方就用 #{},尽量少用 ${}
2.表名作参数,或者order by 排序时用 ${}
3.传参时参数使用@Param("")注解,@Param注解的作用是给参数命名,参数命名后就能根据名字得到参数值(相当于又加了一层密),
正确的将参数传入sql语句中(一般通过#{}的方式,${}会有sql注入的问题)

7.为什么#{}可以预防sql注入

mybatis的#{}之所以能够预防sql注入是因为底层使用了PrepardStatment类的setString()方法来设置参数, 此方法会获取参数传递过来的每个字符,然后进行循环对比,如果发现有敏感字符(如:单引号、双引号等), 则会在上边加一个'/'代表转义此符号,让其变成一个普通的字符串,不参与SQL语句的生成,达到预防sql注入的效果

相关推荐
架构师刘伟7 分钟前
MyBatis-Dynamic 进阶:无需实体类的全动态数据建模
mybatis
SimonKing10 分钟前
分享一款可以管理本地端口的IDEA插件:Port Manager
java·后端·程序员
索荣荣15 分钟前
Maven配置文件(pom.xml)终极指南
java·开发语言
代码栈上的思考29 分钟前
SpringBoot 拦截器
java·spring boot·spring
送秋三十五33 分钟前
一次大文件处理性能优化实录————Java 优化过程
java·开发语言·性能优化
龙山云仓35 分钟前
MES系统超融合架构
大数据·数据库·人工智能·sql·机器学习·架构·全文检索
雨中飘荡的记忆35 分钟前
千万级数据秒级对账!银行日终批处理对账系统从理论到实战
java
jbtianci41 分钟前
Spring Boot管理用户数据
java·spring boot·后端
Sylvia-girl44 分钟前
线程池~~
java·开发语言
魔力军1 小时前
Rust学习Day3: 3个小demo实现
java·学习·rust