Quick Introduction

ent is a simple, yet powerful entity framework for Go, that makes it easy to build and maintain applications with large data-models and sticks with the following principles:

  • Easily model database schema as a graph structure.
  • Define schema as a programmatic Go code.
  • Static typing based on code generation.
  • Database queries and graph traversals are easy to write.
  • Simple to extend and customize using Go templates.

gopher-schema-as-code

Setup A Go Environment

If your project directory is outside GOPATH or you are not familiar with GOPATH, setup a Go module project as follows:

  1. go mod init entdemo

Create Your First Schema

Go to the root directory of your project, and run:

  1. go run -mod=mod entgo.io/ent/cmd/ent new User

The command above will generate the schema for User under entdemo/ent/schema/ directory:

entdemo/ent/schema/user.go

  1. package schema
  2. import "entgo.io/ent"
  3. // User holds the schema definition for the User entity.
  4. type User struct {
  5. ent.Schema
  6. }
  7. // Fields of the User.
  8. func (User) Fields() []ent.Field {
  9. return nil
  10. }
  11. // Edges of the User.
  12. func (User) Edges() []ent.Edge {
  13. return nil
  14. }

Add 2 fields to the User schema:

entdemo/ent/schema/user.go

  1. // Fields of the User.
  2. func (User) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.Int("age").
  5. Positive(),
  6. field.String("name").
  7. Default("unknown"),
  8. }
  9. }

Run go generate from the root directory of the project as follows:

  1. go generate ./ent

This produces the following files:

  1. ent
  2. ├── client.go
  3. ├── config.go
  4. ├── context.go
  5. ├── ent.go
  6. ├── generate.go
  7. ├── mutation.go
  8. ... truncated
  9. ├── schema
  10. └── user.go
  11. ├── tx.go
  12. ├── user
  13. ├── user.go
  14. └── where.go
  15. ├── user.go
  16. ├── user_create.go
  17. ├── user_delete.go
  18. ├── user_query.go
  19. └── user_update.go

Create Your First Entity

To get started, create a new Client to run schema migration and interact with your entities:

  • SQLite
  • PostgreSQL
  • MySQL (MariaDB)

entdemo/start.go

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "entdemo/ent"
  6. _ "github.com/mattn/go-sqlite3"
  7. )
  8. func main() {
  9. client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
  10. if err != nil {
  11. log.Fatalf("failed opening connection to sqlite: %v", err)
  12. }
  13. defer client.Close()
  14. // Run the auto migration tool.
  15. if err := client.Schema.Create(context.Background()); err != nil {
  16. log.Fatalf("failed creating schema resources: %v", err)
  17. }
  18. }

entdemo/start.go

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "entdemo/ent"
  6. _ "github.com/lib/pq"
  7. )
  8. func main() {
  9. client, err := ent.Open("postgres","host=<host> port=<port> user=<user> dbname=<database> password=<pass>")
  10. if err != nil {
  11. log.Fatalf("failed opening connection to postgres: %v", err)
  12. }
  13. defer client.Close()
  14. // Run the auto migration tool.
  15. if err := client.Schema.Create(context.Background()); err != nil {
  16. log.Fatalf("failed creating schema resources: %v", err)
  17. }
  18. }

entdemo/start.go

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "entdemo/ent"
  6. _ "github.com/go-sql-driver/mysql"
  7. )
  8. func main() {
  9. client, err := ent.Open("mysql", "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True")
  10. if err != nil {
  11. log.Fatalf("failed opening connection to mysql: %v", err)
  12. }
  13. defer client.Close()
  14. // Run the auto migration tool.
  15. if err := client.Schema.Create(context.Background()); err != nil {
  16. log.Fatalf("failed creating schema resources: %v", err)
  17. }
  18. }

After running schema migration, we’re ready to create our user. For the sake of this example, let’s name this function CreateUser:

