The package-doc examples showed a fictional launcher.Register / health.Register API (and launcher.Append as a package func). Corrected to the real wiring: lc.Append + web/health.NewHandler(...).ServeHTTP. Documentation-only. No code, API, or dependency changes; version stays v1.1.0 (the v1.1.0 tag is moved to include this fix). Verified by compiling the corrected pattern against the real API.
141 lines
4.4 KiB
Markdown
141 lines
4.4 KiB
Markdown
# einherjar/db-mysql
|
|
|
|
[](https://code.nochebuena.dev/einherjar/db-mysql)
|
|
[](LICENSE)
|
|
[](https://go.dev)
|
|
[]()
|
|
|
|
> A warrior's deeds are committed to stone so that what they built survives the builder.
|
|
|
|
`code.nochebuena.dev/einherjar/db-mysql` is the MySQL/MariaDB database component of the Einherjar framework. It wraps `database/sql` with `go-sql-driver/mysql` behind a lifecycle-aware `Component`, exposes a uniform `Executor` interface for queries and transactions, and provides a `UnitOfWork` that injects the active transaction into context — so repositories never need to know whether they are inside a transaction or not.
|
|
|
|
---
|
|
|
|
## Usage
|
|
|
|
### Setup
|
|
|
|
```go
|
|
import dbmysql "code.nochebuena.dev/einherjar/db-mysql"
|
|
|
|
db := dbmysql.New(logger, dbmysql.DefaultConfig())
|
|
lc.Append(db) // OnInit opens pool; OnStop closes it
|
|
// db is observability.Checkable (PING health check, LevelCritical):
|
|
srv.Get("/health", health.NewHandler(logger, db).ServeHTTP)
|
|
```
|
|
|
|
### Querying
|
|
|
|
```go
|
|
// GetExecutor returns the pool when called outside a UnitOfWork.
|
|
rows, err := db.GetExecutor(ctx).QueryContext(ctx, "SELECT id, name FROM users WHERE active = ?", true)
|
|
defer rows.Close()
|
|
|
|
var name string
|
|
err := db.GetExecutor(ctx).QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id).Scan(&name)
|
|
```
|
|
|
|
### Manual transaction
|
|
|
|
```go
|
|
tx, err := db.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
```
|
|
|
|
`BeginTx` is available when you need explicit isolation level control:
|
|
|
|
```go
|
|
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
|
```
|
|
|
|
### Unit of work (recommended)
|
|
|
|
`UnitOfWork` wraps the transaction in context so every call to `GetExecutor` inside `uow.Do` automatically returns the active transaction. Repositories require no changes.
|
|
|
|
```go
|
|
uow := dbmysql.NewUnitOfWork(logger, db)
|
|
|
|
err := uow.Do(ctx, func(ctx context.Context) error {
|
|
_, err := db.GetExecutor(ctx).ExecContext(ctx, "INSERT INTO orders (...) VALUES (...)", ...)
|
|
return err
|
|
})
|
|
```
|
|
|
|
If the function returns an error, the transaction is rolled back. If it returns nil, the transaction is committed.
|
|
|
|
### Error handling
|
|
|
|
```go
|
|
if err := db.HandleError(someErr); err != nil {
|
|
// MySQL error numbers mapped to xerrors:
|
|
// 1062 ER_DUP_ENTRY → ErrAlreadyExists
|
|
// 1216 ER_NO_REFERENCED_ROW → ErrPreconditionFailed
|
|
// 1048 ER_BAD_NULL_ERROR → ErrInvalidInput
|
|
// context.Canceled → ErrCancelled
|
|
// context.DeadlineExceeded → ErrDeadlineExceeded
|
|
}
|
|
```
|
|
|
|
`HandleError` is also available as a package-level function: `dbmysql.HandleError(err)`.
|
|
|
|
---
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| `EINHERJAR_MYSQL_HOST` | Yes | — | MySQL host |
|
|
| `EINHERJAR_MYSQL_PORT` | No | `3306` | Listen port |
|
|
| `EINHERJAR_MYSQL_USER` | Yes | — | Database user |
|
|
| `EINHERJAR_MYSQL_PASSWORD` | Yes | — | Database password |
|
|
| `EINHERJAR_MYSQL_NAME` | Yes | — | Database name |
|
|
| `EINHERJAR_MYSQL_MAX_CONNS` | No | `5` | Maximum open connections |
|
|
| `EINHERJAR_MYSQL_MIN_CONNS` | No | `2` | Minimum idle connections |
|
|
| `EINHERJAR_MYSQL_MAX_CONN_LIFETIME` | No | `1h` | Maximum connection lifetime |
|
|
| `EINHERJAR_MYSQL_MAX_CONN_IDLE_TIME` | No | `30m` | Maximum idle time before connection is closed |
|
|
| `EINHERJAR_MYSQL_CHARSET` | No | `utf8mb4` | Connection charset |
|
|
| `EINHERJAR_MYSQL_LOC` | No | `UTC` | Timezone location |
|
|
| `EINHERJAR_MYSQL_PARSE_TIME` | No | `true` | Parse `DATE`/`DATETIME` as `time.Time` |
|
|
|
|
> **Note:** Set collation at the schema level (DDL), not in the DSN. MariaDB 11.4+ collation names exceed the 1-byte handshake limit in `go-sql-driver`.
|
|
|
|
---
|
|
|
|
## Dependency graph
|
|
|
|
```
|
|
contracts (zero dependencies)
|
|
↑
|
|
core
|
|
↑
|
|
db-mysql (contracts, core, go-sql-driver/mysql)
|
|
↑
|
|
your app
|
|
```
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
cd db-mysql/
|
|
go build ./...
|
|
go vet ./...
|
|
go test ./...
|
|
gofmt -l .
|
|
```
|
|
|
|
---
|
|
|
|
> *The hall is built before the warriors arrive.*
|
|
> *That is the only guarantee worth making.*
|