Go言語のDefined Typeについて
仕事中のコードに出てきて、最初、知らなくて戸惑ったのが、このDefined Type。 以前はNamed Typeと呼ばれていたらしい。
機能としては、ある型を別の型として定義することができる機能。 型としてメソッドも追加できる。
何かの特別な形式を持つ文字列とかアプリに必要なドメインに特化した機能を持った型を定義するのに便利そう。
下の参考先のサイトのサンプルを真似て少し処理を記述してみた。 Passwordという型を定義し、8文字以上の文字列を表現する型とした。
package main import ( "fmt" "log" ) type Password string // Password型を定義。これがDefined Type // Password型は8文字以上かどうかのバリデーション。 // Defined Typeにはメソッドも定義可能 func (p Password) Validate() error { if len(p) < 8 { return fmt.Errorf("パスワードは8文字以上") } return nil } func main() { password := Password("password") if err := password.Validate(); err != nil { log.Fatal(err) } // Password型とstring型は別モノ。 // もし処理先で元の型であるstringにしたい場合は以下のようにする passString := string(password) fmt.Println(passString) }
うまいこと使えば、かなり使える機能だと思った。
参考
entで条件抽出に直接クエリを記述する
ExprP()を使うことで実現可能だった。 これでDBに用意されている関数などを使用することもできる。
具体的例は以下のとおり。 この例ではitemテーブルのnameが5桁以上の文字列のものだけを抽出する。
items, err := r.ent().Item.Query(). Where(func(s *sql.Selector) { s.Where(sql.ExprP("CHAR_LENGTH(name) >= ?", 5)) }). All(ctx)
参考
VSCodeでカーソル下にある単語で検索を行う拡張機能
VScodeを使う以前はvimを使っていて、現在カーソルが当たっている単語で検索をする、ということをよくやっていた。
VScodeでは同じことが出来なさそうで諦めていたけれども、リンク先の拡張機能を試したら、実現できたので紹介。 marketplace.visualstudio.com
vim拡張を入れているので、insert mode以外で; -> wで検索できるように設定した。 これで検索がだいぶ快適になった。
{ "key": "; w", "command": "extension.searchUnderCursor", "when": "!terminalFocus && !sideBarFocus && vim.mode != 'Insert'" },
それにしても、この拡張、ダウンロード数が少ない(この記事を書いてる時点で1300件程度)ことが気になる。 実はこんな風に検索する人って物凄く少ないのか、あるいはすでに標準機能で実現されている機能だったりするのだろうか・・・ 個人的には大満足な格納なんだけれども
entでクエリを確認する方法
entでクエリを確認がわからなかったのでメモ。
全てのクエリを出力したい場合
optionsで指定する
clientを作成する時にoptionsにDebugを指定することで実現可能
func NewDBClient() *ent.Client { user := "user" password := "pass" port := "5432" host := "localhost" dbName := "testdb" url := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, dbName) // ここでDebug()オプションを指定 options := []ent.Option{ent.Debug()} client, err := ent.Open("postgres", url, options...) if err != nil { fmt.Printf("failed connecting to postgres: %v", err) } return client }
clientで指定する
func NewDBClient() *ent.Client { user := "user" password := "pass" port := "5432" host := "localhost" dbName := "testdb" url := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, dbName) client, err := ent.Open("postgres", url) if err != nil { fmt.Printf("failed connecting to postgres: %v", err) } // debugを指定する client = client.Debug() return client }
個別のクエリを出力したい場合
クエリを出力したい箇所だけDebug()を指定することでログに出力される
users, err := client.Debug().User.Query().Where(user.Name("user1")).All(ctx)
参考
微妙に公式では言及されていないのはなんでだろう・・・
entのmixinを使ってスキーマ共通の項目を設定する
Mixin
スキーマ共通の項目を定義して各スキーマで適用することが可能。
ここでは以下の記事で作成したUserテーブルに作成日時(created_at)と更新日時(updated_at)を追加する例を示す。 entを試してみた - miyazi888の覚え書き日記
共通項目の定義
ent/mixin/mixin.go
package schema import ( "time" "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" ) type TimeMixin struct { mixin.Schema } func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time("created_at").Immutable().Default(time.Now), field.Time("updated_at").Default(time.Now()).UpdateDefault(time.Now), } }
Immutable()について
エンティティの生成時だけに設定されてほしい項目に指定する
Userテーブルに共通項目を定義
ent/schema/user.go
package schema import ( mixin "test3/ent/mixin" "entgo.io/ent" "entgo.io/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name").NotEmpty(), field.Int("age").Positive(), } } // 以下のmixinの指定を加える func (User) Mixin() []ent.Mixin { return []ent.Mixin{ mixin.TimeMixin{}, } } // Edges of the User. func (User) Edges() []ent.Edge { return nil }
コード再生成 & マイグレーション
スキーマを修正したので再生成する。
go generate ./ent
DBにも反映する。
go run migrate/migrate.go
DB上のテーブル定義を確認
docker-compose exec db /bin/sh # psql -h localhost -U user -d testdb testdb=# \d users; Table "public.users" Column | Type | Collation | Nullable | Default ------------+--------------------------+-----------+----------+---------------------------------- id | bigint | | not null | generated by default as identity name | character varying | | not null | age | bigint | | not null | created_at | timestamp with time zone | | not null | updated_at | timestamp with time zone | | not null | Indexes: "users_pkey" PRIMARY KEY, btree (id)
参考
VS CodeでのGo言語のデバック
以下の場合にどうやってデバッカーを起動するのか、少し迷ってしまったので、その防備録。
- エントリポイントが./main.go以外の時
- テストコードをデバック
エントリポイントが./main.go以外の時のデバック
./main.goにエントリポイントがある場合は素直にブレイクポイントを設定後、デバッカを起動することで問題なくデバックできる。もしAPIサーバとかの場合はデバッカ起動後にcurlなどでHTTPリクエストを送信する。
ところが、./main.go以外にある時、どうやってデバックできるのかわからなかった。
結論としてはデバッカ起動の設定ファイルである所のlaunch.jsonのprogramの部分に起動したいエントリポイントを記述することで解決する。 例えば、エントリポイントが./api/main.goの場合は以下のように設定する。
{ // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch Package", "type": "go", "request": "launch", "mode": "auto", "program": "${workspaceRoot}/api" } ] }
テストコードのデバック
テストコードをデバックしたい時にもどうすれば良いのかわからなかったので調べた。
こちらはやはり、launch.jsonを下記のように設定することでデバックできた。
{ // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch unit test", "type": "go", "request": "launch", "mode": "test", "program": "${workspaceRoot}/${relativeFileDirname}", "args": [ "-test.run", "TestFunc1" ], "showLog": true, } ] }
programにrelativeFileDirnameを指定することで今現在、VSCodeで開いているファイル(この場合は単体テストコードのファイル)を対象にデバックを実行しようとする。
さらにargsで実行するテスト関数をTestFunc1に限定している。
おわりに
ちなみにlaunch.json内のconfigurationsは配列で、複数の設定を記述することができる。
自分自身はアプリ用の設定とテストコード用の設定を設定して状況に応じて切り替えている。
画面左にサイドバーにデバックビューを表示していれば、上の方にlaunch.jsonで設定した名前の一覧が表示されるハズなので、そこで切り替えできるハズ。
entを試してみた その2(関連テーブル追加とJOIN)
前回のブログでentの基本的なCRUDの使い方がなんとなくわかったので今回は関連テーブルがある場合についてを検証したブログとなる。
前回 https://miyazi888.hatenablog.com/entry/2023/05/05/114518
関連テーブルを追加
ent new Comment
スキーマ変更
この段階で指定するEdgesというのがどうやらテーブルのリレーションを定義する部分っぽい。
ent/shcema/comment.go
package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" ) // Comment holds the schema definition for the Comment entity. type Comment struct { ent.Schema } // Fields of the Comment. func (Comment) Fields() []ent.Field { return []ent.Field{ field.Int("user_id"), field.String("comment").Default("unknown"), } } // Edges of the Comment. func (Comment) Edges() []ent.Edge { return []ent.Edge{ edge.From("user", User.Type). Ref("comments"). Unique(). Required(). Field("user_id"), } }
ent/schema/user.go
package schema import ( "entgo.io/ent" "entgo.io/ent/dialect/entsql" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name").Default("unknown"), field.Int("age").Positive(), field.String("nickname").Default("unknown"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("comments", Comment.Type). Annotations(entsql.Annotation{ OnDelete: entsql.Cascade, }), } }
上記スキーマを元にentityを生成
go generate ./ent
DBに反映する為にmigrate
go run migrate/migrate.go
動作検証
main.go
func cleanUp() { client := db.NewDBClient() ctx := context.Background() _, err := client.Debug().User.Delete().Exec(ctx) if err != nil { fmt.Printf("failed deleting user: %v", err) return } db.CloseDB(client) } func addUserAndComment() { client := db.NewDBClient() ctx := context.Background() // 1件追加 usr, err := client.Debug().User. Create(). SetName("user2"). SetAge(30). Save(ctx) if err != nil { fmt.Printf("failed creating user: %v", err) return } // コメント1件追加 _, err = client.Debug().Comment. Create(). SetUserID(usr.ID). SetComment("comment1"). Save(ctx) if err != nil { fmt.Printf("failed creating comment: %v", err) return } // コメント1件追加 _, err = client.Debug().Comment. Create(). SetUserID(usr.ID). SetComment("comment2"). Save(ctx) if err != nil { fmt.Printf("failed creating comment: %v", err) return } // user2のコメントを全件取得 comments, err := usr.QueryComments().All(ctx) if err != nil { fmt.Printf("failed getting comments: %v", err) return } for _, comment := range comments { fmt.Println(comment.Comment) // comment1, comment2 } // 'comment2'を持つユーザー一覧を取得 usrs, err := client.Debug().User.Query().Where(func(s *sql.Selector) { t := sql.Table(comment.Table) s.Join(t).On(s.C(user.FieldID), t.C(comment.FieldUserID)) s.Where(sql.EQ(t.C(comment.FieldComment), "comment2")) }).All(ctx) if err != nil { fmt.Printf("failed getting users: %v", err) return } for _, usr := range usrs { fmt.Println(usr.Name) // user2 } db.CloseDB(client) } func main() { cleanUp() // crud() addUserAndComment() }
これを実行するとusersテーブルにuser2が追加され、commentsテーブルにコメントが2件追加される。
go run main.go