Middleware
What Middleware Is
In Juice, middleware intercepts the SQL execution chain. It is the right place for logging, timeout control, tracing, metrics, datasource routing, and other cross-cutting behavior.
The current core interfaces are:
type Handler[T any] func(ctx context.Context, query string, args ...any) (T, error)
type QueryHandler = Handler[sql.Rows]
type ExecHandler = Handler[sql.Result]
type Middleware interface {
QueryContext(ctx *StatementContext, next QueryHandler) QueryHandler
ExecContext(ctx *StatementContext, next ExecHandler) ExecHandler
}
StatementContext carries the execution metadata available to middleware:
type StatementContext struct {
// Fields are unexported. Use methods to access them.
}
func (m *StatementContext) Engine() *Engine
func (m *StatementContext) Statement() Statement
func (m *StatementContext) Context() context.Context
func (m *StatementContext) Param() eval.Param
func (m *StatementContext) Session() session.Session
func (m *StatementContext) WithSession(session session.Session)
QueryContext intercepts query statements. ExecContext intercepts insert, update, delete, and write-style raw SQL operations.
Registration and Order
Register middleware with engine.Use:
engine.Use(&TraceMiddleware{})
engine.Use(&MetricsMiddleware{})
Middleware is composed in registration order, but the last registered middleware runs first. If you register A, then B, then C, runtime flow is:
C before -> B before -> A before -> database -> A after -> B after -> C after
juice.New installs the core generated-key middleware. juice.Default builds on New and additionally installs TimeoutMiddleware and DebugMiddleware.
DebugMiddleware
DebugMiddleware logs SQL, bound arguments, and elapsed time.
It is installed automatically when you initialize the engine with juice.Default:
engine, err := juice.Default(cfg)
To disable SQL logging globally:
<settings>
<setting name="debug" value="false"/>
</settings>
To disable SQL logging for a single statement:
<select id="GetUser" debug="false">
select * from user where id = #{id}
</select>
Priority:
Statement-level
debug="false"disables logging for that statement.Global
<setting name="debug" value="false"/>disables logging globally.If neither is set to
false, debug logging is enabled.
TimeoutMiddleware
TimeoutMiddleware reads the statement timeout attribute and wraps execution with context.WithTimeout.
The unit is milliseconds.
<select id="GetUser" timeout="1000">
select * from user where id = #{id}
</select>
Attention
TimeoutMiddleware applies a Go context timeout. Whether a running database statement is interrupted promptly depends on the concrete database driver and its context.Context support.
TxSensitiveDataSourceSwitchMiddleware
TxSensitiveDataSourceSwitchMiddleware routes query statements to different datasources. It is commonly used for read-write splitting.
Enable it with:
engine.Use(&juice.TxSensitiveDataSourceSwitchMiddleware{})
Datasource selection priority:
The
dataSourceattribute on aselectstatement.The global
selectDataSourcesetting.No switch when neither is configured.
Special values:
?: randomly choose from all registered datasources.?!: randomly choose from datasources other than the current default datasource. If none is available, it falls back to the current datasource.Any other string: use that environment id directly, for example
slave1.
Example:
<settings>
<setting name="selectDataSource" value="?!"/>
</settings>
<select id="GetUser" dataSource="slave1">
select * from user where id = #{id}
</select>
The middleware is transaction-aware. If the current execution is already inside a transaction, it does not switch datasources and continues to use the current transaction session.
Custom Middleware
Custom middleware only needs to implement the Middleware interface.
Simple tracing example:
type TraceMiddleware struct{}
func (m TraceMiddleware) QueryContext(sc *juice.StatementContext, next juice.QueryHandler) juice.QueryHandler {
stmt := sc.Statement()
return func(ctx context.Context, query string, args ...any) (juiceSql.Rows, error) {
trace.Log(ctx, "statement", stmt.Name())
trace.Log(ctx, "query", query)
return next(ctx, query, args...)
}
}
func (m TraceMiddleware) ExecContext(sc *juice.StatementContext, next juice.ExecHandler) juice.ExecHandler {
stmt := sc.Statement()
return func(ctx context.Context, query string, args ...any) (juiceSql.Result, error) {
trace.Log(ctx, "statement", stmt.Name())
trace.Log(ctx, "exec", query)
return next(ctx, query, args...)
}
}
In this example, juiceSql is an import alias for github.com/go-juicedev/juice/sql.
Register it with:
engine.Use(TraceMiddleware{})
Changing the Execution Session
Middleware that needs to route the remaining execution chain to another session can call StatementContext.WithSession.
The built-in TxSensitiveDataSourceSwitchMiddleware uses this mechanism to switch datasources for non-transactional queries.
Most application middleware does not need to change the session. Logging, metrics, tracing, and audit middleware can usually just wrap next.