Go言語で簡単にDBアクセスを実現!GORMを使ってORMを体験しよう-Create編(2)

はじめに

以下の 2 つのエントリで Go 言語で利用可能な O/R マッパーである GORM の概要、導入方法、基本的な使い方とデータの登録方法について学びました。

今回は前回のデータ登録 Create で説明しきれなかった登録機能とアソシエーション(テーブル同士の関連)の設定されているデータの登録方法を学びます。

また、 default キーワードを使ってカラムを作成したときにハマるケースについても説明します。


「Belongs To」アソシエーションと関連モデルの登録方法

belongs to アソシエーションは1対1のモデル関係を表現する際に使用されます。

特に、一方のモデルがもう一方に「属する」 ( 依存している ) 関係を表現します。

例をあげます。

作成中のアプリケーションに userscompanies という 2 つのモデルがあるとします。
users モデルは必ず companies テーブルのレコードに依存しているとします。

このような状況で users テーブルと companies テーブルにデータを登録する処理を、 GORM のコードで記述してみます。
( データベース環境の用意を省略するために、今回は SQLite を使用しています )

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

// companies テーブルと対になる
type Company struct {
    gorm.Model
    Name string
}

// users テーブルと対になる
type User struct {
    gorm.Model
    Name      string
    CompanyID uint
    Company   Company
}

func main() {
    // DBに接続
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

    // DBマイグレーション(テーブルを作成)
    db.AutoMigrate(&Company{})
    db.AutoMigrate(&User{})

    // (1) users レコードと companies レコードを同時に作成する
    db.Create(&User{
        Name:    "taro",
        Company: Company{Name: "ninten-done"},
    })

    // (2) users レコードと companies レコードを別々に作成する
    saga := Company{
        Name: "saga",
    }
    db.Create(&saga)

    // 事前に作成した companies レコードを使って users レコードを作成する
    db.Create(&[]User{
        // モデルを指定して作成
        User{
            Name:    "jiro",
            Company: saga,
        },
        User{
            Name:    "saburo",
            Company: saga,
        },
        // モデルのIDを指定して作成
        User{
            Name:      "shiro",
            CompanyID: saga.ID,
        },
    })
}

User オブジェクト(構造体だがドキュメントには Object と書かれているのでこの文言を使用します) は所属するモデルをフィールドに保つ必要があります。

そのため Company を設定しますが、加えて CompanyID も必要となります。
CompanyID フィールドは暗黙的に外部キーとして使用されます。

登録された users テーブルと companies テーブルをまとめて取得したい場合は、クエリ実行の際に DB#Preload() あるいは DB#Joins() メソッドを呼び出します。

このあたりのメソッドもよくある O/R マッパー同様の機能となります。

  • DB#Preload() : アソシエーションが設定されているテーブルを複数回の SQL 発行で取得します(引数に指定されたモデル数分)
  • DB#Joins() : アソシエーションが設定されているテーブルを1回の SQL 発行で取得する
    // すべてのユーザー情報を取得
    var users []User
    db.Preload("Company").Find(&users)
    for _, user := range users {
        println(user.Name, user.Company.Name)
    }

User オブジェクトに設定されているアソシエーションである Company を無視させたい場合には、 Omit() メソッドにモデル名を指定しスキップさせることもできます。

    // Company が設定されているが company_id は null となる
    db.Omit("Company").Create(&User{
        Name: "ichiro",
        Company: Company{
            Name: "SHIKAKU",
        },
    })
}

デフォルト値 ( default )

構造体フィールドのタグに default キーワードを指定して、フィールドの値が "ゼロ値" だった時の値を強制できます。
( ちなみに構造体の名前はパッケージ外から参照しない場合は小文字から初めて問題ありません )

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type user struct {
    gorm.Model
    Name string `gorm:"default:'名無しさん'"`
    Age  uint   `gorm:"default:999"`
}

func main() {
    // DBに接続
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

    db.AutoMigrate(&user{})

    db.Create(&user{Name: "Taro", Age: 20})
    db.Create(&user{Age: 30})
    db.Create(&user{Name: "Jiro"})
    db.Create(&user{Name: "", Age: 0})

    var users []user
    db.Find(&users)
    for _, user := range users {
        println(user.Name, user.Age) // Taro 20, 名無しさん 30, Jiro 999, 名無しさん 999
    }
}

ゼロ値 については注意が必要です。

今回の例では、 string のゼロ値である空文字と、 uint のゼロ値である 0 を指定した場合に default の設定が機能していますが、値に "" や 0 が想定されるような場合に注意しましょう。

特にハマりやすいのは bool 型のフィールドに default タグを設定した場合です。

package main

import (
    "fmt"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name    string
    Deleted bool `gorm:"default:true"`
}

func main() {
    // DBに接続
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

    db.AutoMigrate(&User{})

    user := User{Name: "Taro", Deleted: false}

    db.Create(&user)

    fmt.Printf("Name: %s, Deleted: %t", user.Name, user.Deleted) // Name: Taro, Deleted: true
}

Deleted フィールドには false をセットしているにも関わらず、最新の情報は true となっています。

bool フィールドのゼロ値が false のため、 default の設定値が利用されています。

これを回避するために sql.NullBool という値を利用します。

package main

import (
    "database/sql"
    "fmt"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name    string
    Deleted sql.NullBool `gorm:"default:true"`
}

func main() {
    // DBに接続
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

    db.AutoMigrate(&User{})

    user := User{Name: "Taro", Deleted: sql.NullBool{Bool: false, Valid: true}}

    db.Create(&user)

    fmt.Printf("Name: %s, Deleted: %t", user.Name, user.Deleted) // Name: Taro, Deleted: {false true}
}

sql.NullBool は構造体となっており、 Bool フィールドに値を保持しています。
NULL の場合には Valid フィールドに false をセットします。
(したがって、 Valid が true の場合には Bool フィールドの値は意味がありません )

今回は false が登録できました。

ひとこと

またまた思ったよりもボリュームがあり説明しきれませんでした。
次のエントリに続きます。

Posted by genzouw