Skip to content

tsyber1an/entgo-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Intro to Ent

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)

Ent's edges aka Entity relationships

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages