Ent ORM note 4 — code generation

Time:2021-9-18

In the previous articles, we may often use the command entc. The tool entc brings many functions to. This article mainly arranges the code generation in ENT orm

In the previous example, there is a lack of knowledge, that is, if we want to see the detailed native SQL statements during the execution of ORM, we can open debug to see the code as follows:

client, err := ent.Open("mysql", "root:[email protected](10.211.55.3:3306)/graph_traversal?parseTime=True",ent.Debug())

preface

Initialize A New Schema

You can generate a schema template by using commands like the following:

entc init User Pet

Init will create two schema user.go and pet.go under the ENT / schema directory. If the ENT directory does not exist, it will create

Generate Assets

After adding fields and edges, you can run entc generate in the root directory of the project or use go generate to generate code

go generate ./ent

The generate command generates the following:

  • Client and TX objects for interacting with the graph
  • Crud generator for schema
  • Entity object of each schema type
  • Constants and assertions used to interact with the build
  • Migrate package for SQL dialect

Version Compatibility Between entc And ent

This is mainly about that when using ent in the project, the version of ENT should be the same as the package version of entc, and go modules are used for package management in the project

Code Generation Options

To learn more about the CodeGen option, entc generate – H:

generate go code for the schema directory

Usage:
  entc generate [flags] path

Examples:
  entc generate ./ent/schema
  entc generate github.com/a8m/x

Flags:
      --header string                           override codegen header
  -h, --help                                    help for generate
      --idtype [int int64 uint uint64 string]   type of the id field (default int)
      --storage string                          storage driver to support in codegen (default "sql")
      --target string                           target directory for codegen
      --template strings                        external templates to execute

Storage

Entc can generate assets for SQL and gremlin dialects.

External Templates

Accept the external go template to execute. If the template name is already defined by entc, it will overwrite the existing name. Otherwise, it writes the execution output to a file with the same name as the template. Flag format supports the following files, directories and globs:

entc generate --template  --template glob="path/to/*.tmpl" ./ent/schema

More information and examples can be found in the external template document

Use entc As A Package

Another option to run entc is to use it as a package, as follows:

package main

import (
    "log"

    "github.com/facebook/ent/entc"
    "github.com/facebook/ent/entc/gen"
    "github.com/facebook/ent/schema/field"
)

func main() {
    err := entc.Generate("./schema", &gen.Config{
        Header: "// Your Custom Header",
        IDType: &field.TypeInfo{Type: field.TypeInt},
    })
    if err != nil {
        log.Fatal("running ent codegen:", err)
    }
}

Schema Description

If you want to get the description information of the schema defined by us, you can use the following command:

entc describe ./ent/schema

In the previous example, the implementation effect is as follows:

User:
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        | Field |  Type  | Unique | Optional | Nillable | Default | UpdateDefault | Immutable |       StructTag       | Validators |
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        | id    |   | false  | false    | false    | false   | false         | false     | json:"id,omitempty"   |          0 |
        | name  | string | false  | false    | false    | false   | false         | false     | json:"name,omitempty" |          0 |
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        +-----------+------+---------+-----------+----------+--------+----------+
        |   Edge    | Type | Inverse |  BackRef  | Relation | Unique | Optional |
        +-----------+------+---------+-----------+----------+--------+----------+
        | followers | User | true    | following | M2M      | false  | true     |
        | following | User | false   |           | M2M      | false  | true     |
        +-----------+------+---------+-----------+----------+--------+----------+

CRUD API

Create A New Client

MySQL

package main

import (
    "log"

    "/ent"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    client, err := ent.Open("mysql", ":@tcp(:)/?parseTime=True")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()
}

PostgreSQL

package main

import (
    "log"

    "/ent"

    _ "github.com/lib/pq"
)

func main() {
    client, err := ent.Open("postgres","host= port= user= dbname= password=")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()
}

SQLite

package main

import (
    "log"

    "/ent"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()
}

Gremlin (AWS Neptune)

package main

import (
    "log"

    "/ent"
)

