Go製Webアプリケーションにentを導入する方法と選定理由

## はじめに
GoでWebアプリケーションを開発する際、データベースとのやり取りをスムーズにするためにORM（Object Relational Mapper）を導入することが一般的です。この記事では、Go向けの新しいORMツールである **ent** を導入する方法を説明しながら、entとは何か、なぜ選んだのかについても詳しく解説します。

なお、以下の記事の続きです。

https://qiita.com/jijimama/items/47fe13b529d808caf8b3

## entとは？
**ent** は、Facebook（現Meta）が開発したGo向けのエンティティフレームワークです。他のORMと比較して次のような特徴を持っています：

### 特徴
- **コード生成ベースのスキーマ管理**:
  - スキーマ（データベース構造）をGoのコードで定義します。
  - 定義に基づいて自動的に型安全なクエリメソッドが生成されます。
- **型安全なクエリ**:
  - クエリがGoの型に依存するため、開発中にエラーを早期に発見できます。
- **柔軟な設計**:
  - リレーションやユニーク制約など、複雑なデータ構造を直感的に扱えます。

## なぜentを選んだのか？
Goでの開発において、RailsのActive Recordに慣れている経験がentの採用を後押ししました。entとActive Recordにはいくつか共通点があり、移行時の学習コストが低いと判断したからです。

### Active Record経験が活きるポイント
- **スキーマ定義の親しみやすさ**:
  - Active Recordではマイグレーションファイルを通じてスキーマを管理します。一方で、entではGoコードでスキーマを定義しますが、設計思想が似ており、フィールドやリレーションの設定も直感的に行えます。
- **オブジェクト指向的なデータ操作**:
  - Active Recordではモデルを介してデータベース操作を行いますが、entも型安全なクライアントとメソッドを提供しており、同様の感覚でクエリを記述できます。
- **リレーションのサポート**:
  - Active Recordと同様に、entもリレーション（1対1、1対多、多対多）を簡単に扱えるため、既存の知識を活かしやすいです。

## 導入手順

以下に、新聞コラムWebアプリにentを導入し、データベースとの接続とスキーマ管理を行う手順を説明します。

### 1. entのインストール
コンテナを起動しshellへ入る
```
docker-compose run backend sh
```
Goコマンドを実行してEntをインストール
```
# go get entgo.io/ent/cmd/ent
```
### 2. スキーマの作成とプロジェクトの初期化
```
# go run -mod=mod entgo.io/ent/cmd/ent new Article
# go run -mod=mod entgo.io/ent/cmd/ent new Newspaper
```
すると新たに```ent``` ディレクトリが作成されます。また、```ent/schema``` ディレクトリには、 エンティティスキーマが作成されています。
作成された```ent/schema/newspaper.go```と```ent/schema/article.go```に変更を加えます

```go:ent/schema/newspaper.go
package schema

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

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

// Fields of the Newspaper.
func (Newspaper) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").
			NotEmpty().
			Unique(),
		field.String("column_name").
			NotEmpty(),
		field.Time("created_at").
			Default(time.Now),
		field.Time("updated_at").
			Default(time.Now).
			UpdateDefault(time.Now),
	}
}

// Edges of the Newspaper.
func (Newspaper) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("articles", Article.Type),
	}
}
```

