映射文件
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
Kobe Brayent
24
laker
2
Lebron James
23
laker
3
Tim Duncan
21
spurs
4
leonard
2
raptors
5
Stephen Curry
30
warriors
6
Klay Thompson
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 ) ;
int resultOfFakeNameStatement = searchByName ( fakeName, false ) ;
int resultOfRightNamePs = searchByName ( nameRight, true ) ;
int resultOfFakeNamePs = searchByName ( fakeName, true ) ;
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) {
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 {
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>
@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;
}
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" ) ;
List< People> peoples1 = peopleMapper. findByNameAndAddress1 ( map) ;
for ( People p : peoples1) {
System. out. println ( p) ;
}
PeopleQueryBean peopleQueryBean = new PeopleQueryBean ( ) ;
peopleQueryBean. setName ( "Duncan" ) ;
peopleQueryBean. setAddress ( "beijing" ) ;
List< People> peoples2 = peopleMapper. findByNameAndAddress2 ( peopleQueryBean) ;
for ( People p : peoples2) {
System. out. println ( p) ;
}
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注入,多参数需要考虑可读性
出参方面需要考虑可维护性和复用性
另外还给出了主键回写的方法