Hibernate学习笔记(二)记录生成SQL中的参数

本章主要研究在Hibernate中,如何在生成的SQL当中将参数占位符?替换成实际的值,但同时还支持XA分布式事务管理系统。

1. Hibernate生成的SQL

在Hibernate配置中,可以配置让Hibernate自动输出其生成的SQL语句,以便开发人员了解程序底层的机制,方便开发人员调试程序。要让Hibernate输出SQL语句,只要在persistence.xml文件中配置:

persistence.xml
1
……

然而,在生成SQL语句的时候,Hibernate并不会将参数占位符?替换成具体的参数:

1
2
3
4
5
6
7
8
Hibernate: 
/* insert com.rainsia.hibernate.model.Message
*/ insert
into
Message
(text, id)
values
(?, ?)

虽然可以让Hibernate在SQL语句之后输出参数,但是在参数较多且比较复杂的时候,这种方式还是使得调试非常不便。

2. 使用P6spy辅助Hibernate生成SQL

本章中,我们借助p6spy库的功能来让Hibernate生成的SQL语句中附带具体的参数。

P6spy的功能非常简单,它其实是一个JDBC驱动代理。其主要功能就是让SQL语句通过p6spy的时候输出同时将要执行的SQL语句转交给真正的JDBC驱动真正执行。因此,需要在原来需要JDBC驱动的地方替换成p6spay驱动,然后再p6spay中配置真正的JDBC驱动到底是谁。

对于简单的JDBC应用来说,配置非常简单。然而,我们的程序中使用了XA分布式事务管理框架,配置相对复杂。好在新版本的P6spay本身也是支持XA事务管理框架的。

首先,我们需要在build.gradle中添加p6spy依赖:

build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* This build file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java Library project to get you started.
* For more details take a look at the Java Libraries chapter in the Gradle
* user guide available at https://docs.gradle.org/4.4.1/userguide/java_library_plugin.html
*/

//为Gradle提供Java相关的构建任务
apply plugin: 'java'

//定义依赖仓库的下载地址
repositories {
//添加阿里云Maven镜像仓库,并置顶,提高jar包下载速度
maven {url 'http://maven.aliyun.com/nexus/content/groups/public/'}
//Maven仓库,补充阿里云镜像没有的jar包
mavenCentral()
//bintray仓库
jcenter()
}

dependencies {
//添加Hibernate实体管理器依赖,由于gradle会自动传递依赖,因此Hibernate Core也会被下载
compile 'org.hibernate:hibernate-entitymanager:5.2.12.Final'
//添加mysql驱动
//compile 'mysql:mysql-connector-java:5.1.45'
//添加mariaDB驱动
compile 'org.mariadb.jdbc:mariadb-java-client:2.2.2'
//添加Bitronix分布式事务管理包(不理解可以先不管,照写就行)
compile 'org.codehaus.btm:btm:2.1.4'
//添加日志slf4j适配器,目前我们使用jdk1.4开始自带的日志库,常见的日志库还有log4j和logback
compile 'org.slf4j:slf4j-jdk14:1.7.25'
//sql代理,用于拦截和记录SQL调用的参数
compile 'p6spy:p6spy:3.6.0'

//添加JUnit单元测试依赖
testImplementation 'junit:junit:4.12'
}

然后,再在src/main/resource/目录中添加spy-datasource.properties文件(原来的datasource.properties依然要保留),来配置p6spy的数据源:

spy-datasource.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#配置spy的数据源
resource.spyDS.className=com.p6spy.engine.spy.P6DataSource
resource.spyDS.uniqueName=spyDS

# 连接池大小的配置
resource.spyDS.minPoolSize=1
resource.spyDS.maxPoolSize=5
resource.spyDS.preparedStatementCacheSize=10

# 并发锁的隔离级别设置
resource.spyDS.isolationLevel=READ_COMMITTED
# 保证在非事务环境下,Hibernate也可以自动提交数据
# 相当于JDBC的connection.setAutoCommit(true)
resource.spyDS.allowLocalTransactions=true

# 常规JDBC参数配置
resource.spyDS.driverProperties.realDataSource=myDS

其配置几乎和原来的datasource.properties一致,区别在于数据源的名称使用p6spy提供的数据源驱动,驱动属性当中的realDataSource属性配置成原来的数据源名称myDS,以实现执行SQL语句的委托。需要提供一个和源数据源JNDI名称不同的全新名称来查找p6spy数据源,本程序中使用名称spayDS。

修改persistence.xml文件,现在需要使用spyDS为JNDI名称来查找数据源:

persistence.xml
1
<persistence version="2.1"

然后,修改TransactionManagerSetup类,现在需要加载两次数据源,第一次加载真正的数据源,并初始化。然后再加载p6spy数据源,并依赖于真正数据源:

TransactionManagerSetup.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package com.rainsia.hibernate.env;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.transaction.Status;
import javax.transaction.UserTransaction;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import bitronix.tm.Configuration;
import bitronix.tm.TransactionManagerServices;

