Gorm Libreria ORM Database
Documentazione ufficiale: GORM - The fantastic ORM library for Golang, aims to be developer friendly.
Repository: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)
Nella comunità Go, per quanto riguarda l'interazione con il database, ci sono due scuole di pensiero: una preferisce librerie più semplici come sqlx, che non sono così potenti ma permettono di avere il controllo completo sulle SQL, ottimizzando le prestazioni al massimo. L'altra preferisce ORM nati per l'efficienza dello sviluppo, che possono risparmiare molti problemi inutili durante lo sviluppo. Quando si parla di ORM, nella comunità Go non si può assolutamente evitare gorm, che è un ORM molto storico, simile ad altri più giovani come xorm, ent, ecc. Questo articolo tratta di gorm, e qui spiegheremo solo i contenuti di base, come un punto di partenza; per maggiori dettagli si può leggere la documentazione ufficiale, la cui documentazione in cinese è già abbastanza completa, e l'autore è anche uno dei traduttori della documentazione di gorm.
Caratteristiche
- ORM completo
- Associazioni (Has One, Has Many, Belongs To, Many To Many, Polimorfismo, Ereditarietà Single-Table)
- Hook per Create, Save, Update, Delete, Find
- Supporto per Preload e Joins
- Transazioni, transazioni annidate, Save Point, Rollback To Save Point
- Context, modalità Prepared Statement, modalità DryRun
- Inserto batch, FindInBatches, Find/Create con Map, CRUD con espressioni SQL, Context Valuer
- SQL Builder, Upsert, Lock, Optimizer/Index/Comment Hint, Named Parameters, Subquery
- Chiavi primarie composte, Indici, Vincoli
- Migrazione automatica
- Logger personalizzabile
- API plugin flessibile ed estendibile: Database Resolver (multi-database, lettura/scrittura separata), Prometheus...
- Ogni funzionalità è ampiamente testata
- Friendly per gli sviluppatori
gorm ha anche alcuni svantaggi, ad esempio quasi tutti i parametri dei metodi sono di tipo interfaccia vuota, senza leggere la documentazione è difficile sapere cosa passare, a volte si può passare una struct, a volte una stringa, a volte una map, a volte una slice, la semantica è piuttosto vaga, e in molti casi è comunque necessario scrivere SQL a mano.
Come alternative ci sono due ORM da provare, il primo è aorm, open source da poco tempo, non richiede più di scrivere a mano i nomi dei campi della tabella, nella maggior parte dei casi usa operazioni a catena, basato su reflection. Dato che il numero di stelle non è elevato, si può aspettare ancora. Il secondo è ent, open source di facebook, supporta anche operazioni a catena e nella maggior parte dei casi non richiede di scrivere SQL a mano. La sua filosofia di progettazione è basata sui grafi (quello delle strutture dati), e l'implementazione è basata sulla generazione di codice piuttosto che su reflection (cosa abbastanza condivisibile), ma la documentazione è solo in inglese, quindi ha una certa curva di apprendimento.
Installazione
Installa la libreria gorm
$ go get -u gorm.io/gormConnessione
gorm supporta attualmente i seguenti database
- MySQL:
"gorm.io/driver/mysql" - PostgreSQL:
"gorm.io/driver/postgres" - SQLite:
"gorm.io/driver/sqlite" - SQL Server:
"gorm.io/driver/sqlserver" - TIDB:
"gorm.io/driver/mysql", TIDB compatibile con protocollo mysql - ClickHouse:
"gorm.io/driver/clickhouse"
Oltre a questi, ci sono altri driver di database forniti da sviluppatori terzi, come il driver oracle CengSin/oracle. Questo articolo userà MySQL per la dimostrazione, e bisogna installare il driver del database che si usa, qui installiamo il driver gorm per Mysql.
$ go get -u gorm.io/driver/mysqlPoi ci si connette al database usando dsn (data source name), la libreria del driver parserà automaticamente il dsn nella configurazione corrispondente
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log/slog"
)
func main() {
dsn := "root:123456@tcp(192.168.48.138:3306)/hello?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
slog.Error("db connect error", err)
}
slog.Info("db connect success")
}Oppure passando manualmente la configurazione
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log/slog"
)
func main() {
db, err := gorm.Open(mysql.New(mysql.Config{}))
if err != nil {
slog.Error("db connect error", err)
}
slog.Info("db connect success")
}Entrambi i metodi sono equivalenti, dipende dalle preferenze personali.
Configurazione Connessione
Passando la struct di configurazione gorm.Config, possiamo controllare alcuni comportamenti di gorm
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})Di seguito alcune semplici spiegazioni, si può configurare in base alle proprie esigenze.
type Config struct {
// Disabilita transazioni predefinite, gorm avvia transazioni per singoli create e update per mantenere la consistenza dei dati
SkipDefaultTransaction bool
// Strategia di denominazione personalizzata
NamingStrategy schema.Namer
// Salva associazioni complete
FullSaveAssociations bool
// Logger personalizzato
Logger logger.Interface
// nowfunc personalizzato, per inserire campi CreatedAt e UpdatedAt
NowFunc func() time.Time
// Genera solo SQL senza eseguire
DryRun bool
// Usa prepared statement
PrepareStmt bool
// Dopo la connessione, ping al database
DisableAutomaticPing bool
// Ignora chiavi esterne durante la migrazione del database
DisableForeignKeyConstraintWhenMigrating bool
// Ignora riferimenti alle associazioni durante la migrazione del database
IgnoreRelationshipsWhenMigrating bool
// Disabilita transazioni annidate
DisableNestedTransaction bool
// Abilita aggiornamenti globali, update senza where
AllowGlobalUpdate bool
// Query per tutti i campi della tabella
QueryFields bool
// Dimensione batch per create
CreateBatchSize int
// Abilita conversione errori
TranslateError bool
// ClauseBuilders clause builder
ClauseBuilders map[string]clause.ClauseBuilder
// ConnPool db conn pool
ConnPool ConnPool
// Dialector database dialector
Dialector
// Plugins registered plugins
Plugins map[string]Plugin
callbacks *callbacks
cacheStore *sync.Map
}Modello
In gorm, il modello corrisponde alla tabella del database, ed è solitamente rappresentato da una struct, come la struct seguente.
type Person struct {
Id uint
Name string
Address string
Mom string
Dad string
}L'interno della struct può contenere tipi di dati di base e tipi che implementano le interfacce sql.Scanner e sql.Valuer. Di default, la tabella mappata dalla struct Person si chiama persons, in stile snake_case plurale, separato da underscore. Anche i nomi delle colonne sono in stile snake_case, ad esempio Id corrisponde alla colonna id. gorm fornisce anche alcuni modi per configurarlo.
Specifica Nome Colonna
Tramite i tag della struct, possiamo specificare i nomi delle colonne per i campi della struct, in modo che gorm usi i nomi specificati durante il mapping delle entità.
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}Specifica Nome Tabella
Implementando l'interfaccia Table, si può specificare il nome della tabella, ha un solo metodo che restituisce il nome della tabella.
type Tabler interface {
TableName() string
}Nel metodo implementato, restituisce la stringa person, e durante la migrazione del database, gorm creerà una tabella chiamata person.
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}
func (p Person) TableName() string {
return "person"
}Per la strategia di denominazione, si può anche passare la propria implementazione durante la creazione della connessione per ottenere un effetto personalizzato.
Tracciamento Tempo
type Person struct {
Id uint
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}
func (p Person) TableName() string {
return "person"
}Quando sono presenti i campi CreatedAt o UpdatedAt, durante la creazione o l'aggiornamento del record, se il valore è zero, gorm imposterà automaticamente il tempo usando time.Now().
db.Create(&Person{
Name: "jack",
Address: "usa",
Mom: "lili",
Dad: "tom",
})
// INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom','2023-10-25 14:43:57.16','2023-10-25 14:43:57.16')gorm supporta anche il tracciamento timestamp
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string
Mom string
Dad string
// nanoseconds
CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
// milliseconds
UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}Allora durante l'esecuzione di Create, equivale alla seguente SQL
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)Nella situazione reale, se c'è bisogno di tracciamento temporale, è più consigliato memorizzare timestamp nel backend, il trattamento è più semplice in caso di fusi orari diversi.
Model
gorm fornisce una struct Model predefinita, che contiene il campo ID primary key, due campi di tracciamento temporale e un campo di soft delete.
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}Per usarlo basta incorporarlo nel proprio modello entità.
type Order struct {
gorm.Model
Name string
}Così avrà automaticamente tutte le caratteristiche di gorm.Model.
Primary Key
Di default, il campo chiamato Id è la primary key, si può specificare il campo primary key usando i tag della struct
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}Più campi formano una primary key composta
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}Index
Tramite il tag index della struct si può specificare l'indice della colonna
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string `gorm:"index:idx_addr,unique,sort:desc;"`
Mom string
Dad string
// nanoseconds
CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
// milliseconds
UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}Nella struct sopra, è stato creato un indice unico sul campo Address. Due campi che usano lo stesso nome di indice creano un indice composto
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string `gorm:"index:idx_addr,unique;"`
School string `gorm:"index:idx_addr,unique;"`
Mom string
Dad string
// nanoseconds
CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
// milliseconds
UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}Foreign Key
Nella struct, la relazione di foreign key è definita incorporando la struct, ad esempio
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
MomId uint
Mom Mom `gorm:"foreignKey:MomId;"`
DadId uint
Dad Dad `gorm:"foreignKey:DadId;"`
}
type Mom struct {
Id uint
Name string
Persons []Person `gorm:"foreignKey:MomId;"`
}
type Dad struct {
Id uint
Name string
Persons []Person `gorm:"foreignKey:DadId;"`
}Nell'esempio, la struct Person ha due foreign key, che riferiscono rispettivamente alle primary key delle struct Dad e Mom, di default la primary key. Person ha una relazione uno-a-uno con Dad e Mom, una persona può avere solo un padre e una madre. Dad e Mom hanno una relazione uno-a-molti con Person, perché padre e madre possono avere più figli.
Mom Mom `gorm:"foreignKey:MomId;"`Lo scopo di incorporare la struct è per specificare comodamente foreign key e riferimenti, di default il formato del campo foreign key è NomeTipoRiferito+Id, ad esempio MomId. Di default si riferisce alla primary key, tramite i tag della struct si può specificare di riferire a un certo campo
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
MomId uint
Mom Mom `gorm:"foreignKey:MomId;references:Sid;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
DadId uint
Dad Dad `gorm:"foreignKey:DadId;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
type Mom struct {
Id uint
Sid uint `gorm:"uniqueIndex;"`
Name string
Persons []Person `gorm:"foreignKey:MomId;"`
}Dove constraint:OnUpdate:CASCADE,OnDelete:SET NULL; definisce il vincolo della foreign key.
Hook
Un modello entità può definire hook personalizzati
- Create
- Update
- Delete
- Query
Le interfacce corrispondenti sono le seguenti
// Triggerato prima della creazione
type BeforeCreateInterface interface {
BeforeCreate(*gorm.DB) error
}
// Triggerato dopo la creazione
type AfterCreateInterface interface {
AfterCreate(*gorm.DB) error
}
// Triggerato prima dell'aggiornamento
type BeforeUpdateInterface interface {
BeforeUpdate(*gorm.DB) error
}
// Triggerato dopo l'aggiornamento
type AfterUpdateInterface interface {
AfterUpdate(*gorm.DB) error
}
// Triggerato prima del salvataggio
type BeforeSaveInterface interface {
BeforeSave(*gorm.DB) error
}
// Triggerato dopo il salvataggio
type AfterSaveInterface interface {
AfterSave(*gorm.DB) error
}
// Triggerato prima dell'eliminazione
type BeforeDeleteInterface interface {
BeforeDelete(*gorm.DB) error
}
// Triggerato dopo l'eliminazione
type AfterDeleteInterface interface {
AfterDelete(*gorm.DB) error
}
// Triggerato dopo la query
type AfterFindInterface interface {
AfterFind(*gorm.DB) error
}La struct può personalizzare alcuni comportamenti implementando queste interfacce.
Tag
Di seguito alcuni tag supportati da gorm
| Nome Tag | Descrizione |
|---|---|
column | Specifica il nome della colonna db |
type | Tipo di dato della colonna, si raccomandano tipi generici con buona compatibilità, ad esempio: bool, int, uint, float, string, time, bytes sono supportati da tutti i database e possono essere usati con altri tag come not null, size, autoIncrement... Specificare tipi di dato del database come varbinary(8) è supportato. Usando tipi di dato specifici del database, deve essere il tipo completo, come MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT |
serializer | Specifica il serializzatore per serializzare o deserializzare i dati nel database, ad esempio: serializer:json/gob/unixtime |
size | Definisce la dimensione o lunghezza del tipo di dato della colonna, ad esempio size: 256 |
primaryKey | Definisce la colonna come primary key |
unique | Definisce la colonna come unique key |
default | Definisce il valore predefinito della colonna |
precision | Specifica la precisione della colonna |
scale | Specifica la scala della colonna |
not null | Specifica la colonna come NOT NULL |
autoIncrement | Specifica la colonna come auto-incremento |
autoIncrementIncrement | Passo auto-incremento, controlla l'intervallo tra record consecutivi |
embedded | Campo annidato |
embeddedPrefix | Prefisso del nome della colonna per campi incorporati |
autoCreateTime | Traccia il tempo corrente durante la creazione, per campi int, traccia i secondi del timestamp, si può usare nano/milli per tracciare timestamp in nanosecondi/millisecondi, ad esempio: autoCreateTime:nano |
autoUpdateTime | Traccia il tempo corrente durante creazione/aggiornamento, per campi int, traccia i secondi del timestamp, si può usare nano/milli per tracciare timestamp in nanosecondi/millisecondi, ad esempio: autoUpdateTime:milli |
index | Crea indice in base ai parametri, più campi con lo stesso nome creano un indice composto, vedere Indici per dettagli |
uniqueIndex | Come index, ma crea un indice unico |
check | Crea vincolo check, ad esempio check:age > 13, vedere Vincoli per dettagli |
<- | Imposta i permessi di scrittura del campo, <-:create solo creazione, <-:update solo aggiornamento, <-:false nessun permesso di scrittura, <- permessi di creazione e aggiornamento |
-> | Imposta i permessi di lettura del campo, ->:false nessun permesso di lettura |
- | Ignora il campo, - significa nessuna lettura/scrittura, -:migration nessun permesso di migrazione, -:all nessun permesso di lettura/scrittura/migrazione |
comment | Aggiunge commento al campo durante la migrazione |
foreignKey | Specifica la colonna del modello corrente come foreign key nella tabella di join |
references | Specifica il nome della colonna nella tabella riferita, che sarà mappata come foreign key nella tabella di join |
polymorphic | Specifica il tipo polimorfico, ad esempio il nome del modello |
polymorphicValue | Specifica il valore polimorfico, nome tabella predefinito |
many2many | Specifica il nome della tabella di join |
joinForeignKey | Specifica il nome della colonna foreign key nella tabella di join, che sarà mappata alla tabella corrente |
joinReferences | Specifica il nome della colonna foreign key nella tabella di join, che sarà mappata alla tabella riferita |
constraint | Vincoli di relazione, ad esempio: OnUpdate, OnDelete |
Migrazione
Il metodo AutoMigrate ci aiuta con la migrazione automatica, creerà tabelle, vincoli, indici, foreign key, ecc.
func (db *DB) AutoMigrate(dst ...interface{}) errorAd esempio
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"type:varchar(100);uniqueIndex;"`
Address string
}
type Order struct {
Id uint
Name string
}
db.AutoMigrate(Person{}, Order{})
// CREATE TABLE `person` (`id` bigint unsigned AUTO_INCREMENT,`name` varchar(100),`address` longtext,PRIMARY KEY (`id`),UNIQUE INDEX `idx_person_name` (`name`))
// CREATE TABLE `orders` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))Oppure si può operare manualmente, accedendo all'interfaccia Migrator tramite il metodo Migrator
func (db *DB) Migrator() MigratorSupporta i seguenti metodi di interfaccia
type Migrator interface {
// AutoMigrate
AutoMigrate(dst ...interface{}) error
// Database
CurrentDatabase() string
FullDataTypeOf(*schema.Field) clause.Expr
GetTypeAliases(databaseTypeName string) []string
// Tables
CreateTable(dst ...interface{}) error
DropTable(dst ...interface{}) error
HasTable(dst interface{}) bool
RenameTable(oldName, newName interface{}) error
GetTables() (tableList []string, err error)
TableType(dst interface{}) (TableType, error)
// Columns
AddColumn(dst interface{}, field string) error
DropColumn(dst interface{}, field string) error
AlterColumn(dst interface{}, field string) error
MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
HasColumn(dst interface{}, field string) bool
RenameColumn(dst interface{}, oldName, field string) error
ColumnTypes(dst interface{}) ([]ColumnType, error)
// Views
CreateView(name string, option ViewOption) error
DropView(name string) error
// Constraints
CreateConstraint(dst interface{}, name string) error
DropConstraint(dst interface{}, name string) error
HasConstraint(dst interface{}, name string) bool
// Indexes
CreateIndex(dst interface{}, name string) error
DropIndex(dst interface{}, name string) error
HasIndex(dst interface{}, name string) bool
RenameIndex(dst interface{}, oldName, newName string) error
GetIndexes(dst interface{}) ([]Index, error)
}La lista dei metodi coinvolge database, tabelle, colonne, viste, indici, vincoli su più dimensioni, permettendo operazioni più raffinate per gli utenti che necessitano di personalizzazione.
Specifica Commento Tabella
Durante la migrazione, se si desidera aggiungere un commento alla tabella, si può impostare come segue
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})Va notato che se si usa il metodo AutoMigrate() per la migrazione, e le struct hanno relazioni di riferimento, gorm eseguirà una ricorsione creando prima le tabelle riferite, il che causerà commenti duplicati sia per la tabella riferita che per quella riferente, quindi si raccomanda di usare il metodo CreateTable per la creazione.
TIP
Durante la creazione della tabella, il metodo CreateTable richiede che la tabella riferita sia creata prima della tabella riferente, altrimenti si verificherà un errore, mentre il metodo AutoMigrate non ne ha bisogno perché crea ricorsivamente seguendo le relazioni di riferimento.
Creazione
Create
Nella creazione di nuovi record, nella maggior parte dei casi si usa il metodo Create
func (db *DB) Create(value interface{}) (tx *DB)Date le seguenti struct
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
}Crea un record
user := Person{
Name: "jack",
}
// Deve essere passato un riferimento
db = db.Create(&user)
// Errore durante l'esecuzione
err = db.Error
// Numero di record creati
affected := db.RowsAffectedDopo la creazione, gorm scriverà la primary key nella struct user, ecco perché deve essere passato un puntatore. Se si passa una slice, verrà creato un inserimento batch
user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Create(&user)Allo stesso modo, gorm scriverà anche le primary key nella slice. Quando il volume dei dati è troppo elevato, si può anche usare il metodo CreateInBatches per creare in batch, perché le SQL generate INSERT INTO table VALUES (),() diventeranno molto lunghe, e ogni database ha un limite sulla lunghezza SQL, quindi quando necessario si può scegliere di creare in batch.
db = db.CreateInBatches(&user, 50)Oltre a questo, il metodo Save può anche creare record, il suo scopo è aggiornare il record se la primary key corrisponde, altrimenti inserire.
func (db *DB) Save(value interface{}) (tx *DB)user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Save(&user)Upsert
Il metodo Save può solo corrispondere alla primary key, possiamo costruire una Clause per completare un upsert più personalizzato. Ad esempio questo codice
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: false,
DoUpdates: clause.AssignmentColumns([]string{"address"}),
UpdateAll: false,
}).Create(&p)Il suo scopo è aggiornare il valore del campo address quando il campo name va in conflitto, altrimenti crea un nuovo record. Si può anche non fare nulla in caso di conflitto
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Create(&p)Oppure aggiornare direttamente tutti i campi
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
UpdateAll: true,
}).Create(&p)Prima di usare upsert, ricordarsi di aggiungere un indice ai campi in conflitto.
Query
First
gorm fornisce molti metodi per le query, il primo è il metodo First
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)Il suo scopo è cercare il primo record ordinando per primary key ascendente, ad esempio
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffectedPassando un puntatore dest per consentire a gorm di mappare i dati queryati nella struct.
Oppure si possono usare i metodi Table e Model per specificare la tabella di query, il primo riceve un nome tabella stringa, il secondo un modello entità.
db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)TIP
Se il puntatore passato contiene un modello entità come puntatore a struct, o puntatore a slice di struct, allora non è necessario specificare manualmente quale tabella queryare, questa regola si applica a tutte le operazioni di creazione, eliminazione, modifica e query.
Take
Il metodo Take è simile a First, la differenza è che non ordina per primary key.
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffectedPluck
Il metodo Pluck è usato per queryare batch di una singola colonna da una tabella, i risultati possono essere raccolti in una slice del tipo specificato, non necessariamente una slice di tipo entità.
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)Ad esempio per raccogliere gli indirizzi di tutte le persone in una slice di stringhe
var adds []string
// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)In realtà è equivalente a
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)Count
Il metodo Count è usato per contare il numero di record entità
func (db *DB) Count(count *int64) (tx *DB)Vedi un esempio d'uso
var count int64
// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)Find
Il metodo più comune per query batch è Find
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)Cerca tutti i record che soddisfano le condizioni date
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)Select
gorm di default querya tutti i campi, possiamo specificare i campi tramite il metodo Select
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)Ad esempio
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)Equivalente a
db.Select([]string{"address", "name"}).First(&p)Allo stesso tempo, si può anche usare il metodo Omit per ignorare i campi
func (db *DB) Omit(columns ...string) (tx *DB)Ad esempio
// SELECT `person`.`id`,`person`.`name` FROM `person` WHERE id IN (1,2,3,4)
db.Omit("address").Where("id IN ?", []int{1, 2, 3, 4}).Find(&ps)I campi selezionati o ignorati da Select e Omit avranno effetto anche durante la creazione e l'aggiornamento delle query.
Where
Le query condizionali usano il metodo Where
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)Ecco un semplice esempio
var p Person
db.Where("id = ?", 1).First(&p)Nelle operazioni a catena, l'uso di più Where costruirà più clausole AND, ad esempio
// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)Oppure si può usare il metodo Or per costruire clausole OR
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
Or("name = ?", "jack").
Where("address = ?", "usa").
First(&p)E anche il metodo Not, sono simili
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND NOT name = 'mike' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
Or("name = ?", "jack").
Not("name = ?", "mike").
Where("address = ?", "usa").
First(&p)Per la condizione IN, si può passare direttamente una slice nel metodo Where.
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)Oppure condizioni IN multi-colonna, serve usare il tipo [][]any per contenere i parametri
// SELECT * FROM `person` WHERE (id, name, address) IN ((1,'jack','uk'),(2,'mike','usa'))
db.Where("(id, name, address) IN ?", [][]any{{1, "jack", "uk"}, {2, "mike", "usa"}}).Find(&ps)gorm supporta l'uso di gruppi where, combinando le istruzioni sopra
db.Where(
db.Where("name IN ?", []string{"cn", "uk"}).Where("id IN ?", []uint{1, 2}),
).Or(
db.Where("name IN ?", []string{"usa", "jp"}).Where("id IN ?", []uint{3, 4}),
).Find(&ps)
// SELECT * FROM `person` WHERE (name IN ('cn','uk') AND id IN (1,2)) OR (name IN ('usa','jp') AND id IN (3,4))Order
L'ordinamento usa il metodo Order
func (db *DB) Order(value interface{}) (tx *DB)Vedi un esempio d'uso
var ps []Person
// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)Si può anche chiamare più volte
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)Limit
I metodi Limit e Offset sono spesso usati per query paginate
func (db *DB) Limit(limit int) (tx *DB)
func (db *DB) Offset(offset int) (tx *DB)Ecco un semplice esempio di paginazione
var (
ps []Person
page = 2
size = 10
)
// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)Group
I metodi Group e Having sono spesso usati per operazioni di raggruppamento
func (db *DB) Group(name string) (tx *DB)
func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)Vedi un esempio
var (
ps []Person
)
// SELECT `address` FROM `person` GROUP BY `address` HAVING address IN ('cn','us')
db.Select("address").Group("address").Having("address IN ?", []string{"cn", "us"}).Find(&ps)Distinct
Il metodo Distinct è spesso usato per rimuovere duplicati
func (db *DB) Distinct(args ...interface{}) (tx *DB)Vedi un esempio
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)Subquery
Le subquery sono query annidate, ad esempio se si vogliono queryare tutte le persone con id maggiore della media
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)Subquery from
// SELECT * FROM (SELECT * FROM `person` WHERE address IN ('cn','uk')) as p
db.Table("(?) as p", db.Model(Person{}).Where("address IN ?", []string{"cn", "uk"})).Find(&ps)Lock
gorm usa la clausola clause.Locking per fornire supporto ai lock
// SELECT * FROM `person` FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&ps)
// SELECT * FROM `person` FOR SHARE NOWAIT
db.Clauses(clause.Locking{Strength: "SHARE", Options: "NOWAIT"}).Find(&ps)Iterazione
Tramite il metodo Rows si può ottenere un iteratore
func (db *DB) Rows() (*sql.Rows, error)Iterando sull'iteratore, si può usare il metodo ScanRows per scansionare ogni riga del risultato nella struct.
rows, err := db.Model(Person{}).Rows()
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var p Person
err := db.ScanRows(rows, &p)
if err != nil {
return
}
}Modifica
Save
È stato menzionato il metodo Save durante la creazione, può anche essere usato per aggiornare record, e aggiornerà tutti i campi, anche se alcuni campi della struct sono valori zero, ma se la primary key non corrisponde, eseguirà un inserimento.
var p Person
db.First(&p)
p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)Si può vedere che ha aggiunto tutti i campi tranne la primary key nella clausola SET.
Update
Quindi nella maggior parte dei casi, si consiglia di usare il metodo Update
func (db *DB) Update(column string, value interface{}) (tx *DB)È principalmente usato per aggiornare singoli campi colonna
var p Person
db.First(&p)
// UPDATE `person` SET `address`='poland' WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Update("address", "poland")Updates
Il metodo Updates è usato per aggiornare più colonne, riceve struct e map come parametri, e quando i campi della struct sono valori zero, ignora quei campi, ma nelle map no.
func (db *DB) Updates(values interface{}) (tx *DB)Ecco un esempio
var p Person
db.First(&p)
// UPDATE `person` SET `name`='jojo',`address`='poland' WHERE `id` = 2
db.Model(p).Updates(Person{Name: "jojo", Address: "poland"})
// UPDATE `person` SET `address`='poland',`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "address": "poland"})Espressioni SQL
A volte, spesso è necessario eseguire operazioni di auto-incremento o auto-decremento o calcoli con se stessi sui campi, generalmente si fa prima una query poi si calcola e si aggiorna, oppure si usano espressioni SQL.
func Expr(expr string, args ...interface{}) clause.ExprVedi l'esempio seguente
// UPDATE `person` SET `age`=age + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age + age")})
// UPDATE `person` SET `age`=age * 2 + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age * 2 + age")})Eliminazione
In gorm, per eliminare record si usa il metodo Delete, si può passare direttamente la struct entità o le condizioni.
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)Ad esempio passando direttamente la struct
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)Oppure
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)Oppure specificando condizioni
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)Si può anche semplificare in
var p Person
db.First(&p)
// DELETE FROM `person` WHERE id = 2
db.Delete(&Person{}, "id = ?", 2)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&Person{}, 2)Per l'eliminazione batch si passa una slice
// DELETE FROM `person` WHERE id IN (1,2,3)
db.Delete(&Person{}, "id IN ?", []uint{1, 2, 3})
// DELETE FROM `person` WHERE `person`.`id` IN (1,2,3)
db.Delete(&Person{}, []uint{1, 2, 3})Soft Delete
Se il tuo modello entità usa soft delete, durante l'eliminazione, di default viene eseguita un'operazione di aggiornamento, se si desidera eliminare permanentemente si può usare il metodo Unscoped
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})Definizione Associazioni
gorm fornisce capacità di interazione con associazioni di tabelle, definendo le associazioni tra struct tramite incorporamento di struct e campi.
Uno-a-Uno
La relazione uno-a-uno è la più semplice, normalmente una persona può avere solo una madre, guarda la struct seguente
type Person struct {
Id uint
Name string
Address string
Age uint
MomId sql.NullInt64
Mom Mom `gorm:"foreignKey:MomId;"`
}
type Mom struct {
Id uint
Name string
}La struct Person, incorporando la struct Mom, realizza il riferimento al tipo Mom, dove Person.MomId è il campo di riferimento, e Mom.Id è il campo riferito, completando così l'associazione uno-a-uno. Come personalizzare foreign key, riferimenti e vincoli e le regole predefinite delle foreign key sono già stati spiegati in Definizione Foreign Key, non li ripeterò
TIP
Per i campi foreign key, si raccomanda di usare i tipi forniti dal pacchetto sql, perché le foreign key di default possono essere NULL, durante la creazione di record con Create, se si usano tipi ordinari, anche il valore zero 0 verrà creato, e creare foreign key inesistenti ovviamente non è consentito.
Uno-a-Molti
Aggiungiamo ora una struct scuola, la relazione tra scuola e studente è uno-a-molti, una scuola ha più studenti, ma uno studente può frequentare solo una scuola.
type Person struct {
Id uint
Name string
Address string
Age uint
MomId sql.NullInt64
Mom Mom `gorm:"foreignKey:MomId;"`
SchoolId sql.NullInt64
School School `gorm:"foreignKey:SchoolId;"`
}
type Mom struct {
Id uint
Name string
}
type School struct {
Id uint
Name string
Persons []Person `gorm:"foreignKey:SchoolId;"`
}school.Persons è di tipo []Person, indicando che può possedere più studenti, mentre Person deve necessariamente contenere la foreign key che riferisce School, cioè Person.SchoolId.
Molti-a-Molti
Una persona può possedere molte case, e una casa può essere abitata da molte persone, questa è una relazione molti-a-molti.
type Person struct {
Id uint
Name string
Address string
Age uint
MomId sql.NullInt64
Mom Mom `gorm:"foreignKey:MomId;"`
SchoolId sql.NullInt64
School School `gorm:"foreignKey:SchoolId;"`
Houses []House `gorm:"many2many:person_house;"`
}
type Mom struct {
Id uint
Name string
}
type School struct {
Id uint
Name string
Persons []Person
}
type House struct {
Id uint
Name string
Persons []Person `gorm:"many2many:person_house;"`
}
type PersonHouse struct {
PersonId sql.NullInt64
Person Person `gorm:"foreignKey:PersonId;"`
HouseId sql.NullInt64
House House `gorm:"foreignKey:HouseId;"`
}Person e House si riferiscono a vicenda con slice dell'altro tipo per indicare la relazione molti-a-molti, le relazioni molti-a-molti generalmente richiedono la creazione di una tabella di join, si specifica la tabella di join tramite many2many, le foreign key della tabella di join devono essere specificate correttamente.
Dopo aver creato le struct, lascia che gorm faccia la migrazione automatica nel database
tables := []any{
School{},
Mom{},
Person{},
House{},
PersonHouse{},
}
for _, table := range tables {
db.Migrator().CreateTable(&table)
}Attenzione all'ordine di creazione tra tabelle riferite e riferenti.
Operazioni di Associazione
Dopo aver creato le tre relazioni di associazione sopra,接下来就是如何使用关联来进行增删改查。这主要会用到 Association 方法
func (db *DB) Association(column string) *AssociationRiceve un parametro di associazione, il suo valore dovrebbe essere il nome del campo del tipo riferito incorporato nella struct di riferimento.
db.Model(&person).Association("Mom").Find(&mom)Ad esempio per cercare la madre di una persona tramite associazione, il parametro di Association è Mom, cioè il nome del campo Person.Mom.
Creazione Associazione
// Definisci i dati
jenny := Mom{
Name: "jenny",
}
mit := School{
Name: "MIT",
Persons: nil,
}
h1 := House{
Id: 0,
Name: "h1",
Persons: nil,
}
h2 := House{
Name: "h2",
Persons: nil,
}
jack := Person{
Name: "jack",
Address: "usa",
Age: 18,
}
mike := Person{
Name: "mike",
Address: "uk",
Age: 20,
}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('jack','usa',18,NULL,NULL)
db.Create(&jack)
// INSERT INTO `schools` (`name`) VALUES ('MIT')
db.Create(&mit)
// Aggiungi associazione Person con Mom, associazione uno-a-uno
// INSERT INTO `moms` (`name`) VALUES ('jenny') ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=1 WHERE `id` = 1
db.Model(&jack).Association("Mom").Append(&jenny)
// Aggiungi associazione school con Person, associazione uno-a-molti
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('jack','usa',18,1,1,1),('mike','uk',20,NULL,1,DEFAULT) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
db.Model(&mit).Association("Persons").Append([]Person{jack, mike})
// Aggiungi associazione Person con Houses, associazione molti-a-molti
// INSERT INTO `houses` (`name`) VALUES ('h1'),('h2') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
db.Model(&jack).Association("Houses").Append([]House{h1, h2})Se tutti i record non esistono, durante la creazione dell'associazione, verranno prima creati i record e poi l'associazione.
Query Associazione
Di seguito viene dimostrato come cercare le associazioni.
// Query associazione uno-a-uno
var person Person
var mom Mom
// SELECT * FROM `people` ORDER BY `people`.`id` LIMIT 1
db.First(&person)
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
db.Model(person).Association("Mom").Find(&mom)
// Query associazione uno-a-molti
var school School
var persons []Person
// SELECT * FROM `schools` ORDER BY `schools`.`id` LIMIT 1
db.First(&school)
// SELECT * FROM `people` WHERE `people`.`school_id` = 1
db.Model(&school).Association("Persons").Find(&persons)
// Query associazione molti-a-molti
var houses []House
// SELECT `houses`.`id`,`houses`.`name` FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Model(&persons).Association("Houses").Find(&houses)La query di associazione cercherà i record che soddisfano le condizioni nella tabella riferita in base ai dati esistenti, per le relazioni molti-a-molti, gorm completerà automaticamente il processo di join delle tabelle.
Aggiornamento Associazione
Di seguito viene dimostrato come aggiornare le associazioni.
// Aggiornamento associazione uno-a-uno
var jack Person
lili := Mom{
Name: "lili",
}
// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)
// INSERT INTO `moms` (`name`) VALUES ('lili')
db.Create(&lili)
// INSERT INTO `moms` (`name`,`id`) VALUES ('lili',2) ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=2 WHERE `id` = 1
db.Model(&jack).Association("Mom").Replace(&lili)
// Aggiornamento associazione uno-a-molti
var mit School
newPerson := []Person{{Name: "bob"}, {Name: "jojo"}}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('bob','',0,NULL,NULL),('jojo','',0,NULL,NULL)
db.Create(&newPerson)
// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('bob','',0,NULL,1,4),('jojo','',0,NULL,1,5) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
// UPDATE `people` SET `school_id`=NULL WHERE `people`.`id` NOT IN (4,5) AND `people`.`school_id` = 1
db.Model(&mit).Association("Persons").Replace(newPerson)
// Aggiornamento associazione molti-a-molti
// INSERT INTO `houses` (`name`) VALUES ('h3'),('h4'),('h5') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,3),(1,4),(1,5) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` NOT IN (3,4,5)
db.Model(&jack).Association("Houses").Replace([]House{{Name: "h3"}, {Name: "h4"}, {Name: "h5"}})Durante l'aggiornamento dell'associazione, se i dati riferiti e i dati riferenti non esistono, gorm proverà a crearli.
Eliminazione Associazione
Di seguito viene dimostrato come eliminare le associazioni.
// Eliminazione associazione uno-a-uno
var (
jack Person
lili Mom
)
// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)
// SELECT * FROM `moms` WHERE name = 'lili' ORDER BY `moms`.`id` LIMIT 1
db.Where("name = ?", "lili").First(&lili)
// UPDATE `people` SET `mom_id`=NULL WHERE `people`.`id` = 1 AND `people`.`mom_id` = 2
db.Model(&jack).Association("Mom").Delete(&lili)
// Eliminazione associazione uno-a-molti
var (
mit School
persons []Person
)
// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// SELECT * FROM `people` WHERE name IN ('jack','mike')
db.Where("name IN ?", []string{"jack", "mike"}).Find(&persons)
// UPDATE `people` SET `school_id`=NULL WHERE `people`.`school_id` = 1 AND `people`.`id` IN (1,2)
db.Model(&mit).Association("Persons").Delete(&persons)
// Eliminazione associazione molti-a-molti
var houses []House
// SELECT * FROM `houses` WHERE name IN ('h3','h4')
db.Where("name IN ?", []string{"h3", "h4"}).Find(&houses)
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` IN (3,4)
db.Model(&jack).Association("Houses").Delete(&houses)L'eliminazione dell'associazione eliminerà solo la relazione di riferimento tra di esse, non eliminerà i record entità. Possiamo anche usare il metodo Clear per svuotare direttamente l'associazione
db.Model(&jack).Association("Houses").Clear()Se si desidera eliminare i record entità corrispondenti, si può aggiungere l'operazione Unscoped dopo l'operazione Association (non influenzerà many2many)
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)Per uno-a-molti e molti-a-molti, si può usare l'operazione Select per eliminare i record
var (
mit School
)
db.Where("name = ?", "mit").First(&mit)
db.Select("Persons").Delete(&mit)Preload
Il preload è usato per queryare dati associati, per entità con relazioni di associazione, caricherà prima le entità riferite associate. La query di associazione menzionata in precedenza è per queryare le relazioni di associazione, il preload querya direttamente i record entità, incluse tutte le relazioni di associazione. Dal punto di vista della sintassi, la query di associazione deve prima queryare []Person specificato, poi queryare []Mom associato in base a []Person, il preload dalla sintassi querya direttamente []Person, e caricherà anche tutte le relazioni di associazione, ma in realtà le SQL eseguite sono più o meno le stesse. Vedi un esempio
var users []Person
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)Questo è un esempio di query di associazione uno-a-uno, il suo output
[{Id:1 Name:jack Address:usa Age:18 MomId:{Int64:1 Valid:true} Mom:{Id:1 Name:jenny} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]} {Id:2 Name:mike Address:uk Age:20 MomId:{Int64:0 Valid:false} Mom:{Id:0 Name:} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]}]Si può vedere che ha queryato anche Mom associato, ma non ha preloadato School e Houses. Per preloadare più associazioni, si può usare la sintassi seguente
// SELECT * FROM `moms` WHERE `moms`.`id` IN (1,2)
// SELECT * FROM `schools` WHERE `schools`.`id` IN (1,2)
// SELECT * FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
// SELECT * FROM `people`
db.Preload("Mom").Preload("School").Preload("Houses").Find(&users)Oppure usare la notazione annidata
// SELECT * FROM `people`
// SELECT * FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Preload("Houses").Find(&users)Preload Condizionale
Si può anche applicare condizioni al preload
// SELECT * FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2) AND houses.name = 'h1'
db.Preload("Houses", "name = ?", "h1").Find(&users)Preload con Funzione
Si può anche usare una funzione per personalizzare il preload
// SELECT * FROM `houses` WHERE name IN ('h1','h2')
db.Preload("Houses", func(db *gorm.DB) *gorm.DB {
return db.Where("name IN ?", []string{"h1", "h2"})
}).Find(&users)Transazioni
gorm supporta le transazioni, che possono garantire la consistenza dei dati.
// Inizia una transazione
tx := db.Begin()
// Usa la transazione per le operazioni
// CREATE TABLE `persons` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))
err := tx.Migrator().CreateTable(&Person{})
if err != nil {
// Rollback in caso di errore
tx.Rollback()
return
}
// Commit della transazione
tx.Commit()Oppure si può usare il metodo Transaction per gestire automaticamente commit e rollback
err := db.Transaction(func(tx *gorm.DB) error {
// CREATE TABLE `persons` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))
err := tx.Migrator().CreateTable(&Person{})
if err != nil {
return err // Rollback automatico
}
return nil // Commit automatico
})Transazioni Annidate
gorm supporta anche le transazioni annidate tramite SavePoint
err := db.Transaction(func(tx *gorm.DB) error {
// Crea un savepoint
tx.SavePoint("sp1")
// Operazioni...
// Rollback al savepoint
tx.RollbackTo("sp1")
return nil
})Logger
gorm fornisce un logger personalizzabile
newLogger := log.New(os.Stdout, "\r\n", log.LstdFlags)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.New(
newLogger,
logger.Config{
SlowThreshold: time.Second, // Soglia slow SQL
LogLevel: logger.Info, // Livello log
IgnoreRecordNotFoundError: true, // Ignora ErrRecordNotFound
ParameterizedQueries: true, // Non includere parametri nel log SQL
Colorful: true, // Colori
},
),
})Plugin
gorm supporta plugin estendibili, alcuni plugin comuni includono:
- prometheus - Metriche Prometheus
- opentelemetry - Tracciamento OpenTelemetry
- soft-delete - Soft delete personalizzata
Best Practices
- Usa prepared statement - Abilita
PrepareStmtnella configurazione per riutilizzare i statement - Evita N+1 query - Usa
Preloadper caricare le associazioni in anticipo - Usa transazioni - Per operazioni multiple che devono essere atomiche
- Index appropriati - Aggiungi indici sui campi usati frequentemente nelle query
- Batch operations - Usa
CreateInBatcheseFindInBatchesper grandi volumi di dati - Context - Usa
WithContextper timeout e cancellazione
Conclusione
Gorm è un ORM potente e flessibile per Go, adatto per la maggior parte degli scenari di sviluppo. Anche se ha alcune limitazioni prestazionali rispetto all'uso diretto di SQL, la produttività che offre lo rende una scelta popolare nella comunità Go.
