2023-06-06  阅读(32)
原文作者:惑边 原文地址:https://blog.csdn.net/my_momo_csdn

映射文件

  • mybatis的查询sql和参数结果集映射的配置都是在映射文件中配置。相关的配置很多,这里我们从出参和入参2个角度先写一部分

一、入参

1.1 #和$

  • 使用JDBC我们都知道,jdbc传参时,一种是使用Statement,还有一种是使用PreparedStatement。前者有SQL注入的潜在隐患,在MyBatis中,传递单个参数有两种方式,一种是使用#,还有一种是使用KaTeX parse error: Expected 'EOF', got '#' at position 4: ,其中#̲对应了Jdbc种的Prepar…则对应了Jdbc种的Statement,因此在MyBatis种,推荐使用#。

1.1.1 案例

  • 我的数据库存在如下记录:(select * from tb_player;)
id playName playNo team
1 KobeBrayent 24 laker
2 LebronJames 23 laker
3 TimDuncan 21 spurs
4 leonard 2 raptors
5 StephenCurry 30 warriors
6 KlayThompson 11 warriors

1.1.2 测试代码

    public class Test04 {
    
        //数据库相关信息
        static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
        static final String DB_URL = "jdbc:mysql://192.168.11.27:3306/demo?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true";
        static final String USER = "root";
        static final String PASS = "introcks1234";
    
        static Statement stmt = null;
        static PreparedStatement preparedStatement = null;
        static Connection conn = null;
    
        @Test
        public void test() {
            String nameRight = "Lebron James"; //模拟用户输入正确的名称时候的查询
            String fakeName = "Lebron Jamesxxx' or '1 = 1 "; //模拟用户输入错误的名称时候的查询
            int resultOfRightNameStatement = searchByName(nameRight, false); //使用Statement查询正确的名字
            int resultOfFakeNameStatement = searchByName(fakeName, false); //使用Statement查询错误的名字
            int resultOfRightNamePs = searchByName(nameRight, true); //使用PreparedStatement查询正确的名字
            int resultOfFakeNamePs = searchByName(fakeName, true); //使用PreparedStatement查询错误的名字
            //打印结果,通过观察查询到多少记录,来判断是否有SQL注入
            System.out.println("使用Statement查询正确的sql, 查询总数为:" + resultOfRightNameStatement);
            System.out.println("使用Statement查询错误的sql,查询总数为:" + resultOfFakeNameStatement);
            System.out.println("使用PreparedStatement查询正确的sql, 查询总数为:" + resultOfRightNamePs);
            System.out.println("使用PreparedStatement查询错误的sql,查询总数为:" + resultOfFakeNamePs);
        }
    
        public static int searchByName(String username, boolean safe) {
            int count = 0;
            try {
                Class.forName("com.mysql.jdbc.Driver");
                Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
                String sql;
                ResultSet rs = null;
                if (safe) {
                    //安全的查询,则使用PreparedStatement
                    sql = "SELECT * FROM tb_player where playName= ?";
                    PreparedStatement preparedStatement = conn.prepareStatement(sql);
                    preparedStatement.setString(1, username);
                    System.out.println("打印sql:" + preparedStatement.toString());
                    rs = preparedStatement.executeQuery();
                } else {
                    //不安全的查询,使用Statement
                    sql = "SELECT * FROM tb_player where playName='" + username + "'";
                    Statement statement = conn.createStatement();
                    System.out.println("打印sql:" + sql);
                    rs = statement.executeQuery(sql);
                }
                if (rs != null) {
                    while (rs.next()) {
                        //计算查询到的结果总数
                        count++;
                    }
                }
                return count;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 关闭资源
                try {
                    if (stmt != null)
                        stmt.close();
                } catch (SQLException se2) {
                } 
                try {
                    if (conn != null)
                        conn.close();
                } catch (SQLException se) {
                    se.printStackTrace();
                }
            }
            return -1;
        }
    }
    
    
    结果:
    打印sql:SELECT * FROM tb_player where playName='Lebron James'
    打印sql:SELECT * FROM tb_player where playName='Lebron Jamesxxx' or '1 = 1 '
    打印sql:com.mysql.jdbc.JDBC4PreparedStatement@27abe2cd: SELECT * FROM tb_player where playName= 'Lebron James'
    打印sql:com.mysql.jdbc.JDBC4PreparedStatement@60215eee: SELECT * FROM tb_player where playName= 'Lebron Jamesxxx\' or \'1 = 1 '
    使用Statement查询正确的sql, 查询总数为:1
    使用Statement查询错误的sql,查询总数为:6
    使用PreparedStatement查询正确的sql, 查询总数为:1
    使用PreparedStatement查询错误的sql,查询总数为:0