/**
* 创建JNDI上下文,加载数据源配置文件,将创建数据源并放入JNDI上下文中以备查找
*
* @author Yu Xia <rainsia@163.com>
*
*/
public class TransactionManagerSetup {

public static final String CONFIGURATION_FILE = "datasource.properties";
public static final String SPY_CONFIGURATION_FILE = "spy-datasource.properties";

/**
* 日志
*/
private static final Logger logger = LoggerFactory.getLogger(TransactionManagerSetup.class);

/**
* JNDI上下文,创建的时候会直接读取jndi.properties文件
*/
protected final Context context = new InitialContext();

/**
* 构造函数,创建JNDI上下文、创建并初始化数据源、
*
* @throws NamingException
*/
public TransactionManagerSetup() throws NamingException {
Configuration conf = TransactionManagerServices.getConfiguration();

logger.info("创建用于事务恢复的唯一id");
conf.setServerId("myServer1234");

logger.info("禁用JMX绑定");
conf.setDisableJmx(true);

logger.info("禁用事务日志");
conf.setJournal("null");

logger.info("不警告空事务访问(即在事务期间无访问数据库的操作)");
conf.setWarnAboutZeroResourceTransaction(false);

String path = getConfigurationFilePath(CONFIGURATION_FILE);
logger.info("读取真实数据源配置文件" + path);
TransactionManagerServices.getConfiguration().setResourceConfigurationFilename(path);

logger.info("初始化真实数据源");
TransactionManagerServices.getResourceLoader().init();

path = getConfigurationFilePath(SPY_CONFIGURATION_FILE);
logger.info("读取p6spy数据源配置文件" + path);
TransactionManagerServices.getConfiguration().setResourceConfigurationFilename(path);

logger.info("初始化p6spy数据源");
TransactionManagerServices.getResourceLoader().init();
}

public Context getNamingContext() {
return context;
}

/**
* 创建UserTransaction,这是JTA的入口
*
* @return
*/
public UserTransaction getUserTransaction() {
try {
return TransactionManagerServices.getTransactionManager();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

/**
* 通过JNDI查找数据源
*
* @return
*/
public DataSource getDataSource(String datasourceName) {
try {
return (DataSource) getNamingContext().lookup(datasourceName);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

/**
* 回滚数据
*/
public void rollback() {
UserTransaction tx = getUserTransaction();
try {
if (tx.getStatus() == Status.STATUS_ACTIVE ||
tx.getStatus() == Status.STATUS_MARKED_ROLLBACK)
tx.rollback();
} catch (Exception ex) {
logger.error("回滚数据库失败,信息如下:");
logger.error(ex.getMessage());
}
}

/**
* 关闭事务管理器
* @throws Exception
*/
public void stop() throws Exception {
logger.info("关闭事务管理器");
TransactionManagerServices.getTransactionManager().shutdown();
}

/**
* 获取数据源配置文件的绝对路径
*
* @return
* @throws UnsupportedEncodingException
*/
private String getConfigurationFilePath(String filename) {
String path = Thread.currentThread().getContextClassLoader().getResource(filename).getPath();
try {
return URLDecoder.decode(path, "utf-8");
} catch (UnsupportedEncodingException e) { //对于utf-8编码来说,这永远不会发生
e.printStackTrace();
}
return null;
}

}

然后,在src/main/resources目录中创建一个spy.properties用于配置p6spy的输出:

spy.properties
1
2
3
4
# 使用slf4j作为日期记录器
appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 控制输出的格式,默认有三种:SingleLineFormat、MultiLineFormat和CustomLineFormat
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat

然后运行程序,可以看到如下输出:

控制台输出

从中可以看出,现在除了输出Hibernate生成的SQL语句之外,p6spy还输出的替换占位符之后的输出。使用MultiLineFormat的时候,p6spy的每条日志会分为两行,第一行记录了原来Hibernate的输出,第二行记录了替换参数之后的输出:

1
2
3
4
5
……
三月 17, 2018 1:23:09 下午 com.p6spy.engine.spy.appender.Slf4JLogger logSQL
信息: #1521264189119 | took 1ms | statement | connection 0|/* insert com.rainsia.hibernate.model.Message */ insert into Message (text, id) values (?, ?)
/* insert com.rainsia.hibernate.model.Message */ insert into Message (text, id) values ('Hello World!', 1);
……

然而,第一行的内容和Hibernate输出的结果几乎是重复的。因此,我们可以配置CustomLineFormat来输出,这时候只输出一行内容:

1
2
3
4
……
三月 17, 2018 1:28:15 下午 com.p6spy.engine.spy.appender.Slf4JLogger logSQL
信息: 1521264495531|1|statement|connection0|/* insert com.rainsia.hibernate.model.Message */ insert into Message (text, id) values ('Hello World!', 1)
……

最终,我们在Hibernate中配置了p6spy库,让其替换SQL语句中的占位符,方便开发人员调试程序。同时又不改变原来的数据库驱动和对XA数据源的支持。

本文标题:Hibernate学习笔记(二)记录生成SQL中的参数

文章作者:Rain Sia

发布时间:2018年03月17日 - 12:03

最后更新:2018年10月29日 - 10:10

原始链接: http://rainsia.github.io/2018/03/17/hibernate-002/

版权信息:本文为作者原创文章,如需进行非商业性转载,请注明出处并保留原文链接及作者。如要进行商业性转载,请获得作者授权!

联系方式:rainsia@163.com