Skip to content

Gorm 데이터베이스 ORM 라이브러리

공식 문서: GORM - The fantastic ORM library for Golang, aims to be developer friendly.

오픈소스 저장소: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)

Go 커뮤니티에서 데이터베이스 상호작용 부분에 대해 두 가지 파벌이 있습니다. 한 파벌은 간결한 sqlx와 같은 라이브러리를 선호하며 기능이 그렇게 강력하지는 않지만 SQL 을 항상 통제할 수 있고 성능을 극단적으로 최적화할 수 있습니다. 다른 한 파벌은 개발 효율성을 위해 ORM 을 선호하며 개발 과정에서 많은 불필요한 번거로움을省去할 수 있습니다. ORM 을 언급하면 Go 언어 커뮤니티에서 gorm을 절대 빼놓을 수 없으며 이는 매우 역사가 깊은 ORM 으로 비교적 젊은 xorm, ent등과 유사합니다. 이 글에서는 gorm 에 대한 내용을 다루며 이는 기초 입문 내용에 대한 설명일 뿐이며 더 깊은 세부 사항을 알고 싶다면 공식 문서를 읽을 수 있습니다. 중국어 문서는 이미 상당히 완성도가 높으며 필자도 gorm 문서 번역진 중 하나입니다.

특징

  • 전체 기능 ORM
  • 연관관계 (하나 소유, 여러 개 소유, 속함, 다대다, 다형성, 단일 테이블 상속)
  • Create, Save, Update, Delete, Find 에서 훅 메서드
  • Preload, Joins 를 사용한 사전 로딩 지원
  • 트랜잭션, 중첩 트랜잭션, Save Point, Rollback To Saved Point
  • Context, 사전 컴파일 모드, DryRun 모드
  • 배치 삽입, FindInBatches, Find/Create with Map, SQL 표현식, Context Valuer 를 사용한 CRUD
  • SQL 빌더, Upsert, 잠금, Optimizer/Index/Comment Hint, 명명된 파라미터, 서브쿼리
  • 복합 기본 키, 인덱스, 제약 조건
  • 자동 마이그레이션
  • 사용자 정의 Logger
  • 유연하고 확장 가능한 플러그인 API: Database Resolver (다중 데이터베이스, 읽기/쓰기 분리), Prometheus...
  • 각 기능이 철저히 테스트됨
  • 개발자 친화적

gorm 에는 몇 가지 단점도 있습니다. 예를 들어 거의 모든 메서드 파라미터가 빈 인터페이스 타입이어서 문서를 보지 않으면 어떤 파라미터를 전달해야 하는지 알기 어렵습니다. 때로는 구조체를 전달할 수 있고, 때로는 문자열을 전달할 수 있으며, 때로는 map 을 전달할 수 있고, 때로는 슬라이스를 전달할 수 있어 의미가 다소 모호하며 많은 상황에서 직접 SQL 을 작성해야 합니다.

대안으로 시도해 볼 수 있는 두 가지 ORM 이 있습니다. 첫 번째는 aorm으로 최근에 오픈소스로 공개되었으며 더 이상 직접 테이블 필드명을 작성할 필요가 없으며 대부분 체인 연산이며 리플렉션을 기반으로 구현되었습니다. star 수가 많지 않아 조금 더 지켜볼 수 있습니다. 두 번째는 entfacebook에서 오픈소스로 공개한 ORM 으로 체인 연산을 지원하며 대부분의 상황에서 직접 SQL 을 작성할 필요가 없습니다. 설계 철학상 그래프 (데이터 구조의 그래프) 를 기반으로 하며 구현상 코드 생성을 기반으로 합니다 (이 부분에 동의합니다). 하지만 문서가 전부 영어여서 일정한 진입 장벽이 있습니다.

설치

gorm 라이브러리 설치

sh
$ go get -u gorm.io/gorm

연결

gorm 은 현재 다음 데이터베이스를 지원합니다.

  • 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 는 mysql 프로토콜과 호환
  • ClickHouse: "gorm.io/driver/clickhouse"

