👨‍🎓

Facebook の ent を使って簡単なウェブアプリケーションを作る

2020/09/23に公開

はじめに

この記事は Facebook が開発している ORM、ent を使ってどれだけ簡単にアプリケーションを作れるかをチュートリアル的に解説するものです。エンティティを操作する一通りの機能が揃っており、扱いやすいライブラリだと思います。

ent の特徴は以下の通り

  • コードをスキーマとして扱う
    • モデルをGoのオブジェクトとして扱います
  • 簡単なグラフの横断
    • クエリの実行、集合、そしてグラフ構造を簡単に横断します
  • 静的型で明確な API
    • 100% 静的な型でコード生成を使った明確な API を提供します
  • 複数のストレージドライバ
    • MySQL, PostgreSQL, SQLite と Gremlin をサポートします
  • 拡張性
    • Go のテンプレートを使って簡単に拡張できます

この記事では ent を使って1行掲示板を作ってみたいと思います。

前準備

ent を使うには始めに CLI の entc をインストールする必要があります。以下の手順でインストールします。

$ go get github.com/facebook/ent/cmd/entc

予め空の Go のプロジェクトディレクトリを作っておきます。今回は1行掲示板を作るので以下の様に実行しました。

$ go mod init github.com/mattn/entgo-bbs

まずは初期化

まずスキーマを作ります。掲示板の1行をエンティティとして扱うのでまず Entry を作りましょう。

$ entc init Entry

以下の様に幾らかファイルが生成されます。

├── ent
│   ├── generate.go
│   └── schema
│       └── entry.go
├── go.mod
└── go.sum

ent/schema/entry.go が生成されたスキーマ(コード)です。ファイルの中身は以下の様になっています。

package schema

import "github.com/facebook/ent"

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

// Fields of the Entry.
func (Entry) Fields() []ent.Field {
	return nil
}

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

フィールドを追加

デフォルトでは Entry のフィールドは空になっています。コンテンツを示す content と、作成日を示す created_at というフィールドを追加します。

package schema

import (
	"time"

	"github.com/facebook/ent"
	"github.com/facebook/ent/schema/field"
)

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

// Fields of the Entry.
func (Entry) Fields() []ent.Field {
	return []ent.Field{
		field.String("content").
			Default(""),
		field.Time("created_at").
			Default(func() time.Time {
				return time.Now()
			}),
	}

}

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

モデルの生成

ここまで出来たら go generate ./ent を実行します。以下の様にファイルが生成されます。

├── ent
│   ├── client.go
│   ├── config.go
│   ├── context.go
│   ├── ent.go
│   ├── entry
│   │   ├── entry.go
│   │   └── where.go
│   ├── entry.go
│   ├── entry_create.go
│   ├── entry_delete.go
│   ├── entry_query.go
│   ├── entry_update.go
│   ├── enttest
│   │   └── enttest.go
│   ├── generate.go
│   ├── hook
│   │   └── hook.go
│   ├── migrate
│   │   ├── migrate.go
│   │   └── schema.go
│   ├── mutation.go
│   ├── predicate
│   │   └── predicate.go
│   ├── privacy
│   │   └── privacy.go
│   ├── runtime
│   │   └── runtime.go
│   ├── runtime.go
│   ├── schema
│   │   └── entry.go
│   └── tx.go
├── go.mod
└── go.sum

沢山ファイルが生成されましたが、殆どのファイルは開発者が見る必要も変更する必要もありません。これで Entry を扱う為のモデルが生成されています。

アプリケーションの実装

ではアプリケーションを作りましょう。まずはマイグレーションする為のコードです。

package main

import (
	"context"
	"log"

	"github.com/mattn/entgo-bbs/ent"

	_ "github.com/mattn/go-sqlite3"
)

func main() {
	client, err := ent.Open("sqlite3", "file:bbs.sqlite?mode=memory&cache=shared&_fk=1")
	if err != nil {
		log.Fatalf("failed opening connection to sqlite: %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)
	}
}

このコードを書いておくと、以降 ent/schema/entry.go を修正したとしてもテーブルを良しなに扱ってくれる様になります。ではウェブアプリケーションを作りましょう。僕は echo が好きなので echo を使います。以下の様なハンドラを追加します。

e := echo.New()
e.GET("/", func(c echo.Context) error {
	return c.String(http.StatusOK, "")
})
e.POST("/add", func(c echo.Context) error {
	return c.String(http.StatusOK, "")
})
e.Logger.Fatal(e.Start(":8989"))

エンティティの操作

ここでようやく Entry を操作する必要が出てきます。Entry の操作は client を介して行います。選択は Query を使います。Entrycreated_at をソートキーとして昇順、件数 10 件を得ます。

eq := client.Entry.Query().Order(ent.Asc(entry.FieldCreatedAt)).Limit(10)
entries := eq.AllX(context.Background())

Entry の追加は Create を使います。

e := client.Entry.Create()
e.SetContent("zenn 面白いやん?")
e.Save(context.Background())

あとは良しなに

これを後はアプリ側に組み込み、entries はテンプレートエンジンにそのまま渡し、add のハンドラでは Entry の追加とリダイレクトを行えば簡単な1行掲示板の完成です。テンプレートエンジンは Slim テンプレートエンジンの Go 言語版を使いました。

package main

import (
	"context"
	"log"
	"net/http"

	"github.com/labstack/echo"
	"github.com/mattn/entgo-bbs/ent"
	"github.com/mattn/entgo-bbs/ent/entry"

	"github.com/mattn/go-slim"
	_ "github.com/mattn/go-sqlite3"
)

type Payload struct {
	Content string `json:"content"`
}

func main() {
	client, err := ent.Open("sqlite3", "file:bbs.sqlite?cache=shared&_fk=1")
	if err != nil {
		log.Fatalf("failed opening connection to sqlite: %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)
	}

	t, err := slim.ParseFile("template/index.slim")
	if err != nil {
		log.Fatal(err)
	}

	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		eq := client.Entry.Query().Order(ent.Desc(entry.FieldCreatedAt)).Limit(10)
		entries := eq.AllX(context.Background())
		c.Request().Header.Set("content-type", "text/html")
		return t.Execute(c.Response(), map[string]interface{}{
			"entries": entries,
		})
	})
	e.POST("/add", func(c echo.Context) error {
		e := client.Entry.Create()
		e.SetContent(c.FormValue("content"))
		if _, err := e.Save(context.Background()); err != nil {
			log.Println(err.Error())
			return c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
		}
		return c.Redirect(http.StatusFound, "/")
	})
	e.Logger.Fatal(e.Start(":8989"))
}

テンプレートは以下の通り。

doctype 5
html lang="ja"
  head
    meta charset="UTF-8"
    title
  body
    form method=post action=add
      input type=text name=content
      input type=submit
    ul
      - for x in entries
        li = x.Content

起動して1行掲示板になっているのを確認して下さい。

作った物は以下のリポジトリに置いてあります。ご自由に改変して遊んで下さい。

https://github.com/mattn/entgo-bbs

おわりに

Facebook が開発している ORM ライブラリ ent を使って簡単なアプリケーションを実装してみました。ご覧いただいた様にわりと分かりやすいインタフェースになっていると思います。同じ手順を踏めば、おおよそ皆さんも簡単にアプリケーションが実装できると思います。ぜひ作って遊んでみて下さい。

Discussion