๐ค Data
Your app keeps its data in one SQLite file, reached through one
*gorm.DB. This page covers the models you write and the handle
db.Open gives you. Migrations covers changing the
schema afterwards, and Scoping covers keeping one
user's rows away from another's.
Models are plain GORM structs
No base type, no embedding, nothing to inherit:
type User struct {
ID int64
Email string `gorm:"uniqueIndex"`
PasswordHash string
}
type Note struct {
ID int64
UserID int64 `gorm:"index"`
Title string
Body string
UpdatedAt time.Time
}
UserID is the owner column. It is an ordinary field with an ordinary
index. What makes it special is that every query on Note goes through
the owner filter โ see Scoping.
Models live in internal/<app>/models.go, and a Models slice beside
them is what rastrillo migration generate diffs against your
migrations.
Opening the database
d, err := db.Open("notes.db", logger)
if err != nil {
return err
}
defer d.Close()
d.G is the *gorm.DB your handlers use. Everything else about
db.Open is the stuff that is easy to get subtly wrong, so it is worth
knowing what it did for you.
One file, two pools
Writes go through a pool capped at one connection, because SQLite allows
one writer and queueing inside the pool beats getting SQLITE_BUSY at
the call site. Reads go through a pool of several, sized to the CPU
count with a floor of four. WAL supports many readers, and serialising
them behind a single connection turns an open *sql.Rows plus any
second query into a silent deadlock.
dbresolver routes each statement to the right pool, so you never pick
one.
The DSN
?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)
busy_timeout is set before journal_mode=WAL. The other order crashes
under concurrent open. Foreign keys are on. Open also pings eagerly so
the file exists on disk from boot, which is what keeps hibernation
replication happy.
Two settings worth knowing
GORM is opened with TranslateError: true, so driver constraint errors
become GORM's portable sentinels and errors.Is(err, gorm.ErrDuplicatedKey) works. Without that flag GORM never calls the
dialector's Translate at all.
The logger ignores ErrRecordNotFound. A scoped by-id miss is the
ordinary 404 path and happens on every not-yours URL, so it is control
flow, not something worth a log line per hit.
Times are stored UTC.
Getting a *sql.DB out
Some packages want database/sql. sessions is the first one you will
hit:
writer, err := d.G.DB()
if err != nil {
return err
}
sess, err := sessions.New(sessions.Config{DB: writer, Origin: origin, Logger: logger})
d.Writer() returns the same write pool directly, and it is the clearer
call when you specifically need the writer. migrate.Apply takes it
that way, because it pins one connection for a whole run:
PRAGMA foreign_keys is per-connection, and a table rebuild has to
toggle it outside the transaction.
d.Close() closes both pools.
Never import a second SQLite driver
// Do not do this.
import _ "github.com/glebarez/sqlite"
import _ "gorm.io/driver/sqlite"
glebarez/sqlite registers the driver name sqlite, which
modernc.org/sqlite already registers. A binary containing both it and
rastrillo/gormlite panics at init, before any of your code runs, with
a duplicate-driver message that does not obviously point at either
import.
gorm.io/driver/sqlite is the cgo one, which defeats a pure-Go build.
You should not need to name a SQLite driver in your app at all.
db.Open wires gormlite over
modernc.org/sqlite for you.
Read this page as markdown โ exact, unstyled, and cheap for an agent to fetch.