Quick Introduction

    • 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.

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

    Create Your First Schema

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

    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. package schema
    2. import (
    3. "entgo.io/ent"
    4. "entgo.io/ent/schema/field"
    5. )
    6. // Fields of the User.
    7. func (User) Fields() []ent.Field {
    8. return []ent.Field{
    9. field.Int("age").
    10. Positive(),
    11. field.String("name").
    12. Default("unknown"),
    13. }
    14. }

    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 ent.Client.

    • SQLite
    • PostgreSQL
    • MySQL

    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/start.go

    Now, we’re ready to create our user. Let’s call this function CreateUser for the sake of example:

    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. return u, nil
    11. }

    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. package main
    2. import (
    3. "log"
    4. "entdemo/ent"
    5. "entdemo/ent/user"
    6. )
    7. func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    8. Query().
    9. Where(user.Name("a8m")).
    10. // `Only` fails if no user found,
    11. // or more than 1 user returned.
    12. Only(ctx)
    13. if err != nil {
    14. return nil, fmt.Errorf("failed querying user: %w", err)
    15. }
    16. log.Println("user returned: ", u)
    17. return u, nil
    18. }

    Add Your First Edge (Relation)

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

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

    entdemo/start.go

    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.

    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.

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

    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 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. return []ent.Edge{
    3. edge.To("users", User.Type),
    4. }
    5. }

    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:

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

      entdemo/start.go

    3. Get all groups that have users (query with a look-aside predicate):

      entdemo/start.go

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

    Full Example

    The full example exists in .