Database providers and client managers
Reference | Relational Databases
Database provider
A database provider is the name Granitic gives to a component in your application which:
- Is responsible for importing the correct database driver into your application
- Allows your code to implement advanced connection management if needed
- Allows your code to provide specific alternative to some driver specific behaviour
You need a database provider if you want to connect to a relational (SQL) database from your Granitic application.
Minimal implementation
For the most common use case where your application will connect to a single instance of a database you must provide a single component which implements rdbms.DatabaseProvider:
type DatabaseProvider interface {
Database() (*sql.DB, error)
}
A minimal implementation (for connecting to a MySQL database), might look like:
package db
import (
"database/sql"
"github.com/go-sql-driver/mysql"
"github.com/graniticio/granitic/v2/logging"
)
type MySQLProvider struct {
Config *mysql.Config
Log logging.Logger
db *sql.DB
}
func (p *MySQLProvider) Database() (*sql.DB, error) {
dsn := p.Config.FormatDSN()
if p.db != nil {
return p.db, nil
}
if db, err := sql.Open("mysql", dsn); err == nil {
return db, nil
} else {
p.Log.LogErrorf("Unable to open connection to MySQL database: %v", err)
return nil, err
}
}
with a corresponding component definition:
"dbProvider": {
"type": "db.MySQLProvider",
"Config": {
"type": "mysql.Config",
"User": "$database.user",
"Passwd": "$database.password",
"Addr": "$database.host",
"DBName": "$database.instance",
"AllowNativePasswords": true
}
}
Caching the DB object
It is important to note that the *sql.DB object returned by your provider is a ‘database handle representing a pool of zero or more underlying connections. It’s safe for concurrent use by multiple goroutines’.
Generally your DatabaseProvider
should only create a DB instance once, store it in a member variable and then
return the cached *sql.DB
on subsequent requests (as in the above example).
Context aware provider
Your provider may optionally implement rdbms.ContextAwareDatabaseProvider:
type ContextAwareDatabaseProvider interface {
DatabaseFromContext(context.Context) (*sql.DB, error)
}
Having access to the context allows your provider implementation to alter its behaviour based on the contents of the context. For example, you might use a different database shard depending on some user ID stored in the context.
Blocking startup until connected
Refer to the RdbmsAccess facility documentation on how to delay application startup until a database connection has been established.
IDs on insert
A common pattern is for a SQL insert query to rely on a database specific mechanism (an auto-increment or a sequence) to create a new numeric primary key (or unique ID) when inserting a row. It is often vital for your application code to know what that new ID is, but the implementation for recovering that ID is not standardised across Go SQL drivers.
By default Granitic uses the function rdbms.DefaultInsertWithReturnedID
to return the newly created ID (which relies on the LastInsertId
function on sql.Result).
If this doesn’t work for your RDBMS, your database provider should implement rdbms.NonStandardInsertProvider:
type NonStandardInsertProvider interface {
InsertIDFunc() InsertWithReturnedID
}
Client managers
A client manager is a Granitic system component that is injected automatically into any component of yours that has a field:
DBClientManager rdbms.ClientManager
once you enable the RdbmsAccess facility and have a component that implements rdbms.DatabaseProvider
At runtime, your code can call the Client
or ClientFromContext
method on the client manager to obtain an
rbms.Client which is the (non-goroutine safe)
interface that your code will use to execute queries, the usage of which is covered in executing queries.
Working with multiple databases
If your application connects to more than one database, configuration is more complex. For each database you want to connect to, you will need:
- A component implementing rdbms.DatabaseProvider
- A component that is an instance of rdbms.ClientManagerConfig
The ClientManagerConfig
component defines:
- A reference to the database provider to use
- The names of fields on your components that are of type
ClientManager
and will receive this ClientManager - A logical name for the client manager
Example
"catalogueDBProvider": {
"type": "db.MySQLProvider",
"Config": {
"type": "mysql.Config",
"User": "$database.catalogue.user",
"Passwd": "$database.catalogue.password",
"Addr": "$database.catalogue.host",
"DBName": "$database.catalogue.instance",
"AllowNativePasswords": true
}
},
"ordersDBProvider": {
"type": "db.MySQLProvider",
"Config": {
"type": "mysql.Config",
"User": "$database.orders.user",
"Passwd": "$database.orders.password",
"Addr": "$database.orders.host",
"DBName": "$database.orders.instance",
"AllowNativePasswords": true
}
},
"catalogueManager": {
"type": "rdbms.ClientManagerConfig",
"Provider": "+catalogueDBProvider",
"InjectFieldNames": ["CatalogueClientManager"],
"BlockUntilConnected": true
},
"ordersManager": {
"type": "rdbms.ClientManagerConfig",
"Provider": "+ordersDBProvider",
"InjectFieldNames": ["OrdersClientManager", "Orders"],
"BlockUntilConnected": true
}
This example shows an application that connects to two different databases. Because they are the same type of database (MySQL in this instance), the providers can re-use the same underlying implementation.
Two different client managers will be created. Each has a reference to a specific database provider and defines the name(s)
of the fields that the managers will be auto-injected into (as long as the field is of type ClientManager
).
Both client managers are configured to block application startup until a successful database connection is established.
Next: Query management
Prev: Principles