entdemo/start.go

  1. func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
  2. u, err := client.User.
  3. Create().
  4. SetAge(30).
  5. SetName("a8m").
  6. Save(ctx)
  7. if err != nil {
  8. return nil, fmt.Errorf("failed creating user: %w", err)
  9. }
  10. log.Println("user was created: ", u)
  11. return u, nil
  12. }

Query Your Entities

ent generates a package for each entity schema that contains its predicates, default values, validators and additional information about storage elements (column names, primary keys, etc).

entdemo/start.go

  1. func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
  2. u, err := client.User.
  3. Query().
  4. Where(user.Name("a8m")).
  5. // `Only` fails if no user found,
  6. // or more than 1 user returned.
  7. Only(ctx)
  8. if err != nil {
  9. return nil, fmt.Errorf("failed querying user: %w", err)
  10. }
  11. log.Println("user returned: ", u)
  12. return u, nil
  13. }

Add Your First Edge (Relation)

In this part of the tutorial, we want to declare an edge (relation) to another entity in the schema.
Let’s create 2 additional entities named Car and Group with a few fields. We use ent CLI to generate the initial schemas:

  1. go run -mod=mod entgo.io/ent/cmd/ent new Car Group

And then we add the rest of the fields manually:

entdemo/ent/schema/car.go

  1. // Fields of the Car.
  2. func (Car) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.String("model"),
  5. field.Time("registered_at"),
  6. }
  7. }

entdemo/ent/schema/group.go

  1. // Fields of the Group.
  2. func (Group) Fields() []ent.Field {
  3. return []ent.Field{
  4. field.String("name").
  5. // Regexp validation for group name.
  6. Match(regexp.MustCompile("[a-zA-Z_]+$")),
  7. }
  8. }

Let’s define our first relation. An edge from User to Car defining that a user can have 1 or more cars, but a car has only one owner (one-to-many relation).

er-user-cars

Let’s add the "cars" edge to the User schema, and run go generate ./ent:

entdemo/ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("cars", Car.Type),
  5. }
  6. }

We continue our example by creating 2 cars and adding them to a user.

entdemo/start.go

  1. func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
  2. // Create a new car with model "Tesla".
  3. tesla, err := client.Car.
  4. Create().
  5. SetModel("Tesla").
  6. SetRegisteredAt(time.Now()).
  7. Save(ctx)
  8. if err != nil {
  9. return nil, fmt.Errorf("failed creating car: %w", err)
  10. }
  11. log.Println("car was created: ", tesla)
  12. // Create a new car with model "Ford".
  13. ford, err := client.Car.
  14. Create().
  15. SetModel("Ford").
  16. SetRegisteredAt(time.Now()).
  17. Save(ctx)
  18. if err != nil {
  19. return nil, fmt.Errorf("failed creating car: %w", err)
  20. }
  21. log.Println("car was created: ", ford)
  22. // Create a new user, and add it the 2 cars.
  23. a8m, err := client.User.
  24. Create().
  25. SetAge(30).
  26. SetName("a8m").
  27. AddCars(tesla, ford).
  28. Save(ctx)
  29. if err != nil {
  30. return nil, fmt.Errorf("failed creating user: %w", err)
  31. }
  32. log.Println("user was created: ", a8m)
  33. return a8m, nil
  34. }

But what about querying the cars edge (relation)? Here’s how we do it:

entdemo/start.go

  1. func QueryCars(ctx context.Context, a8m *ent.User) error {
  2. cars, err := a8m.QueryCars().All(ctx)
  3. if err != nil {
  4. return fmt.Errorf("failed querying user cars: %w", err)
  5. }
  6. log.Println("returned cars:", cars)
  7. // What about filtering specific cars.
  8. ford, err := a8m.QueryCars().
  9. Where(car.Model("Ford")).
  10. Only(ctx)
  11. if err != nil {
  12. return fmt.Errorf("failed querying user cars: %w", err)
  13. }
  14. log.Println(ford)
  15. return nil
  16. }

