Extensions ========== Embedding Configuration Files into the Executable ------------------------------------------------- The initial way to load configuration is usually: .. code-block:: go cfg, err := juice.NewXMLConfiguration("config.xml") This works, but it means the compiled Go program still depends on an external configuration file at runtime. Since Go 1.16, the standard library includes ``embed``, which allows static files to be bundled into the executable. Juice supports this as well. .. code-block:: go package main import ( "embed" "fmt" "github.com/go-juicedev/juice" ) //go:embed config.xml var fs embed.FS func main() { cfg, err := juice.NewXMLConfigurationWithFS(fs, "config.xml") if err != nil { panic(err) } fmt.Println(cfg) } If your ``mappers`` section references other mapper files, those files must also be embedded. For example: .. code-block:: xml In that case, the best approach is to put all related configuration files into one directory: .. code-block:: text config/ ├── config.xml └── mappers.xml Then update the code like this: .. code-block:: go package main import ( "embed" "fmt" "github.com/go-juicedev/juice" ) //go:embed config var fs embed.FS func main() { cfg, err := juice.NewXMLConfigurationWithFS(fs, "config/config.xml") if err != nil { panic(err) } fmt.Println(cfg) } That solves the problem of keeping referenced mapper files together with the executable. Read-Write Splitting -------------------- Juice provides a read-write splitting mechanism to improve database performance and scalability. Configuring Multiple Datasources ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Start by defining multiple datasources in your configuration: .. code-block:: xml root:qwe123@tcp(localhost:3306)/database mysql root:qwe123@tcp(localhost:3307)/database mysql root:qwe123@tcp(localhost:3308)/database mysql By default, Juice connects only to the datasource selected by the ``default`` attribute of ``environments``. It is recommended to set ``master`` as the default for write operations. Enabling Read-Write Splitting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To enable read-write splitting, register ``TxSensitiveDataSourceSwitchMiddleware``: .. code-block:: go var engine *juice.Engine // ... engine.Use(&juice.TxSensitiveDataSourceSwitchMiddleware{}) Routing Strategies ~~~~~~~~~~~~~~~~~~ Juice supports multiple routing strategies for read operations. They can be configured either globally or per statement. Global Configuration ^^^^^^^^^^^^^^^^^^^^ Set the default routing strategy in ``settings``: .. code-block:: xml Statement-Level Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. **Explicit datasource** Read from a specific replica: .. code-block:: xml 2. **Random routing across all datasources** Use ``?`` to choose randomly from all available datasources, including the primary: .. code-block:: xml 3. **Random routing across replicas only** Use ``?!`` to choose randomly from replicas only: .. code-block:: xml Statement-level configuration has higher priority than global configuration. If a statement does not specify ``dataSource``, Juice falls back to the global ``selectDataSource`` setting. Transaction Safety ~~~~~~~~~~~~~~~~~~ This middleware is transaction-aware. If the current operation is already running inside a transaction, Juice will not switch datasources and will keep using the transaction's datasource to preserve consistency. Best Practices ~~~~~~~~~~~~~~ 1. Send all write operations to the primary datasource. 2. Use ``?!`` for read-heavy workloads to spread traffic across replicas. 3. Use explicit routing such as ``slave1`` or ``slave2`` when a specific replica is required. 4. Use ``?`` when read consistency requirements are relaxed and the primary can join load balancing. Typical Use Cases ~~~~~~~~~~~~~~~~~ Read-write splitting is especially useful when: - read traffic is significantly higher than write traffic - you need to scale read capacity - you want to reduce primary database load - you want to improve overall application performance Tracing ------- Just like read-write splitting, tracing can be added in a non-invasive way with middleware. Pseudo-code example: .. code-block:: go type TraceMiddleware struct{} func (r TraceMiddleware) QueryContext(stmt juice.Statement, cfg juice.Configuration, next juice.QueryHandler) juice.QueryHandler { return func(ctx context.Context, query string, args ...any) (sql.Rows, error) { trace.Log(ctx, "query", query) return next(ctx, query, args...) } } func (r TraceMiddleware) ExecContext(stmt juice.Statement, cfg juice.Configuration, next juice.ExecHandler) juice.ExecHandler { return func(ctx context.Context, query string, args ...any) (sql.Result, error) { trace.Log(ctx, "exec", query) return next(ctx, query, args...) } } XML Document Constraints ------------------------ DTD ~~~ XML Document Type Definition, or DTD, is a language for defining the structure and rules of XML documents. With DTD, you can constrain which elements, attributes, relationships, and ordering are allowed in an XML document. In practice, the DTD file is associated with an XML document so that editors and tooling can validate the document automatically. In XML, this is done through the ```` declaration. For Juice configuration and mapper files, you can reference the DTD like this. Configuration XML: .. code-block:: xml Mapper XML: .. code-block:: xml XSD ~~~ XML Schema Definition, or XSD, is the successor to DTD and provides a more powerful and flexible validation mechanism. Compared with DTD, XSD offers: 1. **A richer type system** - richer built-in data types - support for custom complex types - support for inheritance and extension 2. **Namespace support** - better modularity - fewer naming conflicts - easier management of large XML structures 3. **Better readability** - it is itself written in XML syntax - easier to understand and maintain - better tooling support Using XSD in Juice: .. code-block:: xml