이 외에도 oracle 드라이버 CengSin/oracle 와 같이 서드파티 개발자가 제공하는 다른 데이터베이스 드라이버도 있습니다. 이 문서에서는 MySQL 을 사용하여 시연하며 사용하는 데이터베이스에 따라 해당 드라이버를 설치해야 합니다. 여기서는 Mysql 의 gorm 드라이버를 설치합니다.

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

그런 다음 dsn(data source name) 을 사용하여 데이터베이스에 연결하며 드라이버 라이브러리는 dsn 을 해당 구성으로 파싱합니다.

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")
}

또는 수동으로 구성을 전달할 수도 있습니다.

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")
}

두 방법 모두 동등하며 사용 습관에 따라 선택하면 됩니다.

연결 구성

gorm.Config 구성 구조체를 전달하여 gorm 의 일부 동작을 제어할 수 있습니다.

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

다음은 간단한 설명이며 사용 시根据自己的需求 구성할 수 있습니다.

go
type Config struct {
  // 기본 트랜잭션 비활성화, gorm 은 단일 생성 및 업데이트 시 데이터 일관성 유지를 위해 트랜잭션을 시작합니다.
  SkipDefaultTransaction bool
  // 사용자 정의 명명 전략
  NamingStrategy schema.Namer
  // 전체 연관관계 저장
  FullSaveAssociations bool
  // 사용자 정의 logger
  Logger logger.Interface
  // nowfunc 사용자 정의, CreatedAt 및 UpdatedAt 필드 주입용
  NowFunc func() time.Time
  // sql 만 생성하고 실행하지 않음
  DryRun bool
  // 사전 컴파일 문 사용
  PrepareStmt bool
  // 연결 후 데이터베이스 ping
  DisableAutomaticPing bool
  // 데이터베이스 마이그레이션 시 외래 키 무시
  DisableForeignKeyConstraintWhenMigrating bool
  // 데이터베이스 마이그레이션 시 연관관계 참조 무시
  IgnoreRelationshipsWhenMigrating bool
  // 중첩 트랜잭션 비활성화
  DisableNestedTransaction bool
  // 전역 업데이트 허용, where 없는 update
  AllowGlobalUpdate bool
  // 테이블의 모든 필드 쿼리
  QueryFields bool
  // 배치 생성 size
  CreateBatchSize int
  // 오류 변환 활성화
  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
}

모델

gorm 에서 모델은 데이터베이스 테이블에 해당하며 일반적으로 구조체 형태로 나타납니다. 예를 들어 다음 구조체와 같습니다.

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string
}

구조체 내부는 기본 데이터 타입과 sql.Scannersql.Valuer 인터페이스를 구현한 타입으로 구성될 수 있습니다. 기본적으로 Person 구조체가 매핑되는 테이블명은 persons이며 이는 스네이크 케이스 복수형 스타일입니다. 열 이름도 스네이크 케이스 스타일이며 예를 들어 Id는 열 이름 id에 해당합니다. gorm 은 이를 구성하기 위한 몇 가지 방법도 제공합니다.

열 이름 지정

구조체 태그를 통해 구조체 필드에 열 이름을 지정할 수 있으며 이렇게 하면 엔티티 매핑 시 gorm 이 지정된 열 이름을 사용합니다.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

테이블 이름 지정

Table 인터페이스를 구현하여 테이블 이름을 지정할 수 있으며 이 인터페이스에는 테이블 이름을 반환하는 메서드만 있습니다.

go
type Tabler interface {
  TableName() string
}

구현된 메서드에서 문자열 person을 반환하며 데이터베이스 마이그레이션 시 gorm 은 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"
}

명명 전략은 연결 생성 시 자신의 전략 구현을 전달하여 사용자 정의 효과를 달성할 수도 있습니다.

시간 추적

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"
}

CreatedAt 또는 UpdatedAt 필드가 포함되면 레코드 생성 또는 업데이트 시 0 값이면 gorm 은 자동으로 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 은 타임스탬프 추적도 지원합니다.

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;"`
}

그렇다면 Create 실행 시 다음 SQL 과 동일합니다.

sql
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)

