Gorm Biblioteca ORM de Banco de Dados
Documentação oficial: GORM - The fantastic ORM library for Golang, aims to be developer friendly.
Repositório: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)
Na comunidade Go, para interação com banco de dados, existem duas abordagens principais. Uma prefere bibliotecas mais simples como sqlx, que não são tão poderosas mas permitem controle total sobre SQL, otimizando desempenho ao máximo. Outra prefere ORMs que aumentam a eficiência de desenvolvimento, economizando muito tempo. Quando se fala em ORM na comunidade Go, é impossível não mencionar gorm, um ORM muito estabelecido. Outros similares incluem xorm e ent que são relativamente mais jovens. Este artigo aborda gorm, focando em conteúdo básico introdutório. Para detalhes mais profundos, consulte a documentação oficial, que já está bastante completa em chinês, e o autor é um dos tradutores da documentação do gorm.
Características
- ORM completo
- Associações (Has One, Has Many, Belongs To, Many To Many, Polimorfismo, Herança de Tabela Única)
- Hooks para Create, Save, Update, Delete, Find
- Suporte a Preload e Joins
- Transações, transações aninhadas, Save Point, Rollback To Saved Point
- Context, modo Prepared, modo DryRun
- Inserção em lote, FindInBatches, Find/Create com Map, CRUD com expressões SQL, Context Valuer
- Construtor SQL, Upsert, Lock, Optimizer/Index/Comment Hint, Named Parameters, Subqueries
- Chaves primárias compostas, índices, constraints
- Migração automática
- Logger customizável
- API de plugins flexível e extensível: Database Resolver (multi-banco, leitura/escrita separadas), Prometheus...
- Cada recurso é amplamente testado
- Amigável ao desenvolvedor
gorm tem algumas desvantagens. Quase todos os parâmetros de método são do tipo interface vazia, sem consultar a documentação é difícil saber o que passar. Às vezes aceita struct, às vezes string, às vezes map, às vezes slice. A semântica é um pouco vaga, e muitas situações ainda exigem SQL manual.
Como alternativas, há dois ORMs para experimentar. Primeiro é aorm, lançado recentemente, que não requer escrita manual de nomes de colunas, usa operações em cadeia na maioria dos casos, baseado em reflexão. Com poucas stars, pode-se esperar mais. Segundo é ent, open source do Facebook, que também suporta operações em cadeia e na maioria dos casos não requer SQL manual. Sua filosofia é baseada em grafos (da estrutura de dados), implementado via geração de código ao invés de reflexão. Mas a documentação é apenas em inglês, o que representa uma barreira.
Instalação
Instale a biblioteca gorm
$ go get -u gorm.io/gormConexão
gorm atualmente suporta os seguintes bancos de dados
- 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 é compatível com protocolo mysql - ClickHouse:
"gorm.io/driver/clickhouse"
Além disso, existem outros drivers de banco de dados fornecidos por desenvolvedores de terceiros, como o driver oracle CengSin/oracle. Este artigo usará MySQL para demonstração. O banco de dados usado determina qual driver instalar. Aqui instalamos o driver gorm para MySQL.
$ go get -u gorm.io/driver/mysqlEntão conecte ao banco de dados usando dsn (data source name). O driver irá parsear o dsn para as configurações correspondentes.
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")
}Ou passando configuração manualmente
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")
}Ambos os métodos são equivalentes, depende da preferência de uso.
Configuração de Conexão
Através da estrutura de configuração gorm.Config, podemos controlar alguns comportamentos do gorm.
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})Abaixo estão algumas explicações simples. Use conforme suas necessidades.
type Config struct {
// Desabilita transação padrão, gorm inicia transação em criação e atualização única para manter consistência de dados
SkipDefaultTransaction bool
// Estratégia de nomenclatura customizada
NamingStrategy schema.Namer
// Salva associações completas
FullSaveAssociations bool
// Logger customizado
Logger logger.Interface
// Customiza nowfunc, usado para injetar campos CreatedAt e UpdatedAt
NowFunc func() time.Time
// Gera apenas SQL sem executar
DryRun bool
// Usa prepared statements
PrepareStmt bool
// Faz ping no banco de dados após estabelecer conexão
DisableAutomaticPing bool
// Ignora foreign keys ao migrar banco de dados
DisableForeignKeyConstraintWhenMigrating bool
// Ignora referências de relacionamento ao migrar banco de dados
IgnoreRelationshipsWhenMigrating bool
// Desabilita transações aninhadas
DisableNestedTransaction bool
// Permite atualização global, update sem where
AllowGlobalUpdate bool
// Consulta todos os campos da tabela
QueryFields bool
// Tamanho do batch para criação em lote
CreateBatchSize int
// Habilita conversão de erro
TranslateError bool
// ClauseBuilders construtor de cláusulas
ClauseBuilders map[string]clause.ClauseBuilder
// ConnPool pool de conexão db
ConnPool ConnPool
// Dialector dialector do banco de dados
Dialector
// Plugins plugins registrados
Plugins map[string]Plugin
callbacks *callbacks
cacheStore *sync.Map
}Modelos
Em gorm, modelos correspondem a tabelas de banco de dados, geralmente representados por structs. Por exemplo:
type Person struct {
Id uint
Name string
Address string
Mom string
Dad string
}Os campos do struct podem conter tipos de dados básicos e tipos que implementam as interfaces sql.Scanner e sql.Valuer. Por padrão, a tabela mapeada para o struct Person é persons, em estilo snake case plural. Os nomes das colunas também são em snake case, por exemplo Id corresponde à coluna id. gorm também fornece maneiras de configurar isso.
Especificar Nome da Coluna
Através de tags de struct, podemos especificar nomes de colunas para campos.
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}Especificar Nome da Tabela
Implementando a interface Tabler, podemos especificar o nome da tabela.
type Tabler interface {
TableName() string
}No método implementado, retorna a string person. Durante a migração do banco de dados, gorm criará uma tabela chamada 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"
}Para estratégia de nomenclatura, também é possível passar sua própria implementação ao criar a conexão.
Rastreamento de 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 contém campos CreatedAt ou UpdatedAt, ao criar ou atualizar registros, se forem zero, gorm automaticamente usará time.Now() para definir o tempo.
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 também suporta rastreamento de 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;"`
}Então ao executar Create, equivale ao seguinte SQL
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)Na prática, se houver necessidade de rastreamento de tempo, é mais recomendado armazenar timestamps no backend, o que simplifica o tratamento em casos de fusos horários diferentes.
Model
gorm fornece uma estrutura Model predefinida, que contém chave primária ID, dois campos de rastreamento de tempo e um campo de soft delete.
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}Para usar, basta incorporar ao seu modelo de entidade.
type Order struct {
gorm.Model
Name string
}Assim terá automaticamente todas as características de gorm.Model.
Chave Primária
Por padrão, o campo chamado Id é a chave primária. Use tags de struct para especificar campos de chave primária.
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}Múltiplos campos formam chaves primárias compostas
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}Índices
Através da tag index podemos especificar índices de coluna
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;"`
}No struct acima, foi criado um índice único no campo Address. Dois campos usando o mesmo nome de índice criam um índice 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;"`
}Chaves Estrangeiras
Em structs, relações de chave estrangeira são definidas através de incorporação de structs. Por exemplo:
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;"`
}No exemplo, o struct Person tem duas chaves estrangeiras, referenciando as chaves primárias dos structs Dad e Mom. Por padrão, referencia a chave primária. Person tem relação um-para-um com Dad e Mom, uma pessoa só pode ter um pai e uma mãe. Dad e Mom têm relação um-para-muitos com Person, pois pais podem ter múltiplos filhos.
Mom Mom `gorm:"foreignKey:MomId;"`A incorporação de structs facilita a especificação de chaves estrangeiras e referências. Por padrão, o formato do campo de chave estrangeira é NomeDoTipoReferenciado+Id, como MomId. Por padrão referencia a chave primária. Através de tags de struct pode-se especificar referência a outro 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;"`
}Onde constraint:OnUpdate:CASCADE,OnDelete:SET NULL; define a constraint de chave estrangeira.
Hooks
Um modelo de entidade pode definir hooks customizados
- Create
- Update
- Delete
- Query
As interfaces correspondentes são:
// Disparado antes de criar
type BeforeCreateInterface interface {
BeforeCreate(*gorm.DB) error
}
// Disparado depois de criar
type AfterCreateInterface interface {
AfterCreate(*gorm.DB) error
}
// Disparado antes de atualizar
type BeforeUpdateInterface interface {
BeforeUpdate(*gorm.DB) error
}
// Disparado depois de atualizar
type AfterUpdateInterface interface {
AfterUpdate(*gorm.DB) error
}
// Disparado antes de salvar
type BeforeSaveInterface interface {
BeforeSave(*gorm.DB) error
}
// Disparado depois de salvar
type AfterSaveInterface interface {
AfterSave(*gorm.DB) error
}
// Disparado antes de deletar
type BeforeDeleteInterface interface {
BeforeDelete(*gorm.DB) error
}
// Disparado depois de deletar
type AfterDeleteInterface interface {
AfterDelete(*gorm.DB) error
}
// Disparado depois de buscar
type AfterFindInterface interface {
AfterFind(*gorm.DB) error
}Structs podem implementar estas interfaces para customizar comportamentos.
Tags
Abaixo estão algumas tags suportadas pelo gorm
| Nome da Tag | Descrição |
|---|---|
column | Especifica nome da coluna no banco de dados |
type | Tipo de dados da coluna, recomenda-se usar tipos genéricos com boa compatibilidade, como: bool, int, uint, float, string, time, bytes suportados por todos os bancos de dados. Pode ser usado com outras tags como not null, size, autoIncrement... Especificar tipo de dados do banco como varbinary(8) também é suportado |
serializer | Especifica serializador para serializar/desserializar dados no banco de dados, ex: serializer:json/gob/unixtime |
size | Define tamanho/length do tipo de dados da coluna, ex size: 256 |
primaryKey | Define coluna como chave primária |
unique | Define coluna como chave única |
default | Define valor padrão da coluna |
precision | Especifica precisão da coluna |
scale | Especifica escala da coluna |
not null | Especifica coluna como NOT NULL |
autoIncrement | Especifica coluna como auto incremento |
autoIncrementIncrement | Passo de auto incremento, controla intervalo entre registros consecutivos |
embedded | Campo embutido |
embeddedPrefix | Prefixo do nome da coluna para campos embutidos |
autoCreateTime | Rastreia tempo atual na criação, para campos int, rastreia timestamp em segundos, pode usar nano/milli para nanossegundos/milissegundos, ex: autoCreateTime:nano |
autoUpdateTime | Rastreia tempo atual na criação/atualização, para campos int, rastreia timestamp em segundos, pode usar nano/milli para nanossegundos/milissegundos, ex: autoUpdateTime:milli |
index | Cria índice com parâmetros, múltiplos campos com mesmo nome criam índice composto |
uniqueIndex | Igual a index, mas cria índice único |
check | Cria constraint check, ex check:age > 13 |
<- | Define permissão de escrita do campo, <-:create apenas criação, <-:update apenas atualização, <-:false sem permissão de escrita, <- permissão de criação e atualização |
-> | Define permissão de leitura do campo, ->:false sem permissão de leitura |
- | Ignora campo, - sem leitura/escrita, -:migration sem permissão de migração, -:all sem permissão de leitura/escrita/migração |
comment | Adiciona comentário ao campo durante migração |
foreignKey | Especifica coluna do modelo atual como chave estrangeira na tabela de junção |
references | Especifica nome da coluna na tabela referenciada, mapeada como chave estrangeira na tabela de junção |
polymorphic | Especifica tipo polimórfico, como nome do modelo |
polymorphicValue | Especifica valor polimórfico, nome da tabela padrão |
many2many | Especifica nome da tabela de junção |
joinForeignKey | Especifica nome da coluna de chave estrangeira na tabela de junção, mapeada para tabela atual |
joinReferences | Especifica nome da coluna de chave estrangeira na tabela de junção, mapeada para tabela referenciada |
constraint | Constraints de relacionamento, ex: OnUpdate, OnDelete |
Migração
O método AutoMigrate nos ajuda com migração automática, criando tabelas, constraints, índices, chaves estrangeiras, etc.
func (db *DB) AutoMigrate(dst ...interface{}) errorPor exemplo
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`))Ou podemos operar manualmente, acessando a interface Migrator através do método Migrator
func (db *DB) Migrator() MigratorSuporta os seguintes métodos de interface
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)
}A lista de métodos envolve banco de dados, tabelas, colunas, views, índices, constraints em múltiplas dimensões, permitindo operações mais refinadas para usuários que precisam de customização.
Especificar Comentário da Tabela
Durante migração, se quiser adicionar comentário à tabela, pode definir assim
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})Note que se usar AutoMigrate() para migração, e os structs tiverem relações de referência, gorm fará migração recursiva primeiro criando tabelas referenciadas, o que fará com que comentários de tabelas referenciadas e referenciadoras sejam duplicados. Então é recomendado usar o método CreateTable para criar.
TIP
Ao criar tabelas, o método CreateTable requer que a tabela referenciada seja criada antes da tabela referenciadora, caso contrário ocorrerá erro. O método AutoMigrate não requer isso, pois segue a relação de referência recursivamente.
Criação
Create
Na maioria dos casos ao criar novos registros, usamos o método Create
func (db *DB) Create(value interface{}) (tx *DB)Tendo o seguinte struct
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
}Criando um registro
user := Person{
Name: "jack",
}
// Deve passar ponteiro
db = db.Create(&user)
// Erro ocorrido durante execução
err = db.Error
// Número de registros criados
affected := db.RowsAffectedApós criação, gorm escreverá a chave primária no struct user, por isso é necessário passar ponteiro. Se passar um slice, criará em lote
user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Create(&user)Da mesma forma, gorm escreverá as chaves primárias no slice. Quando volume de dados é grande, pode usar CreateInBatches para criar em lotes, pois o SQL gerado INSERT INTO table VALUES (),() ficará muito longo, e cada banco de dados tem limite de tamanho SQL. Então quando necessário, pode criar em lotes.
db = db.CreateInBatches(&user, 50)Além disso, o método Save também pode criar registros. Sua função é atualizar o registro quando a chave primária corresponder, caso contrário insere.
func (db *DB) Save(value interface{}) (tx *DB)user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Save(&user)Upsert
O método Save só corresponde à chave primária. Podemos construir Clause para upsert mais customizado. Por exemplo
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: false,
DoUpdates: clause.AssignmentColumns([]string{"address"}),
UpdateAll: false,
}).Create(&p)Isso atualiza o valor do campo address quando o campo name conflitar, caso contrário cria um novo registro. Também pode fazer nada quando conflitar
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Create(&p)Ou atualizar todos os campos diretamente
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
UpdateAll: true,
}).Create(&p)Antes de usar upsert, lembre-se de adicionar índice aos campos de conflito.
Consulta
First
gorm fornece muitos métodos para consulta. O primeiro é First
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)Busca o primeiro registro ordenado por chave primária ascendente. Por exemplo
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffectedPassa ponteiro dest para facilitar mapeamento dos dados consultados ao struct.
Ou usa métodos Table e Model para especificar tabela de consulta. O primeiro recebe string com nome da tabela, o segundo recebe modelo de entidade.
db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)TIP
Se o ponteiro passado contiver modelo de entidade como ponteiro de struct ou ponteiro de slice de struct, não precisa especificar manualmente qual tabela consultar. Esta regra se aplica a todas as operações de CRUD.
Take
O método Take é similar ao First, diferença é que não ordena por chave primária.
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffectedPluck
O método Pluck é usado para consultar uma única coluna de uma tabela em lote. O resultado pode ser coletado em um slice de tipo especificado, não necessariamente slice de tipo de entidade.
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)Por exemplo, coletar endereços de todas as pessoas em um slice de string
var adds []string
// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)Equivalente a
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)Count
O método Count é usado para contar número de registros de entidade
func (db *DB) Count(count *int64) (tx *DB)Exemplo de uso
var count int64
// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)Find
O método mais comum para consulta em lote é Find
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)Busca todos os registros que correspondem às condições dadas
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)Select
gorm por padrão consulta todos os campos. Podemos usar Select para especificar campos
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)Por exemplo
// 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)Também pode usar método Omit para ignorar campos
func (db *DB) Omit(columns ...string) (tx *DB)Por exemplo
// 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)Campos selecionados ou ignorados por Select e Omit também funcionam em consultas de criação e atualização.
Where
Consulta condicional usa método Where
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)Exemplo simples
var p Person
db.Where("id = ?", 1).First(&p)Em operações em cadeia, múltiplos Where constroem múltiplas cláusulas AND
// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)Ou usa método Or para construir cláusulas 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 método Not, similares
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)Para condição IN, pode passar slice diretamente no Where
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)Ou condição IN multi-coluna, precisa usar tipo [][]any para parâmetros
// 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 suporta agrupamento de where, combinando as declarações acima
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
Ordenação usa método Order
func (db *DB) Order(value interface{}) (tx *DB)Exemplo de uso
var ps []Person
// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)Também pode chamar múltiplas vezes
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)Limit
Métodos Limit e Offset são frequentemente usados para paginação
func (db *DB) Limit(limit int) (tx *DB)
func (db *DB) Offset(offset int) (tx *DB)Exemplo simples de paginação
var (
ps []Person
page = 2
size = 10
)
// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)Group
Métodos Group e Having são usados para operações de agrupamento
func (db *DB) Group(name string) (tx *DB)
func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)Exemplo
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
Método Distinct é usado para remover duplicatas
func (db *DB) Distinct(args ...interface{}) (tx *DB)Exemplo
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)Subqueries
Subquery é query aninhada. Por exemplo, para buscar todas as pessoas com id maior que a média
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)from subquery
// 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 cláusula clause.Locking para fornecer suporte a locks
// 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)Iteração
Através do método Rows podemos obter um iterador
func (db *DB) Rows() (*sql.Rows, error)Iterando sobre o iterador, usa método ScanRows para escanear cada linha para o 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ção
Save
Mencionado em criação, método Save também pode atualizar registros. Atualiza todos os campos, mesmo que alguns campos do struct sejam zero. Mas se a chave primária não corresponder, fará inserção.
var p Person
db.First(&p)
p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)Vê-se que coloca todos os campos exceto chave primária na cláusula SET.
Update
Na maioria dos casos, é recomendado usar método Update
func (db *DB) Update(column string, value interface{}) (tx *DB)Principalmente usado para atualizar campo único
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
Método Updates é usado para atualizar múltiplas colunas, recebe struct e map como parâmetros. Quando campos do struct são zero, ignora o campo, mas em map não.
func (db *DB) Updates(values interface{}) (tx *DB)Exemplo
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"})Expressões SQL
Às vezes, frequentemente precisamos fazer operações como incremento ou decremento em campos, geralmente primeiro busca, depois calcula e atualiza, ou usa expressões SQL.
func Expr(expr string, args ...interface{}) clause.ExprExemplo
// 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")})Deleção
Em gorm, deleção de registros usa método Delete, pode passar struct de entidade ou condições.
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)Por exemplo, passando struct diretamente
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)Ou
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)Ou especificando condições
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)Também pode simplificar para
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)Para deleção em lote, passa 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 seu modelo de entidade usar soft delete, ao deletar, por padrão faz operação de atualização. Para deleção permanente, pode usar método Unscoped
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})Definição de Associação
gorm fornece capacidade de interação de associação entre tabelas, definindo associações entre structs através de incorporação de structs e campos.
Um para Um
Relação um-para-um é a mais simples. Normalmente uma pessoa só pode ter uma mãe. Veja o struct abaixo
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
}Struct Person referencia tipo Mom através de incorporação do struct Mom. Onde Person.MomId é campo de referência, chave primária Mom.Id é campo referenciado, completando associação um-para-um. Como customizar chave estrangeira, referência, constraints e regras padrão de chave estrangeira já foram explicados em Definição de Chave Estrangeira, não será repetido.
TIP
Para campos de chave estrangeira, é recomendado usar tipos fornecidos pelo pacote sql, pois chaves estrangeiras podem ser NULL por padrão. Ao usar Create para criar registros, se usar tipo comum, valor zero 0 também será criado, o que não é permitido para chaves estrangeiras inexistentes.
Um para Muitos
Adicionando struct School, relação escola e estudante é um-para-muitos. Uma escola tem múltiplos estudantes, mas um estudante só pode estudar em uma escola.
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 é tipo []Person, indicando que pode possuir múltiplos estudantes. Person deve conter chave estrangeira referenciando School, que é Person.SchoolId.
Muitos para Muitos
Uma pessoa pode ter muitas casas, uma casa pode acomodar muitas pessoas. Esta é uma relação muitos-para-muitos.
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 mantêm slices um do outro indicando relação muitos-para-muitos. Relação muitos-para-muitos geralmente requer criação de tabela de junção, especificada através de many2many. Chaves estrangeiras da tabela de junção devem estar corretas.
Após criar structs, deixe gorm migrar automaticamente para o banco de dados
tables := []any{
School{},
Mom{},
Person{},
House{},
PersonHouse{},
}
for _, table := range tables {
db.Migrator().CreateTable(&table)
}Note a ordem de criação entre tabelas referenciadas e referenciadoras.
Operações de Associação
Após criar as três relações de associação acima,接下来 é como usar associações para CRUD. Principalmente usa método Association
func (db *DB) Association(column string) *AssociationRecebe um parâmetro de associação, seu valor deve ser nome do campo do tipo referenciado no struct de referência incorporado.
db.Model(&person).Association("Mom").Find(&mom)Por exemplo, buscar mãe de uma pessoa, parâmetro de Association é Mom, nome do campo Person.Mom.
Criar Associação
// Definir dados
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)
// Adicionar associação Person com Mom, associação um-para-um
// 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)
// Adicionar associação school com Person, associação um-para-muitos
// 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})
// Adicionar associação Person com Houses, associação muitos-para-muitos
// 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 todos os registros não existirem, ao criar associação, também criará registros primeiro e depois associação.
Buscar Associação
Abaixo demonstra como buscar associações.
// Busca associação um-para-um
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)
// Busca associação um-para-muitos
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)
// Busca associação muitos-para-muitos
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)Busca de associação buscará registros que correspondem às condições na tabela referenciada com base nos dados existentes. Para relação muitos-para-muitos, gorm completará automaticamente o processo de junção de tabelas.
Atualizar Associação
Abaixo demonstra como atualizar associações.
// Atualização de associação um-para-um
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)
// Atualização de associação um-para-muitos
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)
// Atualização de associação muitos-para-muitos
// 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"}})Ao atualizar associação, se dados referenciados e referenciadores não existirem, gorm tentará criá-los.
Deletar Associação
Abaixo demonstra como deletar associações.
// Deleção de associação um-para-um
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)
// Deleção de associação um-para-muitos
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)
// Deleção de associação muitos-para-muitos
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)Deleção de associação apenas remove relação de referência entre eles, não deleta registros de entidade. Também podemos usar método Clear para limpar associação diretamente
db.Model(&jack).Association("Houses").Clear()Se quiser deletar registros de entidade correspondentes, pode adicionar operação Unscoped após operação Association (não afeta many2many)
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)Para um-para-muitos e muitos-para-muitos, pode usar operação Select para deletar registros
var (
mit School
)
db.Where("name = ?", "mit").First(&mit)
db.Select("Persons").Delete(&mit)Preload
Preload é usado para buscar dados de associação. Para entidades com relações de associação, primeiro carrega entidades referenciadas associadas. Query de associação mencionada anteriormente é para buscar relações de associação, preload é para buscar diretamente registros de entidade, incluindo todas as relações de associação. Sintaticamente, query de associação precisa primeiro buscar []Person especificado, depois buscar []Mom associado baseado em []Person. Preload sintaticamente busca diretamente []Person, e também carrega todas as relações de associação. Mas na prática, o SQL executado é similar. Veja exemplo
var users []Person
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)Este é exemplo de query de associação um-para-um. Sua saída
[{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:[]}]Vê-se que também buscou Mom associado, mas como não fez preload da relação escolar, structs School são todos zero. Também pode usar clause.Associations para indicar preload de todas as relações, exceto relações aninhadas.
db.Preload(clause.Associations).Find(&users)Abaixo exemplo de preload aninhado. Busca todos os estudantes de todas as associações escolares e mães associadas a cada estudante e casas possuídas por cada estudante, e também conjunto de donos de cada casa. Escola->Estudante->Casa->Estudante.
var schools []School
db.Preload("Persons").
Preload("Persons.Mom").
Preload("Persons.Houses").
Preload("Persons.Houses.Persons").Find(&schools)
// Código de saída, lógica pode ser ignorada
for _, school := range schools {
fmt.Println("school", school.Name)
for _, person := range school.Persons {
fmt.Println("person", person.Name)
fmt.Println("mom", person.Mom.Name)
for _, house := range person.Houses {
var persons []string
for _, p := range house.Persons {
persons = append(persons, p.Name)
}
fmt.Println("house", house.Name, "owner", persons)
}
fmt.Println()
}
}Saída
school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]
person mike
momVê-se que outputou mãe de cada estudante de cada escola e suas casas, e todos donos das casas.
Transações
gorm habilita transações por padrão. Qualquer inserção e atualização falhará e fará rollback. Pode desabilitar em Configuração de Conexão, desempenho aumenta cerca de 30%. Uso de transações em gorm tem múltiplos métodos, abaixo introdução simples.
Automático
Transação em closure, através do método Transaction, passa uma função closure. Se valor de retorno não for nil, faz rollback automaticamente.
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)Exemplo
var ps []Person
db.Transaction(func(tx *gorm.DB) error {
err := tx.Create(&ps).Error
if err != nil {
return err
}
err = tx.Create(&ps).Error
if err != nil {
return err
}
err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
return err
}
return nil
})Manual
É mais recomendado usar transação manual, controlamos nós mesmos quando fazer rollback, quando commit. Transação manual usa três métodos abaixo
// Método Begin usado para iniciar transação
func (db *DB) Begin(opts ...*sql.TxOptions) *DB
// Método Rollback usado para reverter transação
func (db *DB) Rollback() *DB
// Método Commit usado para commitar transação
func (db *DB) Commit() *DBExemplo
var ps []Person
tx := db.Begin()
err := tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
err = tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
tx.Rollback()
return
}
tx.Commit()Pode especificar ponto de rollback
var ps []Person
tx := db.Begin()
err := tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
tx.SavePoint("createBatch")
err = tx.Create(&ps).Error
if err != nil {
tx.Rollback()
return
}
err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
tx.RollbackTo("createBatch")
return
}
tx.Commit()Resumo
Se leu todo conteúdo acima e executou o código, então pode usar gorm para CRUD de banco de dados. Além destas operações, gorm tem muitas outras funcionalidades. Mais detalhes podem ser encontrados na documentação oficial.
