Mybatis 映射文件

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

映射文件

  • 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;)
idplayNameplayNoteam
1KobeBrayent24laker
2LebronJames23laker
3TimDuncan21spurs
4leonard2raptors
5StephenCurry30warriors
6KlayThompson11warriors
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);
    
    }
  • 数据库记录
idnameageaddressedu
1Duncan12BeijingDoctor
2Parker20tianjingBachelor
3Duncan21tianjingBachelor
  • 测试代码:
    @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注入,多参数需要考虑可读性
  • 出参方面需要考虑可维护性和复用性
  • 另外还给出了主键回写的方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值