실제 상황에서 시간 추적 필요가 있다면 백엔드에서 타임스탬프를 저장하는 것을 권장하며 타임존이 다른 경우 처리가 더 간단합니다.

Model

gorm 은 미리 정의된 Model 구조체를 제공하며 이는 ID 기본 키와 두 개의 시간 추적 필드 및 하나의 소프트 삭제 기록 필드를 포함합니다.

go
type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

사용 시 엔티티 모델에 임베드하기만 하면 됩니다.

go
type Order struct {
  gorm.Model
  Name string
}

이렇게 하면 gorm.Model의 모든 특성을 자동으로 갖게 됩니다.

기본 키

기본적으로 Id라는 이름의 필드가 기본 키이며 구조체 태그를 사용하여 기본 키 필드를 지정할 수 있습니다.

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

여러 필드가 복합 기본 키를 형성합니다.

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 구조체 태그를 사용하여 열 인덱스를 지정할 수 있습니다.

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;"`
}

위의 구조체에서 Address 필드에 고유 인덱스를 생성했습니다. 두 필드가 동일한 이름의 인덱스를 사용하면 복합 인덱스가 생성됩니다.

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;"`
}

외래 키

구조체에서 외래 키 관계는 구조체를 임베드하는 방식으로 정의됩니다. 예를 들어

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;"`
}

예제에서 Person 구조체는 두 개의 외래 키를 가지며 각각 DadMom 두 구조체의 기본 키를 참조합니다. 기본적으로 참조하는 것은 기본 키입니다. PersonDadMom에 대해 일대일 관계이며 한 사람은 한 명의 아버지와 어머니만 가질 수 있습니다. DadMomPerson에 대해 일대다 관계이며 아버지와 어머니는 여러 자녀를 가질 수 있기 때문입니다.

go
Mom   Mom `gorm:"foreignKey:MomId;"`

임베드 구조체의 역할은 외래 키와 참조를 지정하기 편리하게 하는 것이며 기본적으로 외래 키 필드명 형식은 참조된 타입명 +Id입니다. 예를 들어 MomId입니다. 기본적으로 참조하는 것은 기본 키이며 구조체 태그를 통해 특정 필드를 참조하도록 지정할 수 있습니다.

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;"`
}

그중 constraint:OnUpdate:CASCADE,OnDelete:SET NULL;는 정의된 외래 키 제약입니다.

엔티티 모델은 사용자 정의 훅을 가질 수 있습니다.

  • 생성
  • 업데이트
  • 삭제
  • 쿼리

해당 인터페이스는 다음과 같습니다.

go
// 생성 전 트리거
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

// 생성 후 트리거
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// 업데이트 전 트리거
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

// 업데이트 후 트리거
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// 저장 전 트리거
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

// 저장 후 트리거
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// 삭제 전 트리거
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

// 삭제 후 트리거
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// 쿼리 후 트리거
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

구조체는 이러한 인터페이스를 구현하여 일부 동작을 사용자 정의할 수 있습니다.

태그

다음은 gorm 이 지원하는 일부 태그입니다.

