CRUD API
As mentioned in the introduction section, running ent
on the schemas, will generate the following assets:
Client
andTx
objects used for interacting with the graph.- CRUD builders for each schema type.
- Entity object (Go struct) for each of the schema type.
- Package containing constants and predicates used for interacting with the builders.
- A
migrate
package for SQL dialects. See Migration for more info.
Create A New Client
- SQLite
- PostgreSQL
- MySQL
- Gremlin (AWS Neptune)
package main
import (
"context"
"log"
"entdemo/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.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
// Run the auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres","host=<host> port=<port> user=<user> dbname=<database> password=<pass>")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
// Run the auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/go-sql-driver/mysql"
)
func main() {
client, err := ent.Open("mysql", "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
// Run the auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
package main
import (
"log"
"entdemo/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.
pets, err := client.Pet.CreateBulk(
client.Pet.Create().SetName("pedro").SetOwner(a8m),
client.Pet.Create().SetName("xabi").SetOwner(a8m),
client.Pet.Create().SetName("layla").SetOwner(a8m),
).Save(ctx)
names := []string{"pedro", "xabi", "layla"}
pets, err := client.Pet.MapCreateBulk(names, func(c *ent.PetCreate, i int) {
c.SetName(names[i]).SetOwner(a8m)
}).Save(ctx)
Update One
Update an entity that was returned from the database.
a8m, err = a8m.Update(). // User update builder.
RemoveGroup(g2). // Remove a specific edge.
ClearCard(). // Clear a unique edge.
SetAge(30). // Set a field value.
AddRank(10). // Increment a field value.
AppendInts([]int{1}). // Append values to a JSON array.
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 One With Condition
In some projects, the “update many” operation is not allowed and is blocked using hooks. However, there is still a need to update a single entity by its ID while ensuring it meets a specific condition. In this case, you can use the Where
option as follows:
- By ID
- By Entity
- Update Directly
err := client.Todo.
UpdateOneID(id).
SetStatus(todo.StatusDone).
AddVersion(1).
Where(
todo.Version(currentVersion),
).
Exec(ctx)
switch {
// If the entity does not meet a specific condition,
// the operation will return an "ent.NotFoundError".
case ent.IsNotFound(err):
fmt.Println("todo item was not found")
// Any other error.
case err != nil:
fmt.Println("update error:", err)
}
err := client.Todo.
UpdateOne(node).
SetStatus(todo.StatusDone).
AddVersion(1).
Where(
todo.Version(currentVersion),
).
Exec(ctx)
switch {
// If the entity does not meet a specific condition,
// the operation will return an "ent.NotFoundError".
case ent.IsNotFound(err):
fmt.Println("todo item was not found")
// Any other error.
case err != nil:
fmt.Println("update error:", err)
}
firstTodo, err = firstTodo.
Update().
SetStatus(todo.StatusDone).
AddVersion(1).
Where(
// Ensure the current version matches the one in the database.
todo.Version(firstTodo.Version),
).
Save(ctx)
switch {
// If the entity does not meet a specific condition,
// the operation will return an "ent.NotFoundError".
case ent.IsNotFound(err):
fmt.Println("todo item was not found")
// Any other error.
case err != nil:
fmt.Println("update error:", err)
}
Update Many
Filter using predicates.
n, err := client.User. // UserClient.
Update(). // User update builder.
Where( //
user.Or( // (age >= 30 OR name = "bar")
user.AgeGT(30), //
user.Name("bar"), // AND
), //
user.HasFollowers(), // UserHasFollowers()
). //
SetName("foo"). // Set field name.
Save(ctx) // exec and return.
Query edge-predicates.
n, err := client.User. // UserClient.
Update(). // User 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.
Upsert One
Ent supports upsert) records using the sql/upsert feature-flag.
err := client.User.
Create().
SetAge(30).
SetName("Ariel").
OnConflict().
// Use the new values that were set on create.
UpdateNewValues().
Exec(ctx)
id, err := client.User.
Create().
SetAge(30).
SetName("Ariel").
OnConflict().
// Use the "age" that was set on create.
UpdateAge().
// Set a different "name" in case of conflict.
SetName("Mashraki").
ID(ctx)
// Customize the UPDATE clause.
err := client.User.
Create().
SetAge(30).
SetName("Ariel").
OnConflict().
UpdateNewValues().
// Override some of the fields with a custom update.
Update(func(u *ent.UserUpsert) {
u.SetAddress("localhost")
u.AddCount(1)
u.ClearPhone()
}).
Exec(ctx)
In PostgreSQL, the conflict target is required:
// Setting the column names using the fluent API.
err := client.User.
Create().
SetName("Ariel").
OnConflictColumns(user.FieldName).
UpdateNewValues().
Exec(ctx)
// Setting the column names using the SQL API.
err := client.User.
Create().
SetName("Ariel").
OnConflict(
sql.ConflictColumns(user.FieldName),
).
UpdateNewValues().
Exec(ctx)
// Setting the constraint name using the SQL API.
err := client.User.
Create().
SetName("Ariel").
OnConflict(
sql.ConflictConstraint(constraint),
).
UpdateNewValues().
Exec(ctx)
In order to customize the executed statement, use the SQL API:
id, err := client.User.
Create().
OnConflict(
sql.ConflictColumns(...),
sql.ConflictWhere(...),
sql.UpdateWhere(...),
).
Update(func(u *ent.UserUpsert) {
u.SetAge(30)
u.UpdateName()
}).
ID(ctx)
// INSERT INTO "users" (...) VALUES (...) ON CONFLICT WHERE ... DO UPDATE SET ... WHERE ...
info
Since the upsert API is implemented using the ON CONFLICT
clause (and ON DUPLICATE KEY
in MySQL), Ent executes only one statement to the database, and therefore, only create hooks are applied for such operations.
Upsert Many
err := client.User. // UserClient
CreateBulk(builders...). // User bulk create.
OnConflict(). // User bulk upsert.
UpdateNewValues(). // Use the values that were set on create in case of conflict.
Exec(ctx) // Execute the statement.
Query The Graph
Get all users with followers.
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 the traversal from a node in the graph.
users, err := a8m.
QueryFollowers().
All(ctx)
Get all pets of the followers of a user.
users, err := a8m.
QueryFollowers().
QueryPets().
All(ctx)
Count the number of posts without comments.
n, err := client.Post.
Query().
Where(
post.Not(
post.HasComments(),
)
).
Count(ctx)
More advance traversals can be found in the next section.
Field Selection
Get all pet names.
names, err := client.Pet.
Query().
Select(pet.FieldName).
Strings(ctx)
Get all unique pet names.
names, err := client.Pet.
Query().
Unique(true).
Select(pet.FieldName).
Strings(ctx)
Count the number of unique pet names.
n, err := client.Pet.
Query().
Unique(true).
Select(pet.FieldName).
Count(ctx)
Select partial objects and partial associations. Get all pets and their owners, but select and fill only the ID
and Name
fields.
pets, err := client.Pet.
Query().
Select(pet.FieldName).
WithOwner(func (q *ent.UserQuery) {
q.Select(user.FieldName)
}).
All(ctx)
Scan all pet names and ages to custom struct.
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)
}
Update an entity and return a partial of it.
pedro, err := client.Pet.
UpdateOneID(id).
SetAge(9).
SetName("pedro").
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
Select(pet.FieldName).
Save(ctx)
Delete One
Delete an entity:
err := client.User.
DeleteOne(a8m).
Exec(ctx)
Delete by ID:
err := client.User.
DeleteOneID(id).
Exec(ctx)
Delete One With Condition
In some projects, the “delete many” operation is not allowed and is blocked using hooks. However, there is still a need to delete a single entity by its ID while ensuring it meets a specific condition. In this case, you can use the Where
option as follows:
err := client.Todo.
DeleteOneID(id).
Where(
// Allow deleting only expired todos.
todo.ExpireLT(time.Now()),
).
Exec(ctx)
switch {
// If the entity does not meet a specific condition,
// the operation will return an "ent.NotFoundError".
case ent.IsNotFound(err):
fmt.Println("todo item was not found")
// Any other error.
case err != nil:
fmt.Println("deletion error:", err)
}
Delete Many
Delete using predicates:
affected, err := client.File.
Delete().
Where(file.UpdatedAtLT(date)).
Exec(ctx)
Mutation
Each generated node type has its own type of mutation. For example, all User builders, share the same generated UserMutation
object. However, all builder types implement the generic ent.Mutation interface.
For example, in order to write a generic code that apply a set of methods on both ent.UserCreate
and ent.UserUpdate
, use the UserMutation
object:
func Do() {
creator := client.User.Create()
SetAgeName(creator.Mutation())
updater := client.User.UpdateOneID(id)
SetAgeName(updater.Mutation())
}
// 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 a set of methods on multiple types. For cases like this, either use the generic ent.Mutation
interface, or create your own interface.
func Do() {
creator1 := client.User.Create()
SetName(creator1.Mutation(), "a8m")
creator2 := client.Pet.Create()
SetName(creator2.Mutation(), "pedro")
}
// 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)
}
}