miyazi888の覚え書き日記

学習したことを書き留めてます。

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)
}

うまいこと使えば、かなり使える機能だと思った。

参考

budougumi0617.github.io

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)

参考

entgo.io

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で実行されたSQLを確認する - Qiita

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()について

エンティティの生成時だけに設定されてほしい項目に指定する

Fields | ent

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)

参考

ミックスイン | ent

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

参考

Edges | ent

述語 | ent