태그명설명
columndb 열 이름 지정
type열 데이터 타입, 호환성이 좋은 범용 타입 사용 권장, 예: 모든 데이터베이스가 bool, int, uint, float, string, time, bytes 를 지원하며 다른 태그와 함께 사용 가능, 예: not null, size, autoIncrement... varbinary(8)과 같이 데이터베이스 데이터 타입 지정도 지원. 데이터베이스 데이터 타입 지정 시 완전한 데이터베이스 타입이어야 함, 예: MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer데이터를 데이터베이스로 직렬화 또는 역직렬화할 직렬화기 지정, 예: serializer:json/gob/unixtime
size열 데이터 타입의 크기 또는 길이 정의, 예 size: 256
primaryKey열을 기본 키로 정의
unique열을 고유 키로 정의
default열의 기본값 정의
precision열의 정밀도 지정
scale열 크기 지정
not null열을 NOT NULL 로 지정
autoIncrement열을 자동 증가로 지정
autoIncrementIncrement자동 증가 간격, 연속 레코드 간 간격 제어
embedded중첩 필드
embeddedPrefix임베디드 필드의 열 이름 접두사
autoCreateTime생성 시 현재 시간 추적, int 필드의 경우 초 단위 타임스탬프 추적, nano/milli 를 사용하여 나노초, 밀리초 타임스탬프 추적 가능, 예: autoCreateTime:nano
autoUpdateTime생성/업데이트 시 현재 시간 추적, int 필드의 경우 초 단위 타임스탬프 추적, nano/milli 를 사용하여 나노초, 밀리초 타임스탬프 추적 가능, 예: autoUpdateTime:milli
index파라미터에 따라 인덱스 생성, 여러 필드가 동일한 이름 사용 시 복합 인덱스 생성, 자세한 내용은 인덱스 참조
uniqueIndexindex 와 동일하지만 고유 인덱스 생성
check검사 제약 생성, 예 check:age > 13, 자세한 내용은 제약 참조
<-필드 쓰기 권한 설정, <-:create 생성만, <-:update 업데이트만, <-:false 쓰기 권한 없음, <- 생성 및 업데이트 권한
->필드 읽기 권한 설정, ->:false 읽기 권한 없음
-이 필드 무시, - 는 읽기/쓰기 없음, -:migration 은 마이그레이션 권한 없음, -:all 은 읽기/쓰기/마이그레이션 권한 없음
comment마이그레이션 시 필드에 주석 추가
foreignKey현재 모델의 열을 조인 테이블의 외래 키로 지정
references참조 테이블의 열 이름 지정, 조인 테이블 외래 키로 매핑됨
polymorphic다형성 타입 지정, 예: 모델명
polymorphicValue다형성 값 지정, 기본 테이블명
many2many조인 테이블 이름 지정
joinForeignKey조인 테이블의 외래 키 열 이름 지정, 현재 테이블로 매핑됨
joinReferences조인 테이블의 외래 키 열 이름 지정, 참조 테이블로 매핑됨
constraint관계 제약, 예: OnUpdate, OnDelete

마이그레이션

AutoMigrate 메서드는 자동 마이그레이션을 도와주며 테이블, 제약 조건, 인덱스, 외래 키 등을 생성합니다.

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

예를 들어

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

또는 Migrator 메서드를 통해 Migrator 인터페이스에 액세스하여 수동으로 조작할 수도 있습니다.

go
func (db *DB) Migrator() Migrator

다음 인터페이스 메서드를 지원합니다.

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

메서드 목록에는 데이터베이스, 테이블, 열, 뷰, 인덱스, 제약 조건 등 여러 차원이 포함되어 있어 사용자 정의가 필요한 사용자에게 더 세밀한 조작이 가능합니다.

테이블 주석 지정

마이그레이션 시 테이블 주석을 추가하려면 다음 방법과 같이 설정할 수 있습니다.

go
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})

AutoMigrate() 메서드를 사용하여 마이그레이션하는 경우 구조체 간에 참조 관계가 있으면 gorm 이 재귀적으로 참조 테이블을 먼저 생성하므로 참조된 테이블과 참조하는 테이블의 주석이 모두 반복된다는 점에 유의해야 합니다. 따라서 CreateTable 메서드를 사용하여 생성하는 것을 권장합니다.

TIP

테이블 생성 시 CreateTable 메서드는 참조된 테이블이 참조하는 테이블보다 먼저 생성되어야 하며 그렇지 않으면 오류가 발생합니다. 반면 AutoMigrate 메서드는 관계 참조를 따라 재귀적으로 생성하므로 그렇지 않아도 됩니다.

생성

Create

새 레코드를 생성할 때 대부분 Create 메서드를 사용합니다.

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

다음과 같은 구조체가 있습니다.

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

레코드 하나 생성

go
user := Person{
    Name: "jack",
}

// 반드시 참조를 전달해야 함
db = db.Create(&user)

// 실행 중 발생한 오류
err = db.Error
// 생성된 수
affected := db.RowsAffected

생성 완료 후 gorm 은 기본 키를 user 구조체에 기록하므로 포인터를 전달해야 하는 이유입니다. 슬라이스를 전달하면 배치 생성됩니다.