func main() {
    client, err := ent.Open("gremlin", "http://localhost:8182")
    if err != nil {
        log.Fatal(err)
    }
}

Create An Entity

Save a user.

a8m, err := client.User.    // UserClient.
    Create().               // User create builder.
    SetName("a8m").         // Set field value.
    SetNillableAge(age).    // Avoid nil checks.
    AddGroups(g1, g2).      // Add many edges.
    SetSpouse(nati).        // Set unique edge.
    Save(ctx)               // Create and return.

SaveX a pet; Unlike Save, SaveX panics if an error occurs.

pedro := client.Pet.    // PetClient.
    Create().           // Pet create builder.
    SetName("pedro").   // Set field value.
    SetOwner(a8m).      // Set owner (unique edge).
    SaveX(ctx)          // Create and return.

Create Many

Save a bulk of pets

names := []string{"pedro", "xabi", "layla"}
bulk := make([]*ent.PetCreate, len(names))
for i, name := range names {
    bulk[i] = client.Pet.Create().SetName(name).SetOwner(a8m)
}
pets, err := client.Pet.CreateBulk(bulk...).Save(ctx)

Update One

Update an entity returned from the database

a8m, err = a8m.Update().    // User update builder.
    RemoveGroup(g2).        // Remove specific edge.
    ClearCard().            // Clear unique edge.
    SetAge(30).             // Set field value
    Save(ctx)               // Save and return.

Update By ID

pedro, err := client.Pet.   // PetClient.
    UpdateOneID(id).        // Pet update builder.
    SetName("pedro").       // Set field name.
    SetOwnerID(owner).      // Set unique edge, using id.
    Save(ctx)               // Save and return.

Update Many

Filter with assertions

n, err := client.User.          // UserClient.
    Update().                   // Pet update builder.
    Where(                      //
        user.Or(                // (age >= 30 OR name = "bar") 
            user.AgeEQ(30),     //
            user.Name("bar"),   // AND
        ),                      //  
        user.HasFollowers(),    // UserHasFollowers()  
    ).                          //
    SetName("foo").             // Set field name.
    Save(ctx)                   // exec and return.

Query through edge assertion

n, err := client.User.          // UserClient.
    Update().                   // Pet update builder.
    Where(                      // 
        user.HasFriendsWith(    // UserHasFriendsWith (
            user.Or(            //   age = 20
                user.Age(20),   //      OR
                user.Age(30),   //   age = 30
            )                   // )
        ),                      //
    ).                          //
    SetName("a8m").             // Set field name.
    Save(ctx)                   // exec and return.

Query The Graph

Get followers for all users

users, err := client.User.      // UserClient.
    Query().                    // User query builder.
    Where(user.HasFollowers()). // filter only users with followers.
    All(ctx)                    // query and return.

Get all followers of a specific user; Start traversing from a node in the graph

users, err := a8m.
    QueryFollowers().
    All(ctx)

Get the names of all pets

names, err := client.Pet.
    Query().
    Select(pet.FieldName).
    Strings(ctx)

Get the names and ages of all pets

var v []struct {
    Age  int    `json:"age"`
    Name string `json:"name"`
}
err := client.Pet.
    Query().
    Select(pet.FieldAge, pet.FieldName).
    Scan(ctx, &v)
if err != nil {
    log.Fatal(err)
}

Delete One

This is used if we have queried an entity through the client and want to delete this record:

err := client.User.
    DeleteOne(a8m).
    Exec(ctx)

Delete by ID.

err := client.User.
    DeleteOneID(id).
    Exec(ctx)

Delete Many

Delete using assertions

err := client.File.
    Delete().
    Where(file.UpdatedAtLT(date))
    Exec(ctx)

Mutation

Each schema generated through entc init has its own mutaion. For example, we use entc init user pet, which is found in the code generated through go generate. / ENTent/mutation.go

Defined in this file:

.....
// UserMutation represents an operation that mutate the Users
// nodes in the graph.
type UserMutation struct {
	config
	op            Op
	typ           string
	id            *int
	name          *string
	age           *int
	addage        *int
	clearedFields map[string]struct{}
	done          bool
	oldValue      func(context.Context) (*User, error)
}

