1
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?

【ent】複合主キーで `nil pointer evaluating interface {}.id` が発生した場合の対処法

Posted at

TL; DR

  • 複合主キーを使用する場合、スキーマの Edges メソッドで関連テーブルの指定が必要

はじめに

entで複合主キーを持つテーブルのスキーマを作成した際、ソースコード自動生成時に表題のエラーが発生してしまいました。本記事ではエラーの解消法と原因について紹介します。

エラーの再現

まずはスキーマを作成します。今回は以下のテーブルを考えます。

-- 講義受講者の成績
CREATE TABLE StudentCourses
(student_id integer NOT NULL.
 course_id integer NOT NULL,
 grade integer NOT NULL
PRIMARY KEY (student_id, course_id));

以下コマンドで空のスキーマが生成されます。

$ go run -mod=mod entgo.io/ent/cmd/ent new StudentCourse

フィールド情報を追記すると以下のようになります。

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/field"
)

// StudentCourse holds the schema definition for the StudentCourse entity.
type StudentCourse struct {
	ent.Schema
}

func (StudentCourse) Annotations() []schema.Annotation {
	return []schema.Annotation{
		// 複合主キーを指定(デフォルトでは `id` というフィールドが主キーとなる)
		field.ID("student_id", "course_id"),
	}
}

// Fields of the StudentCourse.
func (StudentCourse) Fields() []ent.Field {
	return []ent.Field{
		field.Int("student_id"),
		field.Int("course_id"),
		field.Int("grade"),
	}
}

// Edges of the StudentCourse.
func (StudentCourse) Edges() []ent.Edge {
	return nil
}

続いてスキーマからソースコードを自動生成しようとすると、表題の <$.Annotations.Fields.StructTag.id>: nil pointer evaluating interface {}.id が発生し失敗してしまいます。

$ go generate ./ent
execute template "model": template: ent.tmpl:33:30: executing "model" at <$.Annotations.Fields.StructTag.id>: nil pointer evaluating interface {}.id
exit status 1
ent/generate.go:3: running "go": exit status 1

対処法

Edges メソッドに、各複合主キーに対応する関連テーブルを指定する(エッジスキーマ)必要があります。

例えば、student_id フィールドに Students テーブル、course_id フィールドに Courses テーブルがそれぞれ対応しているとします。

studentsテーブル
CREATE TABLE Students
(id integer NOT NULL,
 name varchar(255) NOT NULL
PRIMARY KEY (id));
coursesテーブル
CREATE TABLE Courses
(id integer NOT NULL,
 name varchar(255) NOT NULL
PRIMARY KEY (id));

Student, Course スキーマを作成し、StudentCourse スキーマから両者への関連を定義します。

関連のイメージ
| Students | <--- | StudentCourses | ---> | Courses |

スキーマに対して、それぞれの主キーを Edges メソッドで関連テーブルへ対応付けます。

// Edges of the StudentCourse.
func (StudentCourse) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("student", Student.Type). // student_id は Student に対応
			Unique().
			Required().
			Field("student_id"),
		edge.To("course", Course.Type). // course_id は Course に対応
			Unique().
			Required().
			Field("course_id"),
	}
}

Student, Course のスキーマも必要になるので、こちらも生成します。
Student から StudentCourse を経由して Course を取得するような関連を定義します1

go run -mod=mod entgo.io/ent/cmd/ent new Student
go run -mod=mod entgo.io/ent/cmd/ent new Course
// ...
// Fields of the Student.
func (Student) Fields() []ent.Field {
	return []ent.Field{
		field.Int("id"),
		field.String("name"),
	}
}

// Edges of the Student.
func (Student) Edges() []ent.Edge {
	// 関連を定義: 生徒が履修した講義一覧
	// NOTE: StudentCourseを経由すること!
	return []ent.Edge{
		edge.To("registered_courses", Course.Type).
			Through("student_courses", StudentCourse.Type),
	}
}
// ...
// Fields of the Course.
func (Course) Fields() []ent.Field {
	return []ent.Field{
		field.Int("id"),
		field.String("name"),
	}
}

// Edges of the Course.
func (Course) Edges() []ent.Edge {
	// 逆向きの関連をこちらにも指定
	return []ent.Edge{
		edge.From("students", Student.Type).
			Ref("registered_courses").
			Through("student_courses", StudentCourse.Type),
	}
}

再びソースコードを自動生成すると、今度はエラーなく生成が成功しました。

$ go generate ./ent

エッジスキーマについて詳細は公式ドキュメント以下のページをご覧ください。

なぜ関連テーブルが必要か?

entの設計思想として、スキーマが単一のID (デフォルトでは ID フィールドに対応)を持つことを原則としているためです。

冒頭のエラーはソースコードを自動生成するGo templateの以下の行で発生します。

entc/gen/template/ent.tmpl
	{{- if $.HasOneFieldID }}
		// ID of the ent.
		{{- if $.ID.Comment }}
			{{- range $line := split $.ID.Comment "\n" }}
				// {{ $line }}
			{{- end }}
		{{- end }}
		{{- /* 以下の行でエラー発生 */ }}
		ID {{ $.ID.Type }} {{ with $.Annotations.Fields.StructTag.id }}`{{ . }}`{{ else }}`{{ $.ID.StructTag }}`{{ end }}
	{{- end }}

複合主キーの場合 ID に相当するフィールドがないためnil pointer dereferenceが発生します。

修正後はエッジスキーマを定義することでこの分岐 (if $.HasOneFieldID)に入らなくなり、エラーが解消されました。HasOneFieldID の定義は以下の通りです。
edge.From, edge.To の両方が定義されている場合 false になります。

entc/gen/type.go
// IsEdgeSchema indicates if the type (schema) is used as an edge-schema.
// i.e. is being used by an edge (or its inverse) with edge.Through modifier.
func (t Type) IsEdgeSchema() bool {
	return t.EdgeSchema.To != nil || t.EdgeSchema.From != nil
}

// HasCompositeID indicates if the type has a composite ID field.
func (t Type) HasCompositeID() bool {
	return t.IsEdgeSchema() && len(t.EdgeSchema.ID) > 1
}

// HasOneFieldID indicates if the type has an ID with one field (not composite).
func (t Type) HasOneFieldID() bool {
	return !t.HasCompositeID() && t.ID != nil
}

おわりに

以上、entで複合主キーを扱う際のエラーの対処法でした。entを採用する場合、あまり複合主キーを使わないテーブル設計をしたほうが良いかもしれません。

  1. 逆向きに定義 (Course から StudentCourse を経由して Student を取得)してもかまいません。使いたい方をえらんでください。

1
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
1
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?