go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Create(&user)

마찬가지로 gorm 은 기본 키를 슬라이스에 기록합니다. 데이터 양이 너무 많을 때는 CreateInBatches 메서드를 사용하여 배치로 생성할 수 있습니다. 생성된 INSERT INTO table VALUES (),()와 같은 SQL 문장이 매우 길어지기 때문이며 각 데이터베이스는 SQL 길이에 제한이 있으므로 필요할 때 배치로 생성할 수 있습니다.

go
db = db.CreateInBatches(&user, 50)

이 외에도 Save 메서드로도 레코드를 생성할 수 있으며 기본 키가 일치하면 업데이트하고 그렇지 않으면 삽입합니다.

go
func (db *DB) Save(value interface{}) (tx *DB)
go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Save(&user)

Upsert

Save 메서드는 기본 키만 매칭할 수 있으며 Clause 를 구축하여 더 사용자 정의된 upsert 를 완료할 수 있습니다. 예를 들어 다음 코드

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: false,
    DoUpdates: clause.AssignmentColumns([]string{"address"}),
    UpdateAll: false,
}).Create(&p)

이는 필드 name 이 충돌하면 필드 address 의 값을 업데이트하고 충돌하지 않으면 새 레코드를 생성합니다. 충돌 시 아무것도 하지 않을 수도 있습니다.

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

또는 모든 필드를 직접 업데이트

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

upsert 를 사용하기 전에 충돌 필드에 인덱스를 추가하는 것을 잊지 마십시오.

쿼리

First

gorm 은 쿼리에 대해 상당히 많은 방법을 제공합니다. 첫 번째는 First 메서드입니다.

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

이는 기본 키 오름차순으로 첫 번째 레코드를 찾습니다. 예를 들어

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

dest 포인터를 전달하여 gorm 이 쿼리된 데이터를 구조체에 매핑하도록 합니다.

또는 TableModel 메서드를 사용하여 쿼리 테이블을 지정할 수 있으며 전자는 문자열 테이블 이름을 받고 후자는 엔티티 모델을 받습니다.

db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)

TIP

전달된 포인터 요소에 구조체 포인터나 구조체 슬라이스 포인터와 같은 엔티티 모델이 포함되어 있으면 수동으로 어떤 테이블을 쿼리할지 지정할 필요가 없으며 이 규칙은 모든 CRUD 작업에 적용됩니다.

Take

Take 메서드는 First 와 유사하지만 기본 키로 정렬하지 않는다는 점이 다릅니다.

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

Pluck 메서드는 테이블의 단일 열을 배치 쿼리하는 데 사용되며 쿼리 결과는 반드시 엔티티 타입 슬라이스가 아닌 지정된 타입의 슬라이스로 수집할 수 있습니다.

go
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)

예를 들어 모든 사람의 주소를 문자열 슬라이스로 수집

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)

실제로 다음과 동일합니다.

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

Count

Count 메서드는 엔티티 레코드 수를 통계하는 데 사용됩니다.

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

사용 예제를 봅니다.

go
var count int64

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

Find

배치 쿼리에 가장 일반적으로 사용되는 것은 Find 메서드입니다.

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

주어진 조건에 따라 모든 일치하는 레코드를 찾습니다.

go
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)

Select

gorm 은 기본적으로 모든 필드를 쿼리하며 Select 메서드를 사용하여 필드를 지정할 수 있습니다.

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

예를 들어

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

다음과 동일합니다.

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

동시에 Omit 메서드를 사용하여 필드를 무시할 수도 있습니다.

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

예를 들어

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)

SelectOmit 으로 선택되거나 무시된 필드는 생성 업데이트 쿼리 시에도 작동합니다.

Where

조건 쿼리에는 Where 메서드가 사용됩니다.

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

다음은 간단한 예제입니다.

go
var p Person

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

체인 연산에서 여러 Where 를 사용하면 여러 AND 문이 구축됩니다. 예를 들어

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)

또는 Or 메서드를 사용하여 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)

