映射文件
- 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] ,回复【面试题】 即可免费领取。