1.1.3 结论

  • 我们看到,当使用Statement的时候,我们通过构造非法参数fakeName = "Lebron Jamesxxx’ or '1 = 1 "达到了SQL注入,因为我们查到了全部的记录
  • 使用PreparedStatement,输入错误的sql我们是查询不到任何记录的
  • 我们通过打印出来的预编译语句,我们也可以看到为什么PreparedStatement可以防止SQL注入,因为它把输入中的单引号加了转义字符,因此底层查询的
    时候,我们输入里面的’1=1’变成了条件的一部分,自然查不到,但是对于Statement,他却把’1=1’当做了一个逻辑或的条件,导致总条件永远为true,因此查到了全部的记录,这也是PreparedStatement底层防止sql注入的原理,毫无疑问,我们推荐使用#方式。

1.2 多个参数

  • 当有多个参数的时候,传参方式有map(不建议使用)/注解(小于5个时使用)/javaBean(大于5个时使用)。

1.2.1 Map

  • 不直观,不建议使用

1.2.2 javaBean

  • 参数较多时使用

1.2.3 注解

  • 参数较少时使用

1.2.4 代码

  • 映射文件:
    <?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.intellif.mozping.dao.PeopleMapper">
    
    
        <insert id="addPeople" parameterType="com.intellif.mozping.entity.People">
    		insert into tb_people(id,name,age,address,edu)values(#{id},#{name},#{age},#{address},#{edu})
    	</insert>
    
    
        <select id="findByNameAndAddress1" resultType="com.intellif.mozping.entity.People" parameterType="map">
            select * from tb_people p where p.name = #{name} and p.address = #{address}
        </select>
    
        <select id="findByNameAndAddress2" resultType="com.intellif.mozping.entity.People">
           select * from tb_people p where p.name = #{name} and p.address = #{address}
        </select>
    
    
        <select id="findByNameAndAddress3" resultType="com.intellif.mozping.entity.People"
                parameterType="com.intellif.mozping.querybean.PeopleQueryBean">
           select * from tb_people p where p.name = #{name} and p.address = #{address}
        </select>
        
    </mapper>
  • Java代码
    
    //JavaQueryBean
    @Data
    public class PlayerQueryBean {
    
        String team;
        Float height;
    }
    
    //实体类:
    @Data
    public class People {
    
        private int id;
        private String name;
        private int age;
        private String address;
        private String edu;//学士(Bachelor) 硕士(master)  博士(Doctor)
    
    }
    
    
    //Java接口:
    public interface PeopleMapper {
    
        int addPeople(People people);
    
        List<People> findByNameAndAddress1(Map<String, Object> param);
    
    
        List<People> findByNameAndAddress2(PeopleQueryBean peopleQueryBean);
    
        List<People> findByNameAndAddress3(@Param("name") String name, @Param("address") String address);
    
    }
  • 数据库记录
id name age address edu
1 Duncan 12 Beijing Doctor
2 Parker 20 tianjing Bachelor
3 Duncan 21 tianjing Bachelor
  • 测试代码:
    @Test
        public void query() {
            SqlSession sqlSession = SqlSessionFactoryUtil.getSqlSessionFactoryInstaceByConfig(CONFIG_FILE_PATH).openSession();
            PeopleMapper peopleMapper   = sqlSession.getMapper(PeopleMapper.class);
            HashMap map = new HashMap();
            map.put("name","Duncan");
            map.put("address","beijing");
            //方式1,map传参
            List<People> peoples1 = peopleMapper.findByNameAndAddress1(map);
            for (People p : peoples1) {
                System.out.println(p);
            }
    
            PeopleQueryBean peopleQueryBean = new PeopleQueryBean();
            peopleQueryBean.setName("Duncan");
            peopleQueryBean.setAddress("beijing");
            //方式2,javaBean传参
            List<People> peoples2 = peopleMapper.findByNameAndAddress2(peopleQueryBean);
            for (People p : peoples2) {
                System.out.println(p);
            }
    
            //方式3,注解传参
            List<People> peoples3 = peopleMapper.findByNameAndAddress3("Duncan","beijing");
            for (People p : peoples3) {
                System.out.println(p);
            }
        }
        
    打印:
    19:50:20.653 [main] DEBUG c.i.m.d.P.findByNameAndAddress1 - ==> Parameters: Duncan(String), beijing(String)
    19:50:20.668 [main] DEBUG c.i.m.d.P.findByNameAndAddress1 - <==      Total: 1
    People(id=1, name=Duncan, age=12, address=beijing, edu=Doctor)
    19:50:20.669 [main] DEBUG c.i.m.d.P.findByNameAndAddress2 - ==>  Preparing: select * from tb_people p where p.name = ? and p.address = ? 
    19:50:20.669 [main] DEBUG c.i.m.d.P.findByNameAndAddress2 - ==> Parameters: Duncan(String), beijing(String)
    19:50:20.671 [main] DEBUG c.i.m.d.P.findByNameAndAddress2 - <==      Total: 1
    People(id=1, name=Duncan, age=12, address=beijing, edu=Doctor)
    19:50:20.672 [main] DEBUG c.i.m.d.P.findByNameAndAddress3 - ==>  Preparing: select * from tb_people p where p.name = ? and p.address = ? 
    19:50:20.672 [main] DEBUG c.i.m.d.P.findByNameAndAddress3 - ==> Parameters: Duncan(String), tianjing(String)
    19:50:20.674 [main] DEBUG c.i.m.d.P.findByNameAndAddress3 - <==      Total: 1
    People(id=3, name=Duncan, age=21, address=tianjing, edu=Bachelor)

二、出参

2.1 ResultType

  • 对于简单数据类型,例如查询总记录数、查询某一个用户名这一类返回值是一个基本数据类型的,直接写Java中的基本数据类型即可。
        <select id="countAll" resultType="int" >
           SELECT count(1) FROM tb_people
        </select>
  • 如果返回的是一个对象或者集合,并且SQL中的字段名称和对象中的属性是一一对应的,那么resultType也可以直接写一个对象(当然也可以写别名,这里也可以关闭自动映射开启下划线转驼峰),示例可以参照之前演示多参数传递的写法。

2.2 自动映射和失效

  • 使用自动映射的前提是:
        使用resultType
        Sql列名和JavaBean属性完全一致
  • 在使用resultType的时候,会做自动映射,自动映射默认是开启的,如果sql的命名和java字段一样,那就没有任何问题,前面的例子都是这样的情况。
  • 如果二者不一样,则转换会失败,查询到的就是null,所以这样的方式看起来简单但是约束比较重,必须要保证两边的字段一样,实际上不推荐使用,在阿里巴巴的
    java开发规范中都禁止使用,因此有了下面2种较为灵活的解决方法。

2.2.1 别名

  • 查询时,可以给查询结果取别名。在sql中给数据库的字段去一个别名,保证别名和javaBean中字段一样,因此即使数据库字段修改了,javaBean属性名也不需要修改,
    只要在映射文件中维护即可。

2.2.2 转换

  • 如果javaBean是按照驼峰命名规范,数据库是按照下划线的规范命名,可以关闭自动映射并开启下划线转驼峰,其实这样的方式也是一种自动映射,只是映射规则稍微修改
    了一点点,和之前的字动映射有一样的缺点,维护的时候2边要保持一致。按照良好的java编程规范,最好定义resultMap,这样即使java字段变化,也可以和数据库字段变化
    解耦,便于维护和扩展。

2.2.3 ResultMap

  • 查询的结果集如果是比较复杂的结果集,比如多表的关联,或者javaBean和sql中字段名不一样,那么可以自定义resultMap,其实是自定义一个转换规则,而且可以做复用,
    在很多查询中都可以使用,这也是最为推荐的方法,如下:
        映射文件:
        <select id="findAll" resultMap="BaseResultMap" >
           SELECT * FROM tb_people
        </select>
    
        <resultMap id="BaseResultMap" type="com.intellif.mozping.entity.People">
            <id column="id" property="id" />
            <result column="nameDb" property="name" />
            <result column="age" property="age" />
            <result column="address" property="address" />
            <result column="edu" property="edu" />
        </resultMap> 
    	
  • 如上所示,我临时将数据库的name字段修改为nameDb字段,只需要在BaseResultMap修改即可,不需要修改java对象的代码,测试代码如下:
    	@Test
        public void queryGetWithResultMap() {
            SqlSession sqlSession = SqlSessionFactoryUtil.getSqlSessionFactoryInstaceByConfig(CONFIG_FILE_PATH).openSession();
            PeopleMapper peopleMapper = sqlSession.getMapper(PeopleMapper.class);
    
            List<People> peoples = peopleMapper.findAll( );
            for (People p : peoples) {
                System.out.println(p);
            }
        }

三、主键回写

  • 般情况下,主键有两种生成方式:主键自增长或者自定义主键(一般可以使用UUID),如果是自增长,Java可能需要知道数据添加成功后的主键。 在MyBatis中,可以通过主键回填来解决这个问题(推荐)。如果是第二种,主键一般是在Java代码中生成,然后传入数据库执行。

3.1 useGeneratedKeys

  • 将数据库生成的主键写回到javabean对应的属性中
        <!--传进来的对象不包含主键,数据库生成主键之后,将主键返回给java代码-->
        <insert id="addPeopleWithOutPrimaryKey" parameterType="com.intellif.mozping.entity.People" useGeneratedKeys="true" keyProperty="id">
    		insert into tb_people(name,age,address,edu)values( #{name},#{age},#{address},#{edu})
    	</insert>
    	
    	测试代码:
    	public class Test06 {
    
        private static final String CONFIG_FILE_PATH = "mybatis/mybatis-config-05.xml";
    
        @Test
        public void add() {
            SqlSession sqlSession = SqlSessionFactoryUtil.getSqlSessionFactoryInstaceByConfig(CONFIG_FILE_PATH).openSession();
    
            People people = new People();
            people.setAge(54);
            people.setName("Ma yun");
            people.setAddress("hangzhou");
            people.setEdu("unKnow");
    
            PeopleMapper peopleMapper = sqlSession.getMapper(PeopleMapper.class);
            int rowAffected = peopleMapper.addPeopleWithOutPrimaryKey(people);
            System.out.println("The rows be affected :" + rowAffected);
            System.out.println("The primary key is:" + people.getId());
            //显示提交事务
            sqlSession.commit();
            sqlSession.close();
        }
    }
    
    打印:
    The rows be affected :1
    The primary key is:8

3.2 selectKey

  • 主键回写也可以使用的写法,经测试和上面的方法效果是一样的,配置如下:
        <insert id="addPeopleWithOutPrimaryKey1" parameterType="com.intellif.mozping.entity.People">
            <selectKey keyProperty="id" resultType="int">
                select LAST_INSERT_ID()
            </selectKey>
    	    insert into tb_people(name,age,address,edu)values( #{name},#{age},#{address},#{edu})
    	</insert>

四、小结

  • 本文主要从参数的角度分析了映射文件部分的内容,分为入参和出参2个方面
  • 入参方面包括单个参数和多参数,单个参数需要防止sql注入,多参数需要考虑可读性
  • 出参方面需要考虑可维护性和复用性
  • 另外还给出了主键回写的方法

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文