비슷한 Not 메서드도 있습니다.

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)

IN 조건의 경우 Where 메서드에 직접 슬라이스를 전달할 수 있습니다.

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

또는 다중 열 IN 조건의 경우 [][]any 타입으로 파라미터를 전달해야 합니다.

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 은 where 그룹 사용을 지원하며 위의 몇 가지 문을 결합합니다.

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

정렬에는 Order 메서드가 사용됩니다.

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

사용 예제를 봅니다.

go
var ps []Person

// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)

여러 번 호출할 수도 있습니다.

go
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)

Limit

LimitOffset 메서드는 일반적으로 페이지네이션 쿼리에 사용됩니다.

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

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

다음은 간단한 페이지네이션 예제입니다.

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

GroupHaving 메서드는 일반적으로 그룹화 작업에 사용됩니다.

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

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

다음 예제를 봅니다.

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

Distinct 메서드는 일반적으로 중복 제거에 사용됩니다.

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

예제를 봅니다.

go
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)

서브쿼리

서브쿼리는 중첩 쿼리이며 예를 들어 id 값이 평균값보다 큰 모든 사람을 쿼리하려면

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

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)

잠금

gorm 은 clause.Locking 절을 사용하여 잠금을 지원합니다.

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)

이터레이션

Rows 메서드를 통해 이터레이터를 얻을 수 있습니다.

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

이터레이터를 순회하며 ScanRows 메서드를 사용하여 각 행의 결과를 구조체로 스캔할 수 있습니다.

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
    }
}

수정

Save

생성 시 Save 메서드를 언급했으며 이는 레코드 업데이트에도 사용할 수 있으며 일부 구조체 필드가 0 값이라도 모든 필드를 업데이트합니다. 하지만 기본 키가 일치하지 않으면 삽입 작업이 수행됩니다.

go
var p Person

db.First(&p)

p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)

기본 키를 제외한 모든 필드를 SET 문에 추가하는 것을 볼 수 있습니다.

Update

대부분의 경우 Update 메서드를 사용하는 것을 권장합니다.

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

이는 주로 단일 열 필드를 업데이트하는 데 사용됩니다.

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

Updates 메서드는 다중 열 업데이트에 사용되며 구조체와 map 을 파라미터로 받으며 구조체 필드가 0 값일 때 해당 필드를 무시하지만 map 에서는 그렇지 않습니다.

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

다음은 예제입니다.

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"})

SQL 표현식

때로는 필드에 대해 자체 증가 또는 자체 감소 등 자체 연산 작업을 수행해야 할 때가 있으며 일반적으로 먼저 쿼리한 후 계산하고 업데이트하거나 SQL 표현식을 사용합니다.

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

다음 예제를 봅니다.

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")})

삭제

gorm 에서 레코드 삭제에는 Delete 메서드가 사용되며 엔티티 구조체를 직접 전달하거나 조건을 전달할 수 있습니다.

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

예를 들어 구조체를 직접 전달

go
var p Person

db.First(&p)

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

또는

go
var p Person

db.First(&p)

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

또는 조건 지정

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

간단히 작성할 수도 있습니다.

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)

배치 삭제의 경우 슬라이스를 전달합니다.

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

소프트 삭제

엔티티 모델이 소프트 삭제를 사용하는 경우 삭제 시 기본적으로 업데이트 작업이 수행되며 영구 삭제를 원하면 Unscoped 메서드를 사용할 수 있습니다.

go
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})

연관 정의

gorm 은 임베디드 구조체와 필드 형태로 구조체 간 연관을 정의하는 테이블 연관 상호작용 기능을 제공합니다.

일대일

일대일 관계가 가장 간단하며 일반적으로 한 사람은 한 명의 어머니만 가질 수 있습니다. 다음 구조체를 봅니다.

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
}