.....

// PetMutation represents an operation that mutate the Pets
// nodes in the graph.
type PetMutation struct {
	config
	op            Op
	typ           string
	id            *int
	name          *string
	age           *int
	addage        *int
	clearedFields map[string]struct{}
	done          bool
	oldValue      func(context.Context) (*Pet, error)
}

For example, allUserBuilders share the same usermutaion object, and the left and right builder types inherit the commonent.MutationInterface.

The user builders mentioned here refer to user schemaUserCreateUserDeleteUserQueryUserUpdateObject, in the code generated by go generate, we can

./ent/user_create.go、./ent/user_delete.go、./ent/user_query.go、./ent/user_update.goThe following definitions can be seen in the file:

// ./ent/user_create.go

// UserCreate is the builder for creating a User entity.
type UserCreate struct {
	config
	mutation *UserMutation
	hooks    []Hook
}


//./ent/user_delete.go
// UserDelete is the builder for deleting a User entity.
type UserDelete struct {
	config
	hooks      []Hook
	mutation   *UserMutation
	predicates []predicate.User
}

// ./ent/user_query.go
// UserQuery is the builder for querying User entities.
type UserQuery struct {
	config
	limit      *int
	offset     *int
	order      []OrderFunc
	unique     []string
	predicates []predicate.User
	// intermediate query (i.e. traversal path).
	sql  *sql.Selector
	path func(context.Context) (*sql.Selector, error)
}

// ./ent/user_update.go
// UserUpdate is the builder for updating User entities.
type UserUpdate struct {
	config
	hooks      []Hook
	mutation   *UserMutation
	predicates []predicate.User
}

In the following example, both ent.usercreate and ent.userupdate use a common method to operate on the age and name columns:

package main

import (
   "context"
   "log"

   _ "github.com/go-sql-driver/mysql"
   "github.com/peanut-cc/ent_orm_notes/aboutMutaion/ent"
)

func main() {
   client, err := ent.Open("mysql", "root:[email protected](10.211.55.3:3306)/aboutMutaion?parseTime=True")
   if err != nil {
      log.Fatal(err)
   }
   defer client.Close()
   ctx := context.Background()
   // run the auto migration tool
   if err := client.Schema.Create(ctx); err != nil {
      log.Fatalf("failed creating schema resources:%v", err)
   }
   Do(ctx, client)
}

func Do(ctx context.Context, client *ent.Client) {
   creator := client.User.Create()
   SetAgeName(creator.Mutation())
   creator.SaveX(ctx)
   updater := client.User.UpdateOneID(1)
   SetAgeName(updater.Mutation())
   updater.SaveX(ctx)
}

// SetAgeName sets the age and the name for any mutation.
func SetAgeName(m *ent.UserMutation) {
   m.SetAge(32)
   m.SetName("Ariel")
}

In some cases, you want to apply the same method to multiple different types. In this case, either use the general ent.mutation interface or implement an interface yourself. The code is as follows:

func Do2(ctx context.Context, client *ent.Client) {
   creator1 := client.User.Create().SetAge(18)
   SetName(creator1.Mutation(), "a8m")
   creator1.SaveX(ctx)
   creator2 := client.Pet.Create().SetAge(16)
   SetName(creator2.Mutation(), "pedro")
   creator2.SaveX(ctx)
}

// SetNamer wraps the 2 methods for getting
// and setting the "name" field in mutations.
type SetNamer interface {
   SetName(string)
   Name() (string, bool)
}

func SetName(m SetNamer, name string) {
   if _, exist := m.Name(); !exist {
      m.SetName(name)
   }
}

Graph Traversal

The following graph will be used in the example in this section

er-traversal-graph

er-traversal-graph-gopher

The above traversal starts from a group entity, continues to its admin (edge), continues to its friends (edge), obtains their pets (edge), obtains each pet’s friends (edge), and requests their owners

