Middleware
==========
What Middleware Is
------------------
In Juice, middleware is defined as an interface:
.. code-block:: go
type QueryHandler func(ctx context.Context, query string, args ...any) (sql.Rows, error)
type ExecHandler func(ctx context.Context, query string, args ...any) (sql.Result, error)
type Middleware interface {
QueryContext(stmt Statement, configuration Configuration, next QueryHandler) QueryHandler
ExecContext(stmt Statement, configuration Configuration, next ExecHandler) ExecHandler
}
Middleware lets you intercept SQL execution before it reaches the database, which makes it suitable for logging, caching, timeout control, datasource switching, and other cross-cutting concerns.
When the statement is a query, ``QueryContext`` is used. Otherwise, ``ExecContext`` is used.
Juice ships with some built-in middleware. For example:
- :class:`juice/middleware/DebugMiddleware` for printing SQL statements and timing information
.. code-block:: go
var logger = log.New(log.Writer(), "[juice] ", log.Flags())
type DebugMiddleware struct{}
func (m *DebugMiddleware) QueryContext(stmt Statement, configuration Configuration, next QueryHandler) QueryHandler {
if !m.isDeBugMode(stmt, configuration) {
return next
}
return func(ctx context.Context, query string, args ...any) (sql.Rows, error) {
start := time.Now()
rows, err := next(ctx, query, args...)
spent := time.Since(start)
logger.Printf("\x1b[33m[%s]\x1b[0m args: \u001B[34m%v\u001B[0m time: \u001B[31m%v\u001B[0m \x1b[32m%s\x1b[0m",
stmt.Name(), query, args, spent, query)
return rows, err
}
}
func (m *DebugMiddleware) ExecContext(stmt Statement, configuration Configuration, next ExecHandler) ExecHandler {
if !m.isDeBugMode(stmt, configuration) {
return next
}
return func(ctx context.Context, query string, args ...any) (sql.Result, error) {
start := time.Now()
rows, err := next(ctx, query, args...)
spent := time.Since(start)
logger.Printf("\x1b[33m[%s]\x1b[0m args: \u001B[34m%v\u001B[0m time: \u001B[31m%v\u001B[0m \x1b[32m%s\x1b[0m",
stmt.Name(), query, args, spent, query)
return rows, err
}
}
func (m *DebugMiddleware) isDeBugMode(stmt Statement, configuration Configuration) bool {
debug := stmt.Attribute("debug")
if debug == "false" {
return false
}
if configuration.Settings().Get("debug") == "false" {
return false
}
return true
}
When this middleware is enabled, Juice writes the SQL statement, bound arguments, and elapsed time to the default writer of the standard ``log`` package.
If you want to disable it globally, set ``debug`` to ``false`` in ``settings``:
.. code-block:: xml
If you want to disable it for a single action only, configure that action directly:
.. code-block:: xml
- :class:`juice/middleware/TimeoutMiddleware` for controlling SQL execution timeout
.. code-block:: go
type TimeoutMiddleware struct{}
func (t TimeoutMiddleware) QueryContext(stmt Statement, _ Configuration, next QueryHandler) QueryHandler {
timeout := t.getTimeout(stmt)
if timeout <= 0 {
return next
}
return func(ctx context.Context, query string, args ...any) (sql.Rows, error) {
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Millisecond)
defer cancel()
return next(ctx, query, args...)
}
}
func (t TimeoutMiddleware) ExecContext(stmt Statement, _ Configuration, next ExecHandler) ExecHandler {
timeout := t.getTimeout(stmt)
if timeout <= 0 {
return next
}
return func(ctx context.Context, query string, args ...any) (sql.Result, error) {
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Millisecond)
defer cancel()
return next(ctx, query, args...)
}
}
func (t TimeoutMiddleware) getTimeout(stmt Statement) (timeout int64) {
timeoutAttr := stmt.Attribute("timeout")
if timeoutAttr == "" {
return
}
timeout, _ = strconv.ParseInt(timeoutAttr, 10, 64)
return
}
To enable it, add the ``timeout`` attribute to the action tag. The unit is milliseconds:
.. code-block:: xml
.. attention::
``TimeoutMiddleware`` applies timeouts at the Go context level, not at the database server level.
Custom Middleware
-----------------
To implement custom middleware, simply implement the ``Middleware`` interface and register it on the engine:
.. code-block:: go
func main() {
var mymiddleware juice.Middleware = yourMiddlewareImpl{}
cfg, err := juice.NewXMLConfiguration("config.xml")
if err != nil {
panic(err)
}
engine, err := juice.Default(cfg)
if err != nil {
panic(err)
}
engine.Use(mymiddleware)
}