Person 구조체는 Mom 구조체를 임베드하여 Mom 타입에 대한 참조를 구현하며 Person.MomId 는 참조 필드이고 Mom.Id 기본 키는 참조되는 필드입니다. 이렇게 하면 일대일 관계 연관이 완료됩니다. 외래 키와 참조 및 제약을 사용자 정의하는 방법과 기본 외래 키 규칙은 이미 [외래 키 정의](#외래 - 키) 에서 설명했으므로 더 이상 반복하지 않습니다.

TIP

외래 키 필드의 경우 sql 패키지에서 제공하는 타입을 사용하는 것을 권장합니다. 외래 키는 기본적으로 NULL 일 수 있으며 Create 로 레코드 생성 시 일반 타입을 사용하면 0 값 0 도 생성되므로 존재하지 않는 외래 키가 생성되는 것은 허용되지 않습니다.

일대다

다음으로 학교 구조체를 추가하면 학교와 학생은 일대다 관계이며 한 학교에는 여러 학생이 있지만 한 학생은 한 학교에서만 다닐 수 있습니다.

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[]person 타입으로 여러 학생을 가질 수 있음을 나타내며 Person 은 반드시 School 을 참조하는 외래 키인 Person.SchoolId 를 포함해야 합니다.

다대다

한 사람은 많은 집을 가질 수 있고 한 집에는 많은 사람이 살 수 있으며 이는 다대다 관계입니다.

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;"`
}

PersonHouse 는 서로의 슬라이스 타입을 보유하여 다대다 관계를 나타내며 다대다 관계는 일반적으로 조인 테이블을 생성해야 하며 many2many 를 통해 조인 테이블을 지정하고 조인 테이블의 외래 키가 올바르게 지정되어야 합니다.

구조체 생성 후 gorm 이 자동으로 데이터베이스로 마이그레이션하도록 합니다.

go
tables := []any{
    School{},
    Mom{},
    Person{},
    House{},
    PersonHouse{},
}
for _, table := range tables {
    db.Migrator().CreateTable(&table)
}

참조 테이블과 참조되는 테이블의 선후 생성 순서에 유의하십시오.

연관 작업

위의 세 가지 연관 관계를 생성한 후 연관을 사용하여 CRUD 하는 방법입니다. 이는 주로 Association 메서드를 사용합니다.

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

이는 연관 파라미터를 받으며 그 값은 임베드된 참조 구조체의 참조되는 타입의 필드명이어야 합니다.

go
db.Model(&person).Association("Mom").Find(&mom)

예를 들어 한 사람의 어머니를 연관 조회할 때 Association 의 파라미터는 MomPerson.Mom 필드명입니다.

연관 생성

go
// 데이터 정의
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)

// Person 과 Mom 의 연관 추가, 일대일 연관
// 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)

// school 과 Person 의 연관 추가, 일대다 연관
// 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})

// Person 과 Houses 의 연관 추가, 다대다 연관
// 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})

모든 레코드가 존재하지 않는 경우 연관 생성 시 레코드를 먼저 생성한 후 연관을 생성합니다.

연관 조회

연관 조회 방법을 시연합니다.

go
// 일대일 연관 조회
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)

// 일대다 연관 조회
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)

// 다대다 연관 조회
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)

연관 조회는 기존 데이터에 따라 참조 테이블에서 조건에 맞는 레코드를 찾으며 다대다 관계의 경우 gorm 이 자동으로 테이블 조인을 완료합니다.

연관 업데이트

연관 업데이트 방법을 시연합니다.

go
// 일대일 연관 업데이트
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)

// 일대다 연관 업데이트

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)

// 다대다 연관 업데이트

// 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"}})

연관 업데이트 시 참조된 데이터와 참조하는 데이터가 모두 존재하지 않으면 gorm 이 이를 생성하려고 시도합니다.

연관 삭제

연관 삭제 방법을 시연합니다.

go
// 일대일 연관 삭제
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)

// 일대다 연관 삭제

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)

// 다대다 연관 삭제
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)

연관 삭제는 참조 관계만 삭제하며 엔티티 레코드는 삭제하지 않습니다. Clear 메서드를 사용하여 연관을 직접 지울 수도 있습니다.

go
db.Model(&jack).Association("Houses").Clear()

해당 엔티티 레코드를 삭제하려면 Association 작업 뒤에 Unscoped 작업을 추가할 수 있습니다 (many2many 에는 영향 없음).

