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
テーブルがそれぞれ対応しているとします。
CREATE TABLE Students
(id integer NOT NULL,
name varchar(255) NOT NULL
PRIMARY KEY (id));
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の以下の行で発生します。
{{- 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
になります。
// 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を採用する場合、あまり複合主キーを使わないテーブル設計をしたほうが良いかもしれません。
-
逆向きに定義 (
Course
からStudentCourse
を経由してStudent
を取得)してもかまいません。使いたい方をえらんでください。 ↩