Permalink
Browse files

Add JDBC statement parameter logging

Parameters are logged in tabular form to the new logger
`slick.jdbc.JdbcBackend.parameter` similar to how results can already
be logged to `slick.jdbc.StatementInvoker.result`. The table header
contains the parameter index (1-based, as in JDBC) and the type. Batch
operations with matching parameter types are shown in a single table. If
the types change during a batch, the table gets broken up.

Since the logging is integrated into the `LoggingPreparedStatement`
decorator, it works for all kinds of statements and for both, Plain SQL
queries and generated (Lifted Embedding) queries.

Fixes #1150.
  • Loading branch information...
1 parent c3d6c54 commit 2a06b58c198af0a6c4dcb35aa98aec8bf1b9d440 @szeiger szeiger committed Apr 22, 2016
@@ -43,6 +43,7 @@
<logger name="slick.compiler.VerifyTypes" level="${log.qcomp.verifyTypes:-inherited}" />
<logger name="slick.jdbc.DriverDataSource" level="${log.jdbc.driver:-info}" />
<logger name="slick.jdbc.JdbcBackend.statement" level="${log.jdbc.statement:-info}" />
+ <logger name="slick.jdbc.JdbcBackend.parameter" level="${log.jdbc.parameter:-info}" />
<logger name="slick.jdbc.JdbcBackend.benchmark" level="${log.jdbc.bench:-info}" />
<logger name="slick.jdbc.StatementInvoker.result" level="${log.jdbc.result:-info}" />
<logger name="slick.jdbc.JdbcModelBuilder" level="${log.createModel:-info}" />
@@ -396,10 +396,12 @@ trait JdbcBackend extends RelationalBackend {
}
protected def loggingStatement(st: Statement): Statement =
- if(JdbcBackend.statementLogger.isDebugEnabled || JdbcBackend.benchmarkLogger.isDebugEnabled) new LoggingStatement(st) else st
+ if(JdbcBackend.statementLogger.isDebugEnabled || JdbcBackend.benchmarkLogger.isDebugEnabled)
+ new LoggingStatement(st) else st
protected def loggingPreparedStatement(st: PreparedStatement): PreparedStatement =
- if(JdbcBackend.statementLogger.isDebugEnabled || JdbcBackend.benchmarkLogger.isDebugEnabled) new LoggingPreparedStatement(st) else st
+ if(JdbcBackend.statementLogger.isDebugEnabled || JdbcBackend.benchmarkLogger.isDebugEnabled || JdbcBackend.parameterLogger.isDebugEnabled)
+ new LoggingPreparedStatement(st) else st
/** Start a `transactionally` block */
private[slick] def startInTransaction: Unit
@@ -505,6 +507,7 @@ object JdbcBackend extends JdbcBackend {
protected[jdbc] lazy val statementLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".statement"))
protected[jdbc] lazy val benchmarkLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".benchmark"))
+ protected[jdbc] lazy val parameterLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".parameter"))
protected[jdbc] def logStatement(msg: String, stmt: String) = if(statementLogger.isDebugEnabled) {
val s = if(GlobalConfig.sqlIndent) msg + ":\n" + LogUtil.multilineBorder(stmt) else msg + ": " + stmt
@@ -1,20 +1,89 @@
package slick.jdbc
+import slick.util.TableDump
+
+import scala.collection.mutable.ArrayBuffer
import scala.language.reflectiveCalls
-import java.sql.{PreparedStatement, Connection, SQLWarning, ResultSet, Statement}
+import java.io.{InputStream, Reader}
+import java.util.Calendar
+import java.{sql => js}
+import java.sql.{PreparedStatement, Connection, SQLWarning, ResultSet, Statement, Timestamp}
/** A wrapper for `java.sql.Statement` that logs statements and benchmark results
* to the appropriate [[JdbcBackend]] loggers. */
class LoggingStatement(st: Statement) extends Statement {
private[this] val doStatement = JdbcBackend.statementLogger.isDebugEnabled
private[this] val doBenchmark = JdbcBackend.benchmarkLogger.isDebugEnabled
+ private[this] val doParameter = JdbcBackend.parameterLogger.isDebugEnabled
+
+ private[this] var params: ArrayBuffer[(Any, Any)] = null
+ private[this] var paramss: ArrayBuffer[ArrayBuffer[(Any, Any)]] = null
+
+ /** log a parameter */
+ protected[this] def p(idx: Int, tpe: Any, value: Any): Unit = if(doParameter) {
+ if(params eq null) params = new ArrayBuffer
+ if(params.size == idx) params += ((tpe, value))
+ else {
+ while(params.size < idx) params += null
+ params(idx-1) = (tpe, value)
+ }
+ }
- @inline protected[this] def logged[T](sql: String, what: String = "statement")(f: =>T) = {
+ protected[this] def pushParams: Unit = if(doParameter) {
+ if(params ne null) {
+ if(paramss eq null) paramss = new ArrayBuffer
+ paramss += params
+ }
+ params = null
+ }
+
+ protected[this] def clearParamss: Unit = if(doParameter) {
+ paramss = null
+ params = null
+ }
+
+ protected[this] def logged[T](sql: String, what: String = "statement")(f: =>T) = {
if(doStatement && (sql ne null)) JdbcBackend.statementLogger.debug("Executing "+what+": "+sql)
+ if(doParameter && (paramss ne null) && paramss.nonEmpty) {
+ // like s.groupBy but only group adjacent elements and keep the ordering
+ def groupBy[T](s: TraversableOnce[T])(f: T => AnyRef): IndexedSeq[IndexedSeq[T]] = {
+ var current: AnyRef = null
+ val b = new ArrayBuffer[ArrayBuffer[T]]
+ s.foreach { v =>
+ val id = f(v)
+ if(b.isEmpty || current != id) b += ArrayBuffer(v)
+ else b.last += v
+ current = id
+ }
+ b
+ }
+ def mkTpStr(tp: Int) = JdbcTypesComponent.typeNames.getOrElse(tp, tp.toString)
+ val paramSets = paramss.map { params =>
+ (params.map {
+ case (tp: Int, _) => mkTpStr(tp)
+ case ((tp: Int, scale: Int), _) => mkTpStr(tp)+"("+scale+")"
+ case ((tp: Int, tpStr: String), _) => mkTpStr(tp)+": "+tpStr
+ case (tpe, _) => tpe.toString
+ }, params.map {
+ case (_, null) => "NULL"
+ case (_, ()) => ""
+ case (_, v) =>
+ val s = v.toString
+ if(s eq null) "NULL" else s
+ })
+ }
+ val dump = new TableDump(25)
+ groupBy(paramSets)(_._1).foreach { matchingSets =>
+ val tpes = matchingSets.head._1
+ val idxs = 1.to(tpes.length).map(_.toString)
+ dump(Vector(idxs, tpes), matchingSets.map(_._2)).foreach(s => JdbcBackend.parameterLogger.debug(s))
+ }
+ }
val t0 = if(doBenchmark) System.nanoTime() else 0L
val res = f
if(doBenchmark) JdbcBackend.benchmarkLogger.debug("Execution of "+what+" took "+formatNS(System.nanoTime()-t0))
+ clearParamss
res
}
@@ -77,64 +146,70 @@ class LoggingStatement(st: Statement) extends Statement {
st.asInstanceOf[{ def isCloseOnCompletion(): Boolean }].isCloseOnCompletion()
}
-/** A wrapper for `java.sql.PreparedStatement` that logs statements and benchmark results
+/** A wrapper for `java.sql.PreparedStatement` that logs statements, parameters and benchmark results
* to the appropriate [[JdbcBackend]] loggers. */
class LoggingPreparedStatement(st: PreparedStatement) extends LoggingStatement(st) with PreparedStatement {
- def execute(): Boolean = logged(null, "prepared statement") { st.execute() }
- def executeQuery(): java.sql.ResultSet = logged(null, "prepared query") { st.executeQuery() }
- def executeUpdate(): Int = logged(null, "prepared update") { st.executeUpdate() }
-
- def addBatch(): Unit = st.addBatch()
- def clearParameters(): Unit = st.clearParameters()
- def getMetaData(): java.sql.ResultSetMetaData = st.getMetaData
- def getParameterMetaData(): java.sql.ParameterMetaData = st.getParameterMetaData()
- def setArray(idx: Int, value: java.sql.Array): Unit = st.setArray(idx, value)
- def setAsciiStream(idx: Int, value: java.io.InputStream): Unit = st.setAsciiStream(idx, value)
- def setAsciiStream(idx: Int, value: java.io.InputStream, len: Long): Unit = st.setAsciiStream(idx, value, len)
- def setAsciiStream(idx: Int, value: java.io.InputStream, len: Int): Unit = st.setAsciiStream(idx, value, len)
- def setBigDecimal(idx: Int, value: java.math.BigDecimal): Unit = st.setBigDecimal(idx, value)
- def setBinaryStream(idx: Int, value: java.io.InputStream): Unit = st.setBinaryStream(idx, value)
- def setBinaryStream(idx: Int, value: java.io.InputStream, len: Long): Unit = st.setBinaryStream(idx, value, len)
- def setBinaryStream(idx: Int, value: java.io.InputStream, len: Int): Unit = st.setBinaryStream(idx, value, len)
- def setBlob(idx: Int, value: java.io.InputStream): Unit = st.setBlob(idx, value)
- def setBlob(idx: Int, value: java.io.InputStream, len: Long): Unit = st.setBlob(idx, value, len)
- def setBlob(idx: Int, value: java.sql.Blob): Unit = st.setBlob(idx, value)
- def setBoolean(idx: Int, value: Boolean): Unit = st.setBoolean(idx, value)
- def setByte(idx: Int, value: Byte): Unit = st.setByte(idx, value)
- def setBytes(idx: Int, value: Array[Byte]): Unit = st.setBytes(idx, value)
- def setCharacterStream(idx: Int, value: java.io.Reader): Unit = st.setCharacterStream(idx, value)
- def setCharacterStream(idx: Int, value: java.io.Reader, len: Long): Unit = st.setCharacterStream(idx, value, len)
- def setCharacterStream(idx: Int, value: java.io.Reader, len: Int): Unit = st.setCharacterStream(idx, value, len)
- def setClob(idx: Int, value: java.io.Reader): Unit = st.setClob(idx, value)
- def setClob(idx: Int, value: java.io.Reader, len: Long): Unit = st.setClob(idx, value, len)
- def setClob(idx: Int, value: java.sql.Clob): Unit = st.setClob(idx, value)
- def setDate(idx: Int, value: java.sql.Date, cal: java.util.Calendar): Unit = st.setDate(idx, value, cal)
- def setDate(idx: Int, value: java.sql.Date): Unit = st.setDate(idx, value)
- def setDouble(idx: Int, value: Double): Unit = st.setDouble(idx, value)
- def setFloat(idx: Int, value: Float): Unit = st.setFloat(idx, value)
- def setInt(idx: Int, value: Int): Unit = st.setInt(idx, value)
- def setLong(idx: Int, value: Long): Unit = st.setLong(idx, value)
- def setNCharacterStream(idx: Int, value: java.io.Reader): Unit = st.setNCharacterStream(idx, value)
- def setNCharacterStream(idx: Int, value: java.io.Reader, len: Long): Unit = st.setNCharacterStream(idx, value, len)
- def setNClob(idx: Int, value: java.io.Reader): Unit = st.setNClob(idx, value)
- def setNClob(idx: Int, value: java.io.Reader, len: Long): Unit = st.setNClob(idx, value, len)
- def setNClob(idx: Int, value: java.sql.NClob): Unit = st.setNClob(idx, value)
- def setNString(idx: Int, value: String): Unit = st.setNString(idx, value)
- def setNull(idx: Int, tpe: Int, tpeString: String): Unit = st.setNull(idx, tpe, tpeString)
- def setNull(idx: Int, tpe: Int): Unit = st.setNull(idx, tpe)
- def setObject(idx: Int, value: Any, tpe: Int, scale: Int): Unit = st.setObject(idx, value, tpe, scale)
- def setObject(idx: Int, value: Any): Unit = st.setObject(idx, value)
- def setObject(idx: Int, value: Any, tpe: Int): Unit = st.setObject(idx, value, tpe)
- def setRef(idx: Int, value: java.sql.Ref): Unit = st.setRef(idx, value)
- def setRowId(idx: Int, value: java.sql.RowId): Unit = st.setRowId(idx, value)
- def setSQLXML(idx: Int, value: java.sql.SQLXML): Unit = st.setSQLXML(idx, value)
- def setShort(idx: Int, value: Short): Unit = st.setShort(idx, value)
- def setString(idx: Int, value: String): Unit = st.setString(idx, value)
- def setTime(idx: Int, value: java.sql.Time, cal: java.util.Calendar): Unit = st.setTime(idx, value, cal)
- def setTime(idx: Int, value: java.sql.Time): Unit = st.setTime(idx, value)
- def setTimestamp(idx: Int, value: java.sql.Timestamp, cal: java.util.Calendar): Unit = st.setTimestamp(idx, value, cal)
- def setTimestamp(idx: Int, value: java.sql.Timestamp): Unit = st.setTimestamp(idx, value)
- def setURL(idx: Int, value: java.net.URL): Unit = st.setURL(idx, value)
+
+ def execute(): Boolean = { pushParams; logged(null, "prepared statement") { st.execute() } }
+ def executeQuery(): js.ResultSet = { pushParams; logged(null, "prepared query") { st.executeQuery() } }
+ def executeUpdate(): Int = { pushParams; logged(null, "prepared update") { st.executeUpdate() } }
+
+ def addBatch(): Unit = { pushParams; st.addBatch() }
+ def clearParameters(): Unit = { clearParamss; st.clearParameters() }
+
+ def getMetaData(): js.ResultSetMetaData = st.getMetaData
+ def getParameterMetaData(): js.ParameterMetaData = st.getParameterMetaData()
+
+ // printable parameters
+ def setArray (i: Int, v: js.Array ): Unit = { p(i, "Array", v); st.setArray(i, v) }
+ def setBigDecimal (i: Int, v: java.math.BigDecimal ): Unit = { p(i, "BigDecimal", v); st.setBigDecimal(i, v) }
+ def setBoolean (i: Int, v: Boolean ): Unit = { p(i, "Boolean", v); st.setBoolean(i, v) }
+ def setByte (i: Int, v: Byte ): Unit = { p(i, "Byte", v); st.setByte(i, v) }
+ def setBytes (i: Int, v: Array[Byte] ): Unit = { p(i, "Bytes", v); st.setBytes(i, v) }
+ def setDate (i: Int, v: js.Date, c: Calendar ): Unit = { p(i, "Date", (v, c)); st.setDate(i, v, c) }
+ def setDate (i: Int, v: js.Date ): Unit = { p(i, "Date", v); st.setDate(i, v) }
+ def setDouble (i: Int, v: Double ): Unit = { p(i, "Double", v); st.setDouble(i, v) }
+ def setFloat (i: Int, v: Float ): Unit = { p(i, "Float", v); st.setFloat(i, v) }
+ def setInt (i: Int, v: Int ): Unit = { p(i, "Int", v); st.setInt(i, v) }
+ def setLong (i: Int, v: Long ): Unit = { p(i, "Long", v); st.setLong(i, v) }
+ def setNString (i: Int, v: String ): Unit = { p(i, "NString", v); st.setNString(i, v) }
+ def setNull (i: Int, tp: Int, tpStr: String ): Unit = { p(i, (tp, tpStr), null); st.setNull(i, tp, tpStr) }
+ def setNull (i: Int, tp: Int ): Unit = { p(i, tp, null); st.setNull(i, tp) }
+ def setObject (i: Int, v: Any, tp: Int, scl: Int): Unit = { p(i, (tp, scl), v); st.setObject(i, v, tp, scl) }
+ def setObject (i: Int, v: Any, tp: Int ): Unit = { p(i, tp, v); st.setObject(i, v, tp) }
+ def setObject (i: Int, v: Any ): Unit = { p(i, "Object", v); st.setObject(i, v) }
+ def setRef (i: Int, v: js.Ref ): Unit = { p(i, "Ref", v); st.setRef(i, v) }
+ def setRowId (i: Int, v: js.RowId ): Unit = { p(i, "RowId", v); st.setRowId(i, v) }
+ def setSQLXML (i: Int, v: js.SQLXML ): Unit = { p(i, "SQLXML", v); st.setSQLXML(i, v) }
+ def setShort (i: Int, v: Short ): Unit = { p(i, "Short", v); st.setShort(i, v) }
+ def setString (i: Int, v: String ): Unit = { p(i, "String", v); st.setString(i, v) }
+ def setTime (i: Int, v: js.Time, c: Calendar ): Unit = { p(i, "Time", (v, c)); st.setTime(i, v, c) }
+ def setTime (i: Int, v: js.Time ): Unit = { p(i, "Time", v); st.setTime(i, v) }
+ def setTimestamp (i: Int, v: Timestamp, c: Calendar): Unit = { p(i, "Timestamp", (v, c)); st.setTimestamp(i, v, c) }
+ def setTimestamp (i: Int, v: Timestamp ): Unit = { p(i, "Timestamp", v); st.setTimestamp(i, v) }
+ def setURL (i: Int, v: java.net.URL ): Unit = { p(i, "URL", v); st.setURL(i, v) }
+
+ // hidden parameters
+ def setAsciiStream (i: Int, v: InputStream ): Unit = { p(i, "AsciiStream", ()); st.setAsciiStream(i, v) }
+ def setAsciiStream (i: Int, v: InputStream, len: Long): Unit = { p(i, "AsciiStream", ()); st.setAsciiStream(i, v, len) }
+ def setAsciiStream (i: Int, v: InputStream, len: Int ): Unit = { p(i, "AsciiStream", ()); st.setAsciiStream(i, v, len) }
+ def setBinaryStream (i: Int, v: InputStream ): Unit = { p(i, "BinaryStream", ()); st.setBinaryStream(i, v) }
+ def setBinaryStream (i: Int, v: InputStream, len: Long): Unit = { p(i, "BinaryStream", ()); st.setBinaryStream(i, v, len) }
+ def setBinaryStream (i: Int, v: InputStream, len: Int ): Unit = { p(i, "BinaryStream", ()); st.setBinaryStream(i, v, len) }
+ def setBlob (i: Int, v: InputStream ): Unit = { p(i, "Blob", ()); st.setBlob(i, v) }
+ def setBlob (i: Int, v: InputStream, len: Long): Unit = { p(i, "Blob", ()); st.setBlob(i, v, len) }
+ def setBlob (i: Int, v: js.Blob ): Unit = { p(i, "Blob", ()); st.setBlob(i, v) }
+ def setCharacterStream (i: Int, v: Reader ): Unit = { p(i, "CharacterStream", ()); st.setCharacterStream(i, v) }
+ def setCharacterStream (i: Int, v: Reader, len: Long ): Unit = { p(i, "CharacterStream", ()); st.setCharacterStream(i, v, len) }
+ def setCharacterStream (i: Int, v: Reader, len: Int ): Unit = { p(i, "CharacterStream", ()); st.setCharacterStream(i, v, len) }
+ def setClob (i: Int, v: Reader ): Unit = { p(i, "Clob", ()); st.setClob(i, v) }
+ def setClob (i: Int, v: Reader, len: Long ): Unit = { p(i, "Clob", ()); st.setClob(i, v, len) }
+ def setClob (i: Int, v: js.Clob ): Unit = { p(i, "Clob", ()); st.setClob(i, v) }
+ def setNCharacterStream(i: Int, v: Reader ): Unit = { p(i, "NCharacterStream", ()); st.setNCharacterStream(i, v) }
+ def setNCharacterStream(i: Int, v: Reader, len: Long ): Unit = { p(i, "NCharacterStream", ()); st.setNCharacterStream(i, v, len) }
+ def setNClob (i: Int, v: Reader ): Unit = { p(i, "NClob", ()); st.setNClob(i, v) }
+ def setNClob (i: Int, v: Reader, len: Long ): Unit = { p(i, "NClob", ()); st.setNClob(i, v, len) }
+ def setNClob (i: Int, v: js.NClob ): Unit = { p(i, "NClob", ()); st.setNClob(i, v) }
@deprecated("setUnicodeStream is deprecated", "")
- def setUnicodeStream(idx: Int, value: java.io.InputStream, len: Int): Unit = st.setUnicodeStream(idx, value, len)
+ def setUnicodeStream (i: Int, v: InputStream, len: Int ): Unit = { p(i, "UnicodeStream", ()); st.setUnicodeStream(i, v, len) }
}
@@ -48,6 +48,9 @@ The following loggers are particularly interesting:
``slick.jdbc.JdbcBackend.statement``
Shows all SQL statements which are executed.
+``slick.jdbc.JdbcBackend.parameter``
+ Shows bind variable contents (for supported types) of all SQL statements which are executed.
+
``slick.jdbc.JdbcBackend.benchmark``
Shows execution times for SQL statements.

0 comments on commit 2a06b58

Please sign in to comment.