Add Your First Inverse Edge (BackRef)

Assume we have a Car object and we want to get its owner; the user that this car belongs to. For this, we have another type of edge called “inverse edge” that is defined using the edge.From function.

er-cars-owner

The new edge created in the diagram above is translucent, to emphasize that we don’t create another edge in the database. It’s just a back-reference to the real edge (relation).

Let’s add an inverse edge named owner to the Car schema, reference it to the cars edge in the User schema, and run go generate ./ent.

entdemo/ent/schema/car.go

  1. // Edges of the Car.
  2. func (Car) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. // Create an inverse-edge called "owner" of type `User`
  5. // and reference it to the "cars" edge (in User schema)
  6. // explicitly using the `Ref` method.
  7. edge.From("owner", User.Type).
  8. Ref("cars").
  9. // setting the edge to unique, ensure
  10. // that a car can have only one owner.
  11. Unique(),
  12. }
  13. }

We’ll continue the user/cars example above by querying the inverse edge.

entdemo/start.go

  1. func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
  2. cars, err := a8m.QueryCars().All(ctx)
  3. if err != nil {
  4. return fmt.Errorf("failed querying user cars: %w", err)
  5. }
  6. // Query the inverse edge.
  7. for _, c := range cars {
  8. owner, err := c.QueryOwner().Only(ctx)
  9. if err != nil {
  10. return fmt.Errorf("failed querying car %q owner: %w", c.Model, err)
  11. }
  12. log.Printf("car %q owner: %q\n", c.Model, owner.Name)
  13. }
  14. return nil
  15. }

Visualize the Schema

If you have reached this point, you have successfully executed the schema migration and created several entities in the database. To view the SQL schema generated by Ent for the database, install Atlas and run the following command:

Install Atlas

To install the latest release of Atlas, simply run one of the following commands in your terminal, or check out the Atlas website:

  • macOS + Linux
  • Homebrew
  • Docker
  • Windows
  1. curl -sSf https://atlasgo.sh | sh
  1. brew install ariga/tap/atlas
  1. docker pull arigaio/atlas
  2. docker run --rm arigaio/atlas --help

If the container needs access to the host network or a local directory, use the --net=host flag and mount the desired directory:

  1. docker run --rm --net=host \
  2. -v $(pwd)/migrations:/migrations \
  3. arigaio/atlas migrate apply
  4. --url "mysql://root:pass@:3306/test"

Download the latest release and move the atlas binary to a file location on your system PATH.

  • ERD Schema
  • SQL Schema

Inspect The Ent Schema

  1. atlas schema inspect \
  2. -u "ent://ent/schema" \
  3. --dev-url "sqlite://file?mode=memory&_fk=1" \
  4. -w

ERD and SQL Schema

erd

Inspect The Ent Schema

  1. atlas schema inspect \
  2. -u "ent://ent/schema" \
  3. --dev-url "sqlite://file?mode=memory&_fk=1" \
  4. --format '{{ sql . " " }}'

SQL Output

  1. -- Create "cars" table
  2. CREATE TABLE `cars` (
  3. `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  4. `model` text NOT NULL,
  5. `registered_at` datetime NOT NULL,
  6. `user_cars` integer NULL,
  7. CONSTRAINT `cars_users_cars` FOREIGN KEY (`user_cars`) REFERENCES `users` (`id`) ON DELETE SET NULL
  8. );
  9. -- Create "users" table
  10. CREATE TABLE `users` (
  11. `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
  12. `age` integer NOT NULL,
  13. `name` text NOT NULL DEFAULT 'unknown'
  14. );

Create Your Second Edge

We’ll continue our example by creating a M2M (many-to-many) relationship between users and groups.

er-group-users

As you can see, each group entity can have many users, and a user can be connected to many groups; a simple “many-to-many” relationship. In the above illustration, the Group schema is the owner of the users edge (relation), and the User entity has a back-reference/inverse edge to this relationship named groups. Let’s define this relationship in our schemas:

