What is Ent. Ent is an entity framework for Go.
notes:
- You can learn about Ent in official doc https://entgo.io/. This article was written during learning Ent, for personal use. However, you might find it useful for yourself.
- for full code of this example, see main.go
We will design ER models and generate Ent entities by example. Let's start. Given we have a Player model.
type Player struct {
}
to turn Player
model into an Ent
's entity, it is just enough to embed ent.Schema
, so it becomes:
import (
"entgo.io/ent" // ent lib
)
type Player struct {
ent.Schema
}
After we have the scheme embedded, we will need to define our schema parts such as Fields
, Relationships
and, if necessary, some database specifics like indexes
.
Let's add some fields to our Player
entity, e.g. nickname
, email
and scores
:
import (
"entgo.io/ent/schema/field" // ent.Field struct
)
func (Player) Fields() []ent.Field {
return []ent.Field{
field.String("nickname"),
field.String("email"),
field.Int("scores"),
}
}
In the official introduction of Entgo, it says to init model by running codegeltool. Go ahead and install it first:
go get entgo.io/ent/cmd/ent
After you got a tool, time to generate your first template:
go run entgo.io/ent/cmd/ent init Player
The init
command will create a folder ent
which will contain:
./ent
schema/
player.go <---- your template for Player struct
generate.go
You will find it has the same structure as we describe in the beginning, plus an additional method:
func (Player) Edges() []ent.Edge {
return nil
}
Now, you can copy your Field()
method into ./ent/schema/player.go
. To verify that you did all right, run describe
command, you should get something like the below:
➜ entgo-example git:(main) go run entgo.io/ent/cmd/ent describe ./ent/schema
Player:
+----------+--------+--------+----------+----------+---------+---------------+-----------+---------------------------+------------+
| Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators |
+----------+--------+--------+----------+----------+---------+---------------+-----------+---------------------------+------------+
| id | int | false | false | false | false | false | false | json:"id,omitempty" | 0 |
| nickname | string | false | false | false | false | false | false | json:"nickname,omitempty" | 0 |
| email | string | false | false | false | false | false | false | json:"email,omitempty" | 0 |
| scores | int | false | false | false | false | false | false | json:"scores,omitempty" | 0 |
We can remove ./player.go
file, as it is not gonna be needed anymore. From now on, you will edit your models in ./ent/schema
folder.
Now, you might wonder how do we call SQL queries. To do that, you would need to generate your SQL interfaces for your schema. Invoke the following:
Full version:
go run entgo.io/ent/cmd/ent generate ./ent/schema
Short version:
go generate ./ent
Inspect results in ./ent
. Previously, we had there our scheme file. Now, there are a bunch of files, do not edit them yet. Let's understand their purpose by using it.
let's pick our Postgres
as our RDMS engine. Steps are:
Spin up Postgres
server in docker:
docker run -it --rm --name dev-postgres -e POSTGRES_DB=entgo_example -e POSTGRES_PASSWORD=topsecret -p 5432:5432 postgres
our connection settings:
Host: localhost
Port: 5432
User: postgres
Password: topsecret
Database: entgo_example
Instead of using database/sql
connect to our db:
import (
_ "github.com/lib/pq" // indirect import
)
db, err := sql.Open("postgres", "user=postgres password=topsecret dbname=entgo_example sslmode=disable")
Ent
provides the in-box solution with the same interface:
import (
"github.com/Funfun/entgo-example/ent"
_ "github.com/lib/pq" // pq import should here remain anyways
)
client, err := ent.Open("postgres", "user=postgres password=topsecret dbname=entgo_example sslmode=disable")
At this moment, we will be ready to use our database, but before we create our first Player
record, let's have a look into our SQL schema with again, built-in Ent
tool fot it:
// Dump migration changes to stdout.
if err := client.Schema.WriteTo(ctx, os.Stdout); err != nil {
log.Fatalf("failed printing schema changes: %v", err)
}
// results in:
/*
BEGIN;
CREATE TABLE IF NOT EXISTS "players"("id" bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, "nickname" varchar NOT NULL, "email" varchar NOT NULL, "scores" bigint NOT NULL, PRIMARY KEY("id"));
COMMIT;
*/
if that looks good, let's run migration against our database:
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
let's check it in DB:
➜ entgo-example git:(main) ✗ psql -U postgres -d entgo_example -h localhost -p 5432
Password for user postgres:
psql (11.5, server 14.0 (Debian 14.0-1.pgdg110+1))
WARNING: psql major version 11, server major version 14.
Some psql features might not work.
Type "help" for help.
entgo_example=# \dt
List of relations
Schema | Name | Type | Owner
--------+---------+-------+----------
public | players | table | postgres
(1 row)
We moving to the next thing. Creating our first entry in our players
table.
player, err := client.Player.Create().
SetNickname("John").
SetEmail("info@tsyren.org").
SetScores(1).
Save(ctx)
if err != nil {
log.Fatalln(err)
}
// output:
// 2021/11/14 13:57:57 New player was created Player(id=1, nickname=John, email=info@tsyren.org, scores=1)
verifying in DB:
entgo_example=# SELECT * FROM players;
id | nickname | email | scores
----+----------+-----------------+--------
1 | John | info@tsyren.org | 1
(1 row)
let's verify it using Ent
query interface:
import (
playerEnt "github.com/Funfun/entgo-example/ent/player"
)
player, err = client.Player.Query().Where(playerEnt.Nickname("John")).Only(ctx)
if err != nil {
log.Fatalln(err)
}
// output:
// 2021/11/14 14:06:13 Found player was created Player(id=3, nickname=John, email=info@tsyren.org, scores=1)
Type used to describe relation in Ent
is called edge and defined over model like:
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("games", Game.Type),
}
}
and can be defined as below:
Relation | Definition by example | Usage syntax |
---|---|---|
Has-Many | edge.To("games", Game.Type) |
(create) client.User.Create().SetName("john").AddGames(gta, diablo2) |
(query) john.QueryGames().All(ctx) |
||
Belongs-To | edge.From("author", User.Type).Ref("books").Unique() |
(create) client.Book.Create().SetAuthor() |
(query) myBook.QueryAuthor().Only(ctx) |
||
Many-To-Many | Group edge.To("users", User.Type) |
|
User edge.To("cars", Car.Type), edge.From("groups", Group.Type).Ref("users") |
Example for create:
// Edges of the Book.
func (Book) Edges() []ent.Edge {
return []ent.Edge{
edge.From("author", Person.Type).Ref("books").Unique(),
}
}
// Edges of the Person.
func (Person) Edges() []ent.Edge {
return []ent.Edge{
edge.To("books", Book.Type),
}
}
create person:
leo, err := client.Person.Create().SetName("Leo Tolstoy").Save(ctx)
if err != nil {
log.Fatalln(err)
}
// SQL: INSERT INTO "persons" ("name") VALUES ($1) RETURNING "id" args=[Leo Tolstoy]
create book:
publishedAt, _ := time.Parse(time.RFC3339, "1869-01-01T15:04:06Z07:00")
warAndPeace, err := client.Book.Create().SetTitle("War and Peace").SetAuthor(leo).SetCreatedAt(publishedAt).Save(ctx)
if err != nil {
log.Fatalln(err)
}
// INSERT INTO "books" ("title", "created_at", "person_books") VALUES ($1, $2, $3) RETURNING "id" args=[War and Peace 0001-01-01 00:00:00 +0000 UTC 5]
note that the column that describes the book's author is called person_books
which might not be really semantically readable for debugging.
Query example:
we query over Book
with Where
res, err := client.Book.Query().Where(book.Title("War and Peace")).Only(ctx)
// SQL: SELECT DISTINCT "books"."id", "books"."title", "books"."created_at" FROM "books" WHERE "books"."title" = $1 LIMIT 2 args=[War and Peace]
// it returns:
// => Book(id=4, title=War and Peace, created_at=Mon Jan 1 00:00:00 0001)
however, if we set to query Author, in the same query, it will result in instance of type Author
:
author, err := client.Book.Query().Where(book.Title("War and Peace")).QueryAuthor().Only(ctx)
// SQL: SELECT DISTINCT "persons"."id", "persons"."name" FROM "persons" JOIN (SELECT "books"."person_books" FROM "books" WHERE "books"."title" = $1) AS "t1" ON "persons"."id" = "t1"."person_books" LIMIT 2 args=[War and Peace]
// returns:
// => Person(id=5, name=Leo Tolstoy)
or query from author perpective:
per, err := client.Person.Query().Where(person.HasBooks(), person.Name("Leo Tolstoy")).Only(ctx)
// code returns:
// => Person(id=5, name=Leo Tolstoy)
relevant SQL:
SELECT DISTINCT "persons"."id", "persons"."name" FROM "persons" WHERE "persons"."id" IN (SELECT "books"."person_books" FROM "books" WHERE "books"."person_books" IS NOT NULL) AND "persons"."name" = 'Leo Tolstoy' LIMIT 2;
id | name
----+-------------
5 | Leo Tolstoy
(1 row)
Tsyren Ochirov (c) 2021