func Traverse(ctx context.Context, client *ent.Client) error {
    owner, err := client.Group.         // GroupClient.
        Query().                        // Query builder.
        Where(group.Name("Github")).    // Filter only Github group (only 1).
        QueryAdmin().                   // Getting Dan.
        QueryFriends().                 // Getting Dan's friends: [Ariel].
        QueryPets().                    // Their pets: [Pedro, Xabi].
        QueryFriends().                 // Pedro's friends: [Coco], Xabi's friends: [].
        QueryOwner().                   // Coco's owner: Alex.
        Only(ctx)                       // Expect only one entity to return in the query.
    if err != nil {
        return fmt.Errorf("failed querying the owner: %v", err)
    }
    fmt.Println(owner)
    // Output:
    // User(id=3, age=37, name=Alex)
    return nil
}

How about the following traversal?

er-traversal-graph-gopher-query

We want to get some group administrators whose owners (edges) of all pets are friends (edges).

func Traverse2(ctx context.Context, client *ent.Client) error {
	pets, err := client.Pet.
		Query().
		Where(
			pet.HasOwnerWith(
				user.HasFriendsWith(
					user.HasManage(),
				),
			),
		).
		All(ctx)
	if err != nil {
		return fmt.Errorf("failed querying the pets: %v", err)
	}
	fmt.Println(pets)
	// Output:
	// [Pet(id=1, name=Pedro) Pet(id=2, name=Xabi)]
	return nil
}

In the above query, all pets are queried. The conditions are: the pet must have a owner, the pet owner must have friends, and the owner must belong to the administrator

Eager Loading

Ent supports querying through their edges and adds the associated entities to the returned object

Understand through the following example:

er-group-users

Query all users and their pets in the above relationship. The code is as follows:

func edgerLoading(ctx context.Context, client *ent.Client) {
   users, err := client.User.Query().WithPets().All(ctx)
   if err != nil {
      log.Fatalf("user query failed:%v", err)
   }
   log.Println(users)
   for _, u := range users {
      for _, p := range u.Edges.Pets {
         log.Printf("user (%v) -- > Pet (%v)\n", u.Name, p.Name)
      }
   }

}

The complete code is in: https://github.com/peanut-cc/ent_ orm_ notes/graph_ traversal

The query results are as follows:

2020/09/01 20:09:07 [User(id=1, age=29, name=Dan) User(id=2, age=30, name=Ariel) User(id=3, age=37, name=Alex) User(id=4, age=18, name=peanut)]
2020/09/01 20:09:07 user (Ariel) -- > Pet (Pedro)
2020/09/01 20:09:07 user (Ariel) -- > Pet (Xabi)
2020/09/01 20:09:07 user (Alex) -- > Pet (Coco)

Preloading allows you to query multiple associations, including nested associations. You can also filter, sort or restrict query results, such as:

func edgerLoading2(ctx context.Context, client *ent.Client) {
   users, err := client.User.
      Query().
      Where(
         user.AgeGT(18),
      ).
      WithPets().
      WithGroups(func(q *ent.GroupQuery) {
         q.Limit(5)
         q.WithUsers().Limit(5)
      }).All(ctx)
   if err != nil {
      log.Fatalf("user query failed:%v", err)
   }
   log.Println(users)
   for _, u := range users {
      for _, p := range u.Edges.Pets {
         log.Printf("user (%v) --> Pet (%v)\n", u.Name, p.Name)
      }
      for _, g := range u.Edges.Groups {
         log.Printf("user (%v) -- Group (%v)\n", u.Name, g.Name)
      }
   }

}

Each query builder has a list of methods in the formWith(...func(Query))

Represents the name of the edge (likeWithGroups) ,< N>Represents the edge type (likeGroupQuery)。

Note that only SQL dialects support this feature

Aggregation

Group By

Group all users by their name and age fields and calculate their total age.

package main

import (
	"context"
	"log"

	"github.com/peanut-cc/ent_orm_notes/groupBy/ent/user"

	_ "github.com/go-sql-driver/mysql"

	"github.com/peanut-cc/ent_orm_notes/groupBy/ent"
)