entdemo/ent/schema/group.go

  1. // Edges of the Group.
  2. func (Group) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("users", User.Type),
  5. }
  6. }

entdemo/ent/schema/user.go

  1. // Edges of the User.
  2. func (User) Edges() []ent.Edge {
  3. return []ent.Edge{
  4. edge.To("cars", Car.Type),
  5. // Create an inverse-edge called "groups" of type `Group`
  6. // and reference it to the "users" edge (in Group schema)
  7. // explicitly using the `Ref` method.
  8. edge.From("groups", Group.Type).
  9. Ref("users"),
  10. }
  11. }

We run ent on the schema directory to re-generate the assets.

  1. go generate ./ent

Run Your First Graph Traversal

In order to run our first graph traversal, we need to generate some data (nodes and edges, or in other words, entities and relations). Let’s create the following graph using the framework:

re-graph

entdemo/start.go

  1. func CreateGraph(ctx context.Context, client *ent.Client) error {
  2. // First, create the users.
  3. a8m, err := client.User.
  4. Create().
  5. SetAge(30).
  6. SetName("Ariel").
  7. Save(ctx)
  8. if err != nil {
  9. return err
  10. }
  11. neta, err := client.User.
  12. Create().
  13. SetAge(28).
  14. SetName("Neta").
  15. Save(ctx)
  16. if err != nil {
  17. return err
  18. }
  19. // Then, create the cars, and attach them to the users created above.
  20. err = client.Car.
  21. Create().
  22. SetModel("Tesla").
  23. SetRegisteredAt(time.Now()).
  24. // Attach this car to Ariel.
  25. SetOwner(a8m).
  26. Exec(ctx)
  27. if err != nil {
  28. return err
  29. }
  30. err = client.Car.
  31. Create().
  32. SetModel("Mazda").
  33. SetRegisteredAt(time.Now()).
  34. // Attach this car to Ariel.
  35. SetOwner(a8m).
  36. Exec(ctx)
  37. if err != nil {
  38. return err
  39. }
  40. err = client.Car.
  41. Create().
  42. SetModel("Ford").
  43. SetRegisteredAt(time.Now()).
  44. // Attach this car to Neta.
  45. SetOwner(neta).
  46. Exec(ctx)
  47. if err != nil {
  48. return err
  49. }
  50. // Create the groups, and add their users in the creation.
  51. err = client.Group.
  52. Create().
  53. SetName("GitLab").
  54. AddUsers(neta, a8m).
  55. Exec(ctx)
  56. if err != nil {
  57. return err
  58. }
  59. err = client.Group.
  60. Create().
  61. SetName("GitHub").
  62. AddUsers(a8m).
  63. Exec(ctx)
  64. if err != nil {
  65. return err
  66. }
  67. log.Println("The graph was created successfully")
  68. return nil
  69. }