```go:ent/schema/article.go
package schema

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

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

// Fields of the Article.
func (Article) Fields() []ent.Field {
	return []ent.Field{
		field.Text("content").
			NotEmpty(),
		field.Int("year").
			Positive(),
		field.Int("month").
			Range(1, 12),
		field.Int("day").
			Range(1, 31),
		field.Time("created_at").
			Default(time.Now),
		field.Time("updated_at").
			Default(time.Now).
			UpdateDefault(time.Now),
	}
}

// Edges of the Article.
func (Article) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("newspaper", Newspaper.Type).
			Ref("articles").
			Unique(),
	}
}
```
再度コンテナのshellに入る
```
docker-compose run backend sh
```
```go generate```コマンドを実行
```
# go generate ./ent
```
```go generate ./ent```を実行すると、Entの自動コード生成ツールが起動し、```schema```パッケージで定義したスキーマを使用して、データベースと対話するためのGoコードを生成されます。 
そして、自動生成されたパッケージに必要なモジュールを取得するために、以下のコマンドを実行します。
```
docker-compose run backend sh
# go mod tidy
```
### 3. PostgreSQLとの接続
公式サイトには以下のように記述されています。
https://entgo.io/ja/docs/getting-started/
```go:entdemo/start.go
package main

import (
    "context"
    "log"

    "entdemo/ent"

    _ "github.com/lib/pq"
)

func main() {
    client, err := ent.Open("postgres","host=<host> port=<port> user=<user> dbname=<database> password=<pass>")
    if err != nil {
        log.Fatalf("failed opening connection to postgres: %v", err)
    }
    defer client.Close()
    // Run the auto migration tool.
    if err := client.Schema.Create(context.Background()); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }
}
```
こちらを参考にして、```main.go```の内容を変更します。
```go:backend/main.go
package main

import (
	"github.com/gin-gonic/gin"
	"context"
	"log"
	"os"
	"github.com/joho/godotenv"
	"github.com/jijimama/newspaper-app/ent"
	_ "github.com/lib/pq"
)

func main() {
	router := gin.Default()

	// .envファイルを読み込む
	err := godotenv.Load()
	if err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}

	// 環境変数から取得
	dbHost := os.Getenv("DB_HOST")
	dbPort := os.Getenv("DB_PORT")
	dbUser := os.Getenv("DB_USER")
	dbName := os.Getenv("DB_NAME")
	dbPassword := os.Getenv("DB_PASSWORD")

	// 接続文字列を作成
	dsn := "host=" + dbHost + " port=" + dbPort + " user=" + dbUser + " dbname=" + dbName + " password=" + dbPassword + " sslmode=disable"

    //PostgreSQLに接続
	client, err := ent.Open("postgres", dsn)
	if err != nil {
		log.Fatalf("failed opening connection to postgres: %v", err)
	}
	defer client.Close()
	// Run the auto migration tool.
	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}
	
  	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{
      		"message": "Hello, World!",
		})
  	})

  	router.Run(":8080")
}
```
bakend配下に.envファイルを作成し、以下のようにしました。
```
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_NAME=db
DB_PASSWORD=password
```

### 4. DBの確認

コンテナを起動し、dbのbashに入ります。

```
docker-compose up backend -d
docker compose exec db bash
```
そして、DBに接続します。
```
psql -U postgres -d db
```
そして、テーブルを確認します。
```
db=# \dt
           List of relations
 Schema |    Name    | Type  |  Owner   
--------+------------+-------+----------
 public | articles   | table | postgres
 public | newspapers | table | postgres
(2 rows)

db=# \d articles
                                         Table "public.articles"
       Column       |           Type           | Collation | Nullable |             Default              
--------------------+--------------------------+-----------+----------+----------------------------------
 id                 | bigint                   |           | not null | generated by default as identity
 content            | text                     |           | not null | 
 year               | bigint                   |           | not null | 
 month              | bigint                   |           | not null | 
 day                | bigint                   |           | not null | 
 created_at         | timestamp with time zone |           | not null | 
 updated_at         | timestamp with time zone |           | not null | 
 newspaper_articles | bigint                   |           |          | 
Indexes:
    "articles_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "articles_newspapers_articles" FOREIGN KEY (newspaper_articles) REFERENCES newspapers(id) ON DELETE SET NULL
```
無事に、テーブルが生成されていることを確認できました。

## 遭遇したエラー
column（コラム）テーブルを作成しようとしました。
```
# go run -mod=mod entgo.io/ent/cmd/ent new Column
```
すると、
```
ent/ent.go:78:11: column.Table undefined (type string has no field or method Table)
```
というエラーになりました。予約語であることを失念していました。

## 参考文献
[ent リファレンスマニュアル](https://entgo.io/ja/docs/getting-started/)
https://qiita.com/takakou/items/4f8cd3686c7ec84c141d