使用Mybatis实现动态SQL
摘要
在知道Mybatis(原名ibatis)怎么用之前,对于在代码中连接数据库,我都是用JDBC连接的,例如这样:
Connection connection = DriverManager.getConnection(jdbcUrl, user, password);
String sqlCommend = "select goods.id,goods.name,sum(`order`.goods_num*goods_price) as gmv from `order` \n" +
"join goods\n" +
"on goods.id = `order`.goods_id\n" +
"group by goods_id \n" +
"order by gmv desc\n" +
"\n";
try (PreparedStatement pS = databaseConnection.prepareStatement(sqlCommend)) {
ResultSet resultSet = pS.executeQuery();
return getGoodsAndGmv(resultSet);
}
这样看起来也没多麻烦,但是谁也不想自己的函数中出现这么一段不堪的语句。所以,Mybatis为我们提供了更加方便的执行数据库操作的方法。
Mybatis本身也是一种ORM(Object Relationship Mapping)框架,既对象关系映射,说白了就是实现数据库到Java对象的一个映射,就是我们与数据库打交道的一个中间层。
Mybatis的官方文档写的非常详细,你碰到的问题基本上都可以通过官方文档解决。
1.从配置开始
跟着官方文档一步步走,首先需要从外部引入Mybatis的jar包,使用Maven的话则需要引入Maven配置(Spring中是另外一个依赖),接下来就是配置资源文件了。首先明白,在Java中把非代码的内容都称为资源,包括图片、视频、数据库等,资源目录与代码目录结构类似。
接着在mybatis包下新建config.xml文件,根据文档提示将xml文件的内容扔进去:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
注意:
<!--数据库的驱动类型-->
<property name="driver" value="org.h2.Driver"/>
<!--数据库的连接串-->
<property name="url" value="jdbc:h2:file:H:/githubitem/SinaCrawler/sina-crawler/SinaCrawler"/>
<!--用户名-->
<property name="username" value="root"/>
<!--密码-->
<property name="password" value="password"/>
以及
<mappers>
<mapper resource="XXX/XXX/XXX/XXX.xml"/>
<mapper resource="XXX/XXX/XXX/XXX$XXX"/>
</mappers>
是需要根据自己的实际情况修改的。这里用的是自己的一个H2数据库为例子,
数据库中的内容为:
用户表:
+----+----------+------+----------+
| ID | NAME | TEL | ADDRESS |
+----+----------+------+----------+
| 1 | zhangsan | tel1 | beijing |
+----+----------+------+----------+
| 2 | lisi | tel2 | shanghai |
+----+----------+------+----------+
| 3 | wangwu | tel3 | shanghai |
+----+----------+------+----------+
| 4 | zhangsan | tel4 | shenzhen |
+----+----------+------+----------+
商品表:
+----+--------+-------+
| ID | NAME | PRICE |
+----+--------+-------+
| 1 | goods1 | 10 |
+----+--------+-------+
| 2 | goods2 | 20 |
+----+--------+-------+
| 3 | goods3 | 30 |
+----+--------+-------+
| 4 | goods4 | 40 |
+----+--------+-------+
| 5 | goods5 | 50 |
+----+--------+-------+
订单表:
+------------+-----------------+------------------+---------------------+-------------------------------+
| ID(订单ID) | USER_ID(用户ID) | GOODS_ID(商品ID) | GOODS_NUM(商品数量) | GOODS_PRICE(下单时的商品单价) |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 1 | 1 | 1 | 5 | 10 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 2 | 2 | 1 | 1 | 10 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 3 | 2 | 1 | 2 | 10 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 4 | 4 | 2 | 4 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 5 | 4 | 2 | 100 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 6 | 4 | 3 | 1 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 7 | 5 | 4 | 1 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 8 | 5 | 6 | 1 | 60 |
+------------+-----------------+------------------+---------------------+-------------------------------+
接下来先用一个简单的例子来讲解整个过程:
获取所有的用户信息:
写一个接口:
public interface UserMapper{
@Select("select * from user")
List<User> getUsers();
}
实现这个接口:
public static void main(String[] args) throws IOException {
String resource = "db/mybatis/config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获得SqlSession实例
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
//生成代理类
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.getUsers();
for (User user:users
) {
System.out.println(user);
}
}
config.xml配置文件中添加这个接口的绝对路径
<mappers>
<!-- $区分内部类的分隔符 -->
<mapper class="com.github.hcsp.sql.Sql$UserMapper"/>
</mappers>
点击运行就可以看到结果了:
User{id=1, name='zhangsan', tel='tel1', address='beijing'}
User{id=2, name='lisi', tel='tel2', address='shanghai'}
User{id=3, name='wangwu', tel='tel3', address='shanghai'}
User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}
Process finished with exit code 0
问题来了,我们并没有实现这个接口,那么结果是怎么出来的呢?看到MapperRegistry中的getMapper:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
可以看到,这里使用了代理模式,Mybatis识别出了@Select注解,并生成代理类,在代理类中包含接口的实现方法。
再问一个问题:
可以看到,在我们在数据库中查询数据的结果是这样的,Mybatis是怎么把它转换为User类的呢?
其实这也是通过反射完成的,根据每一列的列名去查找User类中的成员变量,根据查到的行数生成对应个数的对象。
2.配置日志框架
- 1.根据官方文档的提示,先在config.xml中添加配置:
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
注意放置的位置:
要按照这个顺序放置配置,所以setting需要放在前面。
- 2.引入Log4j的maven依赖
- 3.创建 log4j.properties 文件,复制配置参数到其中:
注意日志等级需要修改:
# Global logging configuration
# 日志等级为DEBUG,标准输出
log4j.rootLogger=DEBUG, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
# 标准输出 = 控制台输出源
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
# 标准布局 = log4j模式化布局
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# 转化输出的模式 = 优先级(占5个字节)[线程名]
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
再次运行一下试试看:
DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - PooledDataSource forcefully closed/removed all connections.
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 2076287037.
DEBUG [main] - Setting autocommit to false on JDBC Connection [conn0: url=jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler user=ROOT]
DEBUG [main] - ==> Preparing: select * from user
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 4
User{id=1, name='zhangsan', tel='tel1', address='beijing'}
User{id=2, name='lisi', tel='tel2', address='shanghai'}
User{id=3, name='wangwu', tel='tel3', address='shanghai'}
User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}
DEBUG [main] - Resetting autocommit to true on JDBC Connection [conn0: url=jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler user=ROOT]
DEBUG [main] - Closing JDBC Connection [conn0: url=jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler user=ROOT]
DEBUG [main] - Returned connection 2076287037 to pool.
Process finished with exit code 0
现在就可以看到DEBUG等级及以下的日志信息了。注意:
log4j.properties文件必须直接放在resources目录下,否则系统会找不到该文件。
顺便介绍一下日志等级:
在log4j.jar/org/apache/log4j/Level类中可以看到有关日志等级的声明:
- TRACE-源信息
TRACE对程序运行没有影响,既不打印到控制台也不输出到文件,主要用以线上调试,如果需要查看TRACE等级的日志,需要通过elog命令开启TRACE,或者将程序日志输出级别降至TRACE。
- DEBUG-调试信息
默认情况下,打印至终端,但是不归档到日志文件。因此一般用于程序启动时,查看日志流水信息。
- INFO-应用程序运行信息
INFO等级的日志信息都是一过性的,不会大量反复输出。该级别日志默认情况下会打印到终端和日志文件。
- WARN-警告信息
表明程序处理中可能遇到的错误,以及非法数据。该警告是一过性的,可恢复不影响程序进行。
- ERROR-错误信息
该错误发生后程序任然可以运行,但是极有可能在某种不正常的情况下运行。
- FATAL-致命的错误信息
错误直接导致程序无法启动,需要立即解决。
- ALL/OFF-开启所有信息/关闭所有信息
3.Mybatis中的SQL语句核心Mapper(存放SQL关系映射的文件)
Mapper有两种:
- Mapper:接口由Mybatis动态代理,例如interface UserMapper。
优点:方便。直接写一个接口,然后加上@Selelct注解即可。
缺点:只能写一些简单的SQL语句,对于复杂的逻辑性SQL语句,就不好实现了(虽然JDK13中支持多行SQL)。 - Mapper:用XML编写复杂SQL。
优点:可以方便的使用Mybatis的强大功能。 缺点:SQL与代码分离。
详细介绍第二种Mapper。根据官网提示,新建Mapper.xml文件(文件名自己取),在文件中添加以下内容:
<?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="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
namespace-命名空间,自己随便取个名字。
id -给自己的SQL语句取名。
resultType -返回值类型。
示例:
<mapper namespace="com.github.hcsp.sql.Sql">
<select id="selectUsers" resultType="Map">
select id,name,address,tel from User
</select>
在主函数中:
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers"));
}
结果:
DEBUG [main] - <== Total: 4
[{ADDRESS=beijing, TEL=tel1, ID=1, NAME=zhangsan}, {ADDRESS=shanghai, TEL=tel2, ID=2, NAME=lisi}, {ADDRESS=shanghai, TEL=tel3, ID=3, NAME=wangwu}, {ADDRESS=shenzhen, TEL=tel4, ID=4, NAME=zhangsan}]
4.详细介绍Mapper的参数及返回值类型
返回值类型:
前面的实例中,我们的返回值类型为Map,所以返回的是键值对。那么,我们我们还可以新建一个类来存放结果。
像这样
public class User {
Integer id;
String name;
String address;
String tel;
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", address='" + address + '\'' +
", tel='" + tel + '\'' +
'}';
}
}
返回值类型为User的全限定类名
<select id="selectUsers" resultType="com.github.hcsp.sql.User">
结果如下:
[User{id=1, name='zhangsan', address='beijing', tel='tel1'}, User{id=2, name='lisi', address='shanghai', tel='tel2'}, User{id=3, name='wangwu', address='shanghai', tel='tel3'}, User{id=4, name='zhangsan', address='shenzhen', tel='tel4'}]
可以看到返回值就变成一个个的User了,读写参数都是遵守JavaBean约定使用getter()和setter()进行的。
注意:可以使用类型别名,简化resultType。假如有几十个方法的返回值类型都是User类型,每次都去写全限定类型实在是麻烦,而且包名不能动,否则返回值类型全都要动,所以设置类型别名很有必要。在config.xml中添加以下内容:
<typeAliases>
<typeAlias alias="User" type="com.github.hcsp.sql.Sql.User"/>
</typeAliases>
注意添加顺序,那么返回值类型可以直接写:
<select id="selectUsers" resultType="User">
其实Map也是全限定类名java.lang.HashMap的简写。
传入参数:
查找id为1的用户,可以看到:
<E> List<E> selectList(String statement, Object parameter);
selectList还有一个带parameter的多态方法,对于一个参数的SQL语句
select id,name,address,tel from User where id=#{id}
直接往里面塞一个参数即可:
sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers",1)
如果有多个参数就放一个类进去:
User user = new User();
user.id=1;
user.name = "zhangsan";
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers",user));
}
设置参数还可以使用${},二者区别在于${}只是简单的替换,而#{}是防注入的替换。
演示一下SQL注入:
以这条语句为例:
select id,name,address,tel from User where name='${name}' and id=${id}
我传入这样一个User:
User user = new User();
user.name = "'or 1=1--";
结果如下:
DEBUG [main] - ==> Preparing: select id,name,address,tel from User where name=''or 1=1--' and id=
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 4
[User{id=1, name='zhangsan', tel='tel1', address='beijing'}, User{id=2, name='lisi', tel='tel2', address='shanghai'}, User{id=3, name='wangwu', tel='tel3', address='shanghai'}, User{id=4, name='zhangsan', tel='tel4', address='shenzhen'}]
可以看到,我拿到了数据库中的所有内容。所以,${}是不安全的传参数的方法。
当然,除了每次都新建一个User对象这种耗费内存的方法之外,还可以用Map:
Map<Object,Object> map = new HashMap<>();
map.put("name","zhangsan");
map.put("id",1);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectUsers",map));
}
5.动态SQL-Mybatis的灵魂
文字部分Mybatis官网上有说明,也有例子,自己写两个加深下理解。
- if条件判断动态查找
<select id="selectUsers" resultType="User">
select id,name,address,tel from User where name='${name}'
<if test="id !=null">
and id=${id}
</if>
</select>
这里注意,不要把where、and这种语句写在if判断外面,否则像select * from user where这种语句是不符合格式要求的,就会报错。
- choose,when,otherwise-类似于if-else条件判断
这里有个小坑,举例如下:
<select id="selectUser" resultType="User">
select * from User
<choose>
<when test="name==zhangsan">
where name = 'zhangsan'
</when>
<otherwise>
where name = 'lisi'
</otherwise>
</choose>
</select>
第一次map.put("name","lisi");得到的是select * from User where name = 'lisi' ,这没问题,第二次map.put("name","zhangsan");,得到的还是select * from User where name = 'lisi'。这就蹊跷了,其实原因在于<when test="name==zhangsan">,没有把zhangsan用``包起来,Mybatis误以为zhangsan也是变量,等着你去传值,然后将name传入的`zhangsan`与null进行比较。所有无论后面参数怎么传,sql语句都不会按照你所想的逻辑去执行。同时这也证明了一点,name、zhangsan这种参数的初始值都是null。
-
where、trim、set
这个没什么好讲的,直接看官网的例子一看就明白了。 -
foreach-实现批量更新SQL
先来个简单的:
找出id在某个集合中的User
<select id="selectIdIn" resultType="User">
SELECT *
FROM User
WHERE id in
<!-- item/index-占位符,collection-需要从哪个集合中找出结果-->
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>
map.put("list",Arrays.asList(1,2,3,5,6));
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
System.out.println(sqlSession.selectList("com.github.hcsp.sql.Sql.selectIdIn",map));
}
结果如下:
DEBUG [main] - ==> Preparing: SELECT * FROM User WHERE id in ( ? , ? , ? , ? , ? )
DEBUG [main] - ==> Parameters: 1(Integer), 2(Integer), 3(Integer), 5(Integer), 6(Integer)
DEBUG [main] - <== Total: 3
[User{id=1, name='zhangsan', tel='tel1', address='beijing'}, User{id=2, name='lisi', tel='tel2', address='shanghai'}, User{id=3, name='wangwu', tel='tel3', address='shanghai'}]
foreach帮我们拼接了一个查找id在某个范围内的sql语句。
批量向表中插入user:
SQL语句如下:
<insert id="batchInsertUsers" parameterType="map">
insert into User(id,name,tel,address)
values
<foreach item="user" collection="users" separator=",">
(#{user.id},#{user.name},#{user.tel},#{user.address})
</foreach>
</insert>
其实这样看来Mybatis中的foreach与我们平时写的foreach语句很类似,user是迭代的项目,users是被迭代的集合,中间需要逗号连接。
DEBUG [main] - ==> Preparing: insert into User(id,name,tel,address) values (?,?,?,?) , (?,?,?,?)
DEBUG [main] - ==> Parameters: null, abcd(String), tel-abcd-1(String), addr-abcd(String), null, abcd(String), tel-abcd-2(String), addr-abcd(String)
可以看到Mybatis帮我们拼出了批量插入数据的语句。顺便说一句parameterType不是必要的。
6.如何实现表的连接
查询订单信息,只查询用户名、商品名齐全的订单,即INNER JOIN方式
可以看到:
public class Order {
private Integer id;
/** 订单中的用户 */
private User user;
/** 订单中的商品 */
private Goods goods;
/** 订单中的总成交金额 */
private BigDecimal totalPrice;
在order表中嵌套了user跟goods,那么Order的id可以直接获得,如何把从order表中获取到的结果,赋值给User、Goods类,这就需要使用Mybatis里面的association嵌套了。
<select id="getInnerJoinOrders" resultMap="order">
select `order`.id as order_id,
user.name as user_name,
goods.name as goods_name,
`order`.goods_num as goods_num,
goods.price as goods_price,
`order`.goods_num * `order`.goods_price as total_price
from `order`
inner join goods on goods.id = `order`.goods_id
inner join user on user.id = `order`.user_id
</select>
<resultMap id="order" type="Order">
<result property="id" column="order_id"/>
<result property="totalPrice" column="total_price"/>
<association property="user" javaType="User">
<result property="name" column="user_name"/>
</association>
<association property="goods" javaType="Goods">
<result property="name" column="goods_name"/>
<result property="price" column="goods_price"/>
</association>
</resultMap>
可以看到,对于这种情况,我们不能直接返回一个确切的resultType,而是返回一个结果映射resultMap,也就是在这个例子中,我们的查询结果order会被映射为另一个对象。
- 这里的Order、Goods都已经使用typeAlias修改过别名了。
- 都是一对一的关系,所以使用的是<association>标签。
7.参考资料:
- 1.CSDN.《mybatis框架之 log4j.properties 的配置》点击此处跳转至源文章
- 2.CSDN.《写程序时如何使用日志》点击此处跳转至源文章
- 3.博客园.《编程日志的等级划分》点击此处查看源文章