go
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)

일대다와 다대多的 경우 Select 작업을 사용하여 레코드를 삭제할 수 있습니다.

go
var (
    mit     School
)
db.Where("name = ?", "mit").First(&mit)

db.Select("Persons").Delete(&mit)

사전 로딩

사전 로딩은 연관 데이터를 쿼리하는 데 사용되며 연관 관계가 있는 엔티티의 경우 연관 참조된 엔티티를 먼저 미리 로드합니다. 이전에 언급한 연관 조회는 연관 관계에 대한 쿼리이며 사전 로딩은 모든 연관 관계를 포함하여 엔티티 레코드를 직접 쿼리합니다. 문법상으로 연관 조회는 먼저 지정된 []Person 을 쿼리한 후 []Person 에 따라 연관된 []Mom 을 쿼리하며 사전 로딩은 문법상 직접 []Person 을 쿼리하고 모든 연관 관계도 함께 로드하지만 실제로 실행되는 SQL 은 거의 동일합니다. 다음 예제를 봅니다.

go
var users []Person

// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)

이는 일대일 연관 조회 예제이며 출력은 다음과 같습니다.

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:[]}]

연관된 Mom 도 함께 쿼리되었지만 학교 관계는 사전 로딩되지 않았으므로 모든 School 구조체는 0 값입니다. clause.Associations 를 사용하여 중첩 관계를 제외한 모든 관계를 사전 로딩할 수도 있습니다.

go
db.Preload(clause.Associations).Find(&users)

다음은 중첩 사전 로딩 예제로 모든 학교와 연관된 모든 학생 및 각 학생과 연관된 어머니와 각 학생이 소유한 집을 쿼리하며 각 집의 주인 집합도 쿼리합니다. 학교->학생->집->학생.

go
var schools []School

db.Preload("Persons").
    Preload("Persons.Mom").
    Preload("Persons.Houses").
    Preload("Persons.Houses.Persons").Find(&schools)

// 출력 코드, 로직 무시 가능
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()
    }
}

출력은 다음과 같습니다.

school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]

person mike
mom

각 학교의 각 학생의 어머니와 그들의 집 및 집의 모든 주인을 출력하는 것을 볼 수 있습니다.

트랜잭션

gorm 은 기본적으로 트랜잭션을 시작하며 삽입 및 업데이트 작업 실패 시 롤백됩니다. [연결 구성](#연결 - 구성) 에서 비활성화할 수 있으며 성능은 약 30% 정도 향상됩니다. gorm 에서 트랜잭션 사용에는 여러 방법이 있으며 아래에서 간단히 소개합니다.

자동

클로저 트랜잭션으로 Transaction 메서드를 통해 클로저 함수를 전달하며 함수 반환 값이 nil 이 아니면 자동으로 롤백됩니다.

go
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)

다음 예제를 봅니다. 클로저 내 작업은 외부의 db 가 아닌 파라미터 tx 를 사용해야 합니다.

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

수동

수동 트랜잭션을 사용하는 것을 권장하며 언제 롤백하고 언제 커밋할지 직접 제어합니다. 수동 트랜잭션에는 다음 세 가지 메서드가 사용됩니다.

go
// Begin 메서드는 트랜잭션 시작에 사용
func (db *DB) Begin(opts ...*sql.TxOptions) *DB

// Rollback 메서드는 트랜잭션 롤백에 사용
func (db *DB) Rollback() *DB

// Commit 메서드는 트랜잭션 커밋에 사용
func (db *DB) Commit() *DB

다음 예제를 봅니다. 트랜잭션 시작 후 tx 를 사용하여 orm 을 조작해야 합니다.

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

롤백 포인트를 지정할 수 있습니다.

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

요약

위의 모든 내용을 읽고 코드를 직접 작성했다면 gorm 을 사용하여 데이터베이스에 대한 CRUD 를 수행할 수 있습니다. gorm 에는 이러한 작업 외에도 다른 많은 기능이 있으며 더 많은 세부 사항은 공식 문서에서 확인할 수 있습니다.

Golang by www.golangdev.cn edit