0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

string型をポインタ(*string)とすることでnullでも保存できるようにする

Posted at

要約

  • もともと emailstring + NotEmpty + Validate(A案)にしていたため、空文字が弾かれて「未設定」を表現できなかった
  • B案として、Ent スキーマを Nillable().Optional() に変更して nil = 未設定 を表現
  • 保存・更新時は SetNillableEmail(*string) を使い、LINEのみ登録→後でメール追加のフローを自然に実現できるようになった
  • バリデーション(Validate / NotEmpty)は 値がセットされた時のみ評価されるので、未設定時(nil)には実行されない

背景と課題(A案の限界)

当初は以下のようなスキーマだった:

field.String("email").
  Unique().
  NotEmpty().
  Validate(func(email string) error {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(email) {
      return errors.New("invalid email format")
    }
    return nil
  })
  • NotEmpty() と正規表現 Validate により、空文字が禁止
  • つまり未設定を空文字で表すことができず、LINEだけで初回登録というユースケースに合わない

解決策(B案):nullable にして「未設定 = nil」を表現

Ent スキーマの変更

field.String("email").
  Unique().
  Nillable().   // ← これで DB 列が NULL 許可に
  Optional().   // ← これで「セットしない」を選べる
  Validate(func(email string) error {
    // 値がセットされた時だけ評価される(nil のときは呼ばれない)
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(email) {
      return errors.New("invalid email format")
    }
    return nil
  })

メモ:NotEmpty() は正規表現と役割が重複するので外すのがシンプル(残しても「セット時のみ」評価)。

コード生成を更新

ent generate ./ent/schema
# or
go generate ./...

生成後、UserCreate / UserUpdateOneSetNillableEmail(*string) が生える(SetEmail(string) も共存)。


保存・更新の書き方(重要)

Create

// u.Email は *string(nil なら未設定)
eu, err := tx.User.
  Create().
  SetNillableEmail(u.Email).      // ← ここがキモ。nil をそのまま渡せる
  SetName(u.Name).
  SetNillablePassword(u.Password) // password も Nillable なら同様
  Save(ctx)

Update

_, err := tx.User.
  UpdateOneID(u.ID).
  SetNillableEmail(u.Email).      // nil を渡せば NULL(未設定)に戻せる
  SetNillablePassword(u.Password).
  Save(ctx)

生成 API を確認:SetNillableXxx(*T) があるかどうか。無ければスキーマ or 生成の更新漏れ。


なぜポインタ化(nullable)で null を渡せるのか(論理の要点)

  • Go の string は値型:常に何らかの値(空文字を含む)を持つため、未設定を表現できない

  • *string(ポインタ)にすると nil を持てる:不在(未設定)を値として表現可能

  • Ent の Nillable().Optional() は以下を意味する:

    • DB 列NULL 許可になる
    • mutator(Create/Update ビルダー)に値をセットしなければ、そのフィールドは「未設定」扱い
  • この時、NotEmpty()Validate(func(string) error)値がセットされた時だけ評価される(nil ならそもそも呼ばれない)

結果として:

  • 初回登録(LINEのみ)email = nil のまま保存できる
  • あとからメール追加email = &"foo@example.com" を渡す時だけ正規表現チェックが効く

補足:UNIQUE と NULL
多くの RDBMS(PostgreSQL / MySQL)は UNIQUE 列に複数の NULL を許容する。
つまり「メールが設定されているレコード間のみユニーク」を、自然に満たす。


ドメイン/DTO の型方針

  • Domain: Email *string(未設定 = nil

  • DTO(JSON): Email *stringnull をそのまま返す)

    • フロント(TypeScript)は string | null で扱える → 表示は email ?? '未設定'
// domain.User
type User struct {
  ID          int
  Email       *string
  Name        string
  IsAdmin     bool
  IsRoot      bool
  IsTest      bool
  HasPassword bool
  HasLine     bool
  CreatedAt   time.Time
  UpdatedAt   time.Time
}

// DTO
type UserDetail struct {
  ID               int      `json:"id"`
  Email            *string  `json:"email"` // null 許容
  Name             string   `json:"name"`
  IsAdmin          bool     `json:"isAdmin"`
  IsRoot           bool     `json:"isRoot"`
  IsTest           bool     `json:"isTest"`
  IsLine           bool     `json:"isLine"`
  IsSettedPassword bool     `json:"isSettedPassword"`
  CreatedAt        string   `json:"createdAt"`
  UpdatedAt        string   `json:"updatedAt"`
}

マイグレーションの注意点

  • 既存列が NOT NULL の場合は NULL 許可に変更が必要
  • もし既存データに空文字があるなら、先に NULL へ置換しておくとよい
  • アプリで "" を「未設定」として使っていた箇所は、nil ベースに置き換える

クエリとバリデーションの実務 Tips

  • 「メール未設定のユーザーを検索」email IS NULL 条件(Ent なら user.EmailIsNil()

  • メール or LINE どちらか必須など複合ルールは、

    • 最初は Usecase で実装すると簡単
    • 厳密にやるなら Ent Hook(mutation を参照)で禁止するのも可
// Hook 例(イメージ):Create/Update で email=nil & LINE連携なし → エラー
func UserBusinessRuleHook() ent.Hook { /* ... */ }

よくあるハマりどころ(チェックリスト)

  • スキーマを直したのに SetNillableEmail が見えない → 生成し直しent generate)。古い生成物を参照していないか確認
  • SetEmail(*string) を呼んでしまう → SetNillableEmail(*string) を使う(関数名に注意)
  • Unique × 空文字ハック → やめる。NULL 運用で自然に解ける
  • DTO だけ string にしてしまい "" が飛ぶ → DTO も *string にするとフロントでの扱いが楽

まとめ

  • LINEだけで初回登録 → 後でメール追加を実現するには、emailnullable にするのが正攻法
  • Ent の Nillable().Optional() と、生成される SetNillableEmail(*string) を使えば、
    nil = 未設定 を自然に表現でき、セット時だけ バリデーションが働く
  • Domain/DTO も *string に揃えれば、API で null を返せるため、フロント実装も素直になる

設計の一貫性・拡張性・実装の簡潔さ、すべての観点で nullable 設計がベスト
既存が A案(string 固定)でも、B案(nullable)への移行は最小限の変更で済み、面倒なハック(空文字運用)も不要になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?