Skip to content

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

sh
$ go get -u gorm.io/gorm

Connessione

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.

sh
$ go get -u gorm.io/driver/mysql

Poi ci si connette al database usando dsn (data source name), la libreria del driver parserà automaticamente il dsn nella configurazione corrispondente

go
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

go
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

go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

Di seguito alcune semplici spiegazioni, si può configurare in base alle proprie esigenze.

go
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.

go
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à.

go
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.

go
type Tabler interface {
  TableName() string
}

Nel metodo implementato, restituisce la stringa person, e durante la migrazione del database, gorm creerà una tabella chiamata person.

go
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

go
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().

go
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

go
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

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.

go
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à.

go
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

go
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

go
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

go
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

go
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

go
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.

go
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

go
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

go
// 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 TagDescrizione
columnSpecifica il nome della colonna db
typeTipo 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
serializerSpecifica il serializzatore per serializzare o deserializzare i dati nel database, ad esempio: serializer:json/gob/unixtime
sizeDefinisce la dimensione o lunghezza del tipo di dato della colonna, ad esempio size: 256
primaryKeyDefinisce la colonna come primary key
uniqueDefinisce la colonna come unique key
defaultDefinisce il valore predefinito della colonna
precisionSpecifica la precisione della colonna
scaleSpecifica la scala della colonna
not nullSpecifica la colonna come NOT NULL
autoIncrementSpecifica la colonna come auto-incremento
autoIncrementIncrementPasso auto-incremento, controlla l'intervallo tra record consecutivi
embeddedCampo annidato
embeddedPrefixPrefisso del nome della colonna per campi incorporati
autoCreateTimeTraccia 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
autoUpdateTimeTraccia 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
indexCrea indice in base ai parametri, più campi con lo stesso nome creano un indice composto, vedere Indici per dettagli
uniqueIndexCome index, ma crea un indice unico
checkCrea 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
commentAggiunge commento al campo durante la migrazione
foreignKeySpecifica la colonna del modello corrente come foreign key nella tabella di join
referencesSpecifica il nome della colonna nella tabella riferita, che sarà mappata come foreign key nella tabella di join
polymorphicSpecifica il tipo polimorfico, ad esempio il nome del modello
polymorphicValueSpecifica il valore polimorfico, nome tabella predefinito
many2manySpecifica il nome della tabella di join
joinForeignKeySpecifica il nome della colonna foreign key nella tabella di join, che sarà mappata alla tabella corrente
joinReferencesSpecifica il nome della colonna foreign key nella tabella di join, che sarà mappata alla tabella riferita
constraintVincoli di relazione, ad esempio: OnUpdate, OnDelete

Migrazione

Il metodo AutoMigrate ci aiuta con la migrazione automatica, creerà tabelle, vincoli, indici, foreign key, ecc.

go
func (db *DB) AutoMigrate(dst ...interface{}) error

Ad esempio

go
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

go
func (db *DB) Migrator() Migrator

Supporta i seguenti metodi di interfaccia

go
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

go
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

go
func (db *DB) Create(value interface{}) (tx *DB)

Date le seguenti struct

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string
}

Crea un record

go
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.RowsAffected

Dopo 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

go
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.

go
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.

go
func (db *DB) Save(value interface{}) (tx *DB)
go
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

go
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

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: true,
}).Create(&p)

Oppure aggiornare direttamente tutti i campi

go
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

go
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)

Il suo scopo è cercare il primo record ordinando per primary key ascendente, ad esempio

go
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffected

Passando 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.

go
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)
go
var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffected

Pluck

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à.

go
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

go
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

go
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)

Count

Il metodo Count è usato per contare il numero di record entità

go
func (db *DB) Count(count *int64) (tx *DB)

Vedi un esempio d'uso

go
var count int64

// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)

Find

Il metodo più comune per query batch è Find

go
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)

Cerca tutti i record che soddisfano le condizioni date

go
// 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

go
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)

Ad esempio

go
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)

Equivalente a

go
db.Select([]string{"address", "name"}).First(&p)

Allo stesso tempo, si può anche usare il metodo Omit per ignorare i campi

go
func (db *DB) Omit(columns ...string) (tx *DB)

Ad esempio

go
// 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

go
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)

Ecco un semplice esempio

go
var p Person

db.Where("id = ?", 1).First(&p)

Nelle operazioni a catena, l'uso di più Where costruirà più clausole AND, ad esempio

go
// 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

go
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)
go
// 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

go
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)
go
// 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.

go
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)

Oppure condizioni IN multi-colonna, serve usare il tipo [][]any per contenere i parametri

go
// 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

go
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

go
func (db *DB) Order(value interface{}) (tx *DB)

Vedi un esempio d'uso

go
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

go
// 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

go
func (db *DB) Limit(limit int) (tx *DB)

func (db *DB) Offset(offset int) (tx *DB)

Ecco un semplice esempio di paginazione

go
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

go
func (db *DB) Group(name string) (tx *DB)

func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)

Vedi un esempio

go
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

go
func (db *DB) Distinct(args ...interface{}) (tx *DB)

Vedi un esempio

go
// 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

go
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)

Subquery from

go
// 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

go
// 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

go
func (db *DB) Rows() (*sql.Rows, error)

Iterando sull'iteratore, si può usare il metodo ScanRows per scansionare ogni riga del risultato nella struct.

go
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.

go
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

go
func (db *DB) Update(column string, value interface{}) (tx *DB)

È principalmente usato per aggiornare singoli campi colonna

go
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.

go
func (db *DB) Updates(values interface{}) (tx *DB)

Ecco un esempio

go
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.

go
func Expr(expr string, args ...interface{}) clause.Expr

Vedi l'esempio seguente

go
// 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.

go
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)

Ad esempio passando direttamente la struct

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)

Oppure

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)

Oppure specificando condizioni

go
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)

Si può anche semplificare in

go
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

go
// 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

go
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

go
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.

go
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.

go
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

go
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 方法

go
func (db *DB) Association(column string) *Association

Riceve un parametro di associazione, il suo valore dovrebbe essere il nome del campo del tipo riferito incorporato nella struct di riferimento.

go
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

go
// 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.

go
// 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.

go
// 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.

go
// 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

go
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)

go
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

go
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

go
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

go
[{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

go
// 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

go
// 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

go
// 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

go
// 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.

go
// 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

go
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

go
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

go
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:

Best Practices

  1. Usa prepared statement - Abilita PrepareStmt nella configurazione per riutilizzare i statement
  2. Evita N+1 query - Usa Preload per caricare le associazioni in anticipo
  3. Usa transazioni - Per operazioni multiple che devono essere atomiche
  4. Index appropriati - Aggiungi indici sui campi usati frequentemente nelle query
  5. Batch operations - Usa CreateInBatches e FindInBatches per grandi volumi di dati
  6. Context - Usa WithContext per 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.

Golang by www.golangdev.cn edit