func main() {
	client, err := ent.Open("mysql", "root:[email protected](10.211.55.3:3306)/groupBy?parseTime=True",
		ent.Debug())
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()
	ctx := context.Background()
	// run the auto migration tool
	if err := client.Schema.Create(ctx); err != nil {
		log.Fatalf("failed creating schema resources:%v", err)
	}
	GenData(ctx, client)
	Do(ctx, client)

}

func GenData(ctx context.Context, client *ent.Client) {
	client.User.Create().SetName("peanut").SetAge(18).SaveX(ctx)
	client.User.Create().SetName("jack").SetAge(20).SaveX(ctx)
	client.User.Create().SetName("steve").SetAge(22).SaveX(ctx)
	client.User.Create().SetName("peanut-cc").SetAge(18).SaveX(ctx)
	client.User.Create().SetName("jack-dd").SetAge(18).SaveX(ctx)
}

func Do(ctx context.Context, client *ent.Client) {
	var v []struct {
		Name  string `json:"name"`
		Age   int    `json:"age"`
		Sum   int    `json:"sum"`
		Count int    `json:"count"`
	}
	client.User.
		Query().
		GroupBy(
			user.FieldName, user.FieldAge,
		).
		Aggregate(
			ent.Count(),
			ent.Sum(user.FieldAge),
		).
		ScanX(ctx, &v)
	log.Println(v)

}

Group by one field, for example:

func Do2(ctx context.Context, client *ent.Client) {
	names := client.User.Query().GroupBy(user.FieldName).StringsX(ctx)
	log.Println(names)
}

Predicates

Field Predicates

  • Bool:
    • =, !=
  • Numberic:
    • =, !=, >, =, <=,
    • IN, NOT IN
  • Time:
    • =, !=, >, =, <=
    • IN, NOT IN
  • String:
    • =, !=, >, =, <=
    • IN, NOT IN
    • Contains, HasPrefix, HasSuffix
    • ContainsFold, EqualFold (SQL specific)
  • Optional fields:
    • IsNil, NotNil

Edge Predicates

HasEdgeFor example, to query the owners of all pets, use:

client.Pet.
      Query().
      Where(pet.HasOwner()).
      All(ctx)

HasEdgeWith

client.Pet.
      Query().
      Where(pet.HasOwnerWith(user.Name("a8m"))).
      All(ctx)

Negation (NOT)

client.Pet.
    Query().
    Where(pet.Not(pet.NameHasPrefix("Ari"))).
    All(ctx)

Disjunction (OR)

client.Pet.
    Query().
    Where(
        pet.Or(
            pet.HasOwner(),
            pet.Not(pet.HasFriends()),
        )
    ).
    All(ctx)

Conjunction (AND)

client.Pet.
    Query().
    Where(
        pet.And(
            pet.HasOwner(),
            pet.Not(pet.HasFriends()),
        )
    ).
    All(ctx)

Custom Predicates

Custom predictions can be useful if you want to write your own dialect specific logic.

pets := client.Pet.
    Query().
    Where(predicate.Pet(func(s *sql.Selector) {
        s.Where(sql.InInts(pet.OwnerColumn, 1, 2, 3))
    })).
    AllX(ctx)

Paging And Ordering

Limit

Limit query results to n entities.

users, err := client.User.
    Query().
    Limit(n).
    All(ctx)

Offset

Sets the first maximum number returned from the query.

users, err := client.User.
    Query().
    Offset(10).
    All(ctx)

Ordering

Order returns entities sorted by the value of one or more fields.

users, err := client.User.Query().
    Order(ent.Asc(user.FieldName)).
    All(ctx)

Extended reading

  • https://entgo.io/

Recommended Today

Supervisor

Supervisor [note] Supervisor – H view supervisor command help Supervisorctl – H view supervisorctl command help Supervisorctl help view the action command of supervisorctl Supervisorctl help any action to view the use of this action 1. Introduction Supervisor is a process control system. Generally speaking, it can monitor your process. If the process exits abnormally, […]