goのentでapi serverを作ってみた
最初は、gormを使っていました。
gormは非常に見通しがよく、わかりやすかったです。
package main
import (
"fmt"
"time"
"os"
"aicard/database"
"net/http"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
type User struct {
Id int `json:"id" param:"id" gorm:"primary_key"`
Name string `json:"name" gorm:"unique,collate:utf8,length:7`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
func getUsers(c echo.Context) error {
users := []User{}
database.DB.Find(&users)
return c.JSON(http.StatusOK, users)
}
func getUser(c echo.Context) error {
user := User{}
if err := c.Bind(&user); err != nil {
return err
}
database.DB.Take(&user)
return c.JSON(http.StatusOK, user)
}
func updateUser(c echo.Context) error {
user := User{}
id := c.Param("id")
if err := c.Bind(&user); err != nil {
return err
}
if err := database.DB.Where("id = ?", id).First(&user).Error; err != nil {
fmt.Println(err)
}
database.DB.Save(&user)
c.JSON(http.StatusOK, user)
return nil
}
func createUser(c echo.Context) error {
user := User{}
if err := c.Bind(&user); err != nil {
return err
}
database.DB.Create(&user)
c.JSON(http.StatusCreated, user)
return nil
}
func deleteUser(c echo.Context) error {
id := c.Param("id")
database.DB.Delete(&User{}, id)
return c.NoContent(http.StatusNoContent)
}
func main() {
e := echo.New()
database.Connect()
sqlDB, _ := database.DB.DB()
defer sqlDB.Close()
e.GET("/", hello)
e.GET("/users", getUsers)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.POST("/users", createUser)
e.DELETE("/users/:id", deleteUser)
port := os.Getenv("PORT")
if port == "" {
port = "1323"
}
e.Logger.Fatal(e.Start(":" + port))
}
しかし、[email protected]
に追加されたようなonconflict
の仕組みがなかったので、entを使いはじめました。
onconflict
は、dbに既に一致するdateがある場合、errを出してくれます。これによって、例えば、同じユーザー名では登録できないようにすることが可能です。もちろん、gormでも可能ですが、entのほうが簡単にできます。
b := h.client.User.Create()
b.SetUsername(*d.Username).OnConflict()
それでは、entの基本的な使い方を紹介していきたいと思います。
まずは、project-dirを作り、その中にgo.modを作ります。go get
などする際は依存関連が示されるgo.sumが更新されていきます。
$ mkdir t
$ cd t
$ go mod init $project
$ cat go.mod
なお、herokuにdeployする際は、go.mod
には以下のようにverを指定してやらなければいけません。
module t
// +heroku goVersion go1.17
go 1.17
go.modのmoduleの名前は、ent/以下のファイルをimportする際に使います。ここでは、project-nameをt
としています。例えば、ent/entryをimportしたい場合は、t/ent/entry
というpathを指定することになります。
entはschemaに作成されたファイルからコントロールし、dbのデータをclientと受け渡しする仕組みを構築できます。schemaを書きかえたら都度、go generateでコードファイルを更新できます。追加機能などもこの仕組からコードファイルに追記されていきます。
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert ./schema
go mod initでgo.modを作成できたら、entをインストールして、entcコマンドを使えるようにします。通常はgoを使用しているとインストールされるbinaryにはpathが通っているはずですが、通っていなくてもgo run
からurlを指定すれば実行可能です。
なお、go mod init
やent init
で指定した名前は各ファイルに書かれますので適切なものにしてください。
$ go get -d entgo.io/ent/cmd/ent
$ ent init Entry
or
$ go run entgo.io/ent/cmd/ent init Entry
$ tree .
.
├── ent
│ ├── generate.go
│ └── schema
│ └── entry.go
├── go.mod
└── go.sum
ここで、entの通常の開発手順としては、(1)entc init
してschemaを作成する、(2)ent/schema/*.go
を編集する、(3)go generate ./ent
を実行する、(4)main.go
などのメインファイルからclientを操作する、(5)go run
やgo build
する、という流れになります。
ということで、次はent/schema/entry.go
を書きます。
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
project-rootからgo generate ./ent
を実行します。すると、先程、initした名前のファイル、entry*.go
が作成されています。これらをimportすることで、開発者はschema
とmain
の編集に集中できるという感じになります。
$ go generate ./ent
$ tree .
.
├── 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
│ ├── runtime
│ │ └── runtime.go
│ ├── runtime.go
│ ├── schema
│ │ └── entry.go
│ └── tx.go
├── go.mod
└── go.sum
次は、clientを操作するmain.go
を作成していきます。project-rootにmain.goを置き、それをbuildします。
package main
import (
"log"
"t/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.Fatal(err)
}
defer client.Close()
}
特に重要になる部分は以下のような書き方がされる箇所です。apiへpostされたdateを受け取り、それをdbに保存し、返すための処理を書くのに使われることが多い。
e := client.Entry.Create()
e.SetUser()
また、各種ファイルでのimportはproject-nameからのpathとなります。
import (
"t/ent"
"t/ent/entry"
)
もちろん、projectをgithubにおいているなら、github.com/$USER/$PROJECT/ent
としてもいいです。この場合、go.modのmoduleもそのように書き換えてください。
ここまでがentの基本的な部分です。ここからは、heroku-postgres
やonconflict
に対応します。herokuはportが変わりますので、envから受け取らなければなりません。
まずは、heroku-postgresに対応します。
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Entry struct {
ent.Schema
}
func (Entry) Fields() []ent.Field {
return []ent.Field{
field.String("user").
MaxLen(8).
Unique().
Immutable(),
field.Int("first").
Unique().
Immutable(),
field.Time("created_at").
Immutable().
Default(func() time.Time {
return time.Now()
}),
}
}
func (Entry) Edges() []ent.Edge {
return nil
}
package main
import (
"time"
"t/ent"
"context"
"log"
"os"
"database/sql"
entsql "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect"
_ "github.com/jackc/pgx/v4/stdlib"
)
type User struct {
user string `json:"user"`
first int `json:"first"`
created_at time.Time `json:"created_at"`
}
func Open(databaseUrl string) *ent.Client {
db, err := sql.Open("pgx", databaseUrl)
if err != nil {
log.Fatal(err)
}
drv := entsql.OpenDB(dialect.Postgres, db)
return ent.NewClient(ent.Driver(drv))
}
func main() {
url := os.Getenv("DATABASE_URL") + "?sslmode=require"
client := Open(url)
ctx := context.Background()
if err := client.Schema.Create(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()
}
次に、onconflict
への対応です。
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert ./schema
$ go generate ./...
これでonconflict
が使えるようになります。
b := h.client.Entry.Create()
b.SetUser(*d.User).OnConflict()
例えば、作成したapiにpostすると、同じ名前にはerrを返すようになります。
$ curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"syui"}' "https://$APP.herokuapp.com/u"
...ok
$ !!
...err
$ heroku logs
herokuにdeployする場合は、.gitignore
を書いて、git push
します。
$ cat .gitignore
*.db
t
$ git init
$ heroku git:remote -a $APP
$ git add .
$ git commit -m "first"
$ git push -u heroku main
ここからは、entで作るopen-apiの作り方を紹介します。基本的には、ent/entc.go
を作成し、genarateします。
実は、entでopen-apiの作成はなかなかに面倒で、gormのほうが基本的にはわかりやすく、コードも見通しが良いものになるでしょう。ですが、規模が大きくなると、entのほうがよいapiを作れるのではないかと思います。
//go:build ignore
package main
import (
"log"
"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/ariga/ogent"
"github.com/ogen-go/ogen"
)
func main() {
spec := new(ogen.Spec)
oas, err := entoas.NewExtension(entoas.Spec(spec))
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
ogent, err := ogent.NewExtension(spec)
if err != nil {
log.Fatalf("creating ogent extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ogent, oas))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}
$ go get entgo.io/contrib/entoas ariga.io/ogent
$ entc init Todo
$ vim ent/schema/todo.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Todo holds the schema definition for the Todo entity.
type Todo struct {
ent.Schema
}
// Fields of the Todo.
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
field.Bool("done").
Optional(),
}
}
最初に作ったentryは削除します。
$ rm -rf ent/entry*
$ rm -rf ent/schema/entry*
続いて、ent/entc.goからファイルを再構成します。
package ent
//go:generate go run -mod=mod entc.go
package main
import (
"time"
"t/ent"
"net/http"
"context"
"log"
"os"
"database/sql"
entsql "entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect"
_ "github.com/jackc/pgx/v4/stdlib"
_ "github.com/lib/pq"
"t/ent/ogent"
"entgo.io/ent/dialect/sql/schema"
)
type User struct {
user string `json:"user"`
first int `json:"first"`
created_at time.Time `json:"created_at"`
}
func Open(databaseUrl string) *ent.Client {
db, err := sql.Open("pgx", databaseUrl)
if err != nil {
log.Fatal(err)
}
drv := entsql.OpenDB(dialect.Postgres, db)
return ent.NewClient(ent.Driver(drv))
}
func main() {
url := os.Getenv("DATABASE_URL") + "?sslmode=require"
client, err := ent.Open("postgres", url)
//client, err := Open(url)
if err := client.Schema.Create(context.Background(), schema.WithAtlas(true)); err != nil {
log.Fatal(err)
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
srv,err := ogent.NewServer(ogent.NewOgentHandler(client))
if err != nil {
log.Fatal(err)
}
if err := http.ListenAndServe(":" + port, srv); err != nil {
log.Fatal(err)
}
}
$ go get t
$ go run -mod=mod main.go
or
$ go build
$ ./t
--------------------
$ curl -X POST -H "Content-Type: application/json" -d '{"title":"Give ogen and ogent a Star on GitHub"}' localhost:8080/todos
{"id":1,"title":"Give ogen and ogent a Star on GitHub","done":false}
できました。
次は、userを作成するapiを作ってみます。
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Users struct {
ent.Schema
}
func (Users) Fields() []ent.Field {
return []ent.Field{
field.String("user").
NotEmpty().
Immutable().
MaxLen(7).
Unique(),
field.Int("first").
Optional(),
field.Int("draw").
Optional(),
field.Time("created_at").
Optional().
Default(func() time.Time {
return time.Now()
}),
field.Time("updated_at").
Optional().
Default(func() time.Time {
return time.Now()
}),
}
}
func (Users) Edges() []ent.Edge {
return []ent.Edge{}
}
$ go generate ./...
$ go run -mod=mod main.go
--------------------
$ curl -X POST -H "Content-Type: application/json" -d '{"user":"syui"}' localhost:8080/users
{"id":1,"user":"syui","first":0,"draw":0,"created_at":"2022-02-24T10:15:33Z","updated_at":"2022-02-24T10:15:33Z"}
$ !!
{"code":409,"status":"Conflict","errors":"ent: constraint failed: pq: duplicate key value violates unique constraint \"users_user_key\""}
$ curl localhost:8080/users/1
{"id":1,"user":"syui","first":0,"draw":0,"created_at":"2022-02-24T10:15:33Z","updated_at":"2022-02-24T10:15:33Z"}
以上がentでopen-apiを作成する基本的な手順になると思われます。
entは、最初はとっつきにくいですが、触っているうちに慣れてくると思うので、おすすめです。
ref :