Now when we have a graph with data, we can run a few queries on it:

  1. Get all user’s cars within the group named “GitHub”:

    entdemo/start.go

    1. func QueryGithub(ctx context.Context, client *ent.Client) error {
    2. cars, err := client.Group.
    3. Query().
    4. Where(group.Name("GitHub")). // (Group(Name=GitHub),)
    5. QueryUsers(). // (User(Name=Ariel, Age=30),)
    6. QueryCars(). // (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    7. All(ctx)
    8. if err != nil {
    9. return fmt.Errorf("failed getting cars: %w", err)
    10. }
    11. log.Println("cars returned:", cars)
    12. // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    13. return nil
    14. }
  2. Change the query above, so that the source of the traversal is the user Ariel:

    entdemo/start.go

    1. func QueryArielCars(ctx context.Context, client *ent.Client) error {
    2. // Get "Ariel" from previous steps.
    3. a8m := client.User.
    4. Query().
    5. Where(
    6. user.HasCars(),
    7. user.Name("Ariel"),
    8. ).
    9. OnlyX(ctx)
    10. cars, err := a8m. // Get the groups, that a8m is connected to:
    11. QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
    12. QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
    13. QueryCars(). //
    14. Where( //
    15. car.Not( // Get Neta and Ariel cars, but filter out
    16. car.Model("Mazda"), // those who named "Mazda"
    17. ), //
    18. ). //
    19. All(ctx)
    20. if err != nil {
    21. return fmt.Errorf("failed getting cars: %w", err)
    22. }
    23. log.Println("cars returned:", cars)
    24. // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
    25. return nil
    26. }
  3. Get all groups that have users (query with a look-aside predicate):

    entdemo/start.go

    1. func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
    2. groups, err := client.Group.
    3. Query().
    4. Where(group.HasUsers()).
    5. All(ctx)
    6. if err != nil {
    7. return fmt.Errorf("failed getting groups: %w", err)
    8. }
    9. log.Println("groups returned:", groups)
    10. // Output: (Group(Name=GitHub), Group(Name=GitLab),)
    11. return nil
    12. }

Schema Migration

Ent provides two approaches for running schema migrations: Automatic Migrations and Versioned migrations. Here is a brief overview of each approach:

Automatic Migrations

With Automatic Migrations, users can use the following API to keep the database schema aligned with the schema objects defined in the generated SQL schema ent/migrate/schema.go:

  1. if err := client.Schema.Create(ctx); err != nil {
  2. log.Fatalf("failed creating schema resources: %v", err)
  3. }

This approach is mostly useful for prototyping, development, or testing. Therefore, it is recommended to use the Versioned Migration approach for mission-critical production environments. By using versioned migrations, users know beforehand what changes are being applied to their database, and can easily tune them depending on their needs.

Read more about this approach in the Automatic Migration documentation.

Versioned Migrations

Unlike Automatic Migrations, the Version Migrations approach uses Atlas to automatically generate a set of migration files containing the necessary SQL statements to migrate the database. These files can be edited to meet specific needs and applied using existing migration tools like Atlas, golang-migrate, Flyway, and Liquibase. The API for this approach involves two primary steps.

Generating migrations

  • MySQL
  • MariaDB
  • PostgreSQL
  • SQLite
  1. atlas migrate diff migration_name \
  2. --dir "file://ent/migrate/migrations" \
  3. --to "ent://ent/schema" \
  4. --dev-url "docker://mysql/8/ent"
  1. atlas migrate diff migration_name \
  2. --dir "file://ent/migrate/migrations" \
  3. --to "ent://ent/schema" \
  4. --dev-url "docker://mariadb/latest/test"
  1. atlas migrate diff migration_name \
  2. --dir "file://ent/migrate/migrations" \
  3. --to "ent://ent/schema" \
  4. --dev-url "docker://postgres/15/test?search_path=public"
  1. atlas migrate diff migration_name \
  2. --dir "file://ent/migrate/migrations" \
  3. --to "ent://ent/schema" \
  4. --dev-url "sqlite://file?mode=memory&_fk=1"

Applying migrations

  • MySQL
  • MariaDB
  • PostgreSQL
  • SQLite
  1. atlas migrate apply \
  2. --dir "file://ent/migrate/migrations" \
  3. --url "mysql://root:pass@localhost:3306/example"
  1. atlas migrate apply \
  2. --dir "file://ent/migrate/migrations" \
  3. --url "maria://root:pass@localhost:3306/example"
  1. atlas migrate apply \
  2. --dir "file://ent/migrate/migrations" \
  3. --url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"
  1. atlas migrate apply \
  2. --dir "file://ent/migrate/migrations" \
  3. --url "sqlite://file.db?_fk=1"

Read more about this approach in the Versioned Migrations documentation.

Full Example

The full example exists in GitHub.