作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
学习必须往深处挖,挖的越深,基础越扎实!
阶段1、深入多线程
阶段2、深入多线程设计模式
阶段3、深入juc源码解析
码哥源码部分
码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】
码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】
码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】
码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】
打脸系列【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;)
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注入,多参数需要考虑可读性
- 出参方面需要考虑可维护性和复用性
- 另外还给出了主键回写的方法