Skip to main content

Database Locking Techniques with Ent

Locks are one of the fundamental building blocks of any concurrent computer program. When many things are happening simultaneously, programmers reach out to locks to guarantee the mutual exclusion of concurrent access to a resource. Locks (and other mutual exclusion primitives) exist in many different layers of the stack from low-level CPU instructions to application-level APIs (such as sync.Mutex in Go).

When working with relational databases, one of the common needs of application developers is the ability to acquire a lock on records. Imagine an inventory table, listing items available for sale on an e-commerce website. This table might have a column named state that could either be set to available or purchased. avoid the scenario where two users think they have successfully purchased the same inventory item, the application must prevent two operations from mutating the item from an available to a purchased state.

How can the application guarantee this? Having the server check if the desired item is available before setting it to purchased would not be good enough. Imagine a scenario where two users simultaneously try to purchase the same item. Two requests would travel from their browsers to the application server and arrive roughly at the same time. Both would query the database for the item's state, and see the item is available. Seeing this, both request handlers would issue an UPDATE query setting the state to purchased and the buyer_id to the id of the requesting user. Both queries will succeed, but the final state of the record will be that the user who issued the UPDATE query last will be considered the buyer of the item.

Over the years, different techniques have evolved to allow developers to write applications that provide these guarantees to users. Some of them involve explicit locking mechanisms provided by databases, while others rely on more general ACID properties of databases to achieve mutual exclusion. In this post we will explore the implementation of two of these techniques using Ent.

Optimistic Locking#

Optimistic locking (sometimes also called Optimistic Concurrency Control) is a technique that can be used to achieve locking behavior without explicitly acquiring a lock on any record.

On a high-level, this is how optimistic locking works:

  • Each record is assigned a numeric version number. This value must be monotonically increasing. Often Unix timestamps of the latest row update are used.
  • A transaction reads a record, noting its version number from the database.
  • An UPDATE statement is issued to modify the record:
    • The statement must include a predicate requiring that the version number has not changed from its previous value. For example: WHERE id=<id> AND version=<previous version>.
    • The statement must increase the version. Some applications will increase the current value by 1, and some will set it to the current timestamp.
  • The database returns the amount of rows modified by the UPDATE statement. If the number is 0, this means someone else has modified the record between the time we read it, and the time we wanted to update it. The transaction is considered failed, rolled back and can be retried.

Optimistic locking is commonly used in "low contention" environments (situations where the likelihood of two transactions interfering with one another is relatively low) and where the locking logic can be trusted to happen in the application layer. If there are writers to the database that we cannot ensure to obey the required logic, this technique is rendered useless.

Let鈥檚 see how this technique can be employed using Ent.

We start by defining our ent.Schema for a User. The user has an online boolean field to specify whether they are currently online and an int64 field for the current version number.

// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Bool("online"),
field.Int64("version").
DefaultFunc(func() int64 {
return time.Now().UnixNano()
}).
Comment("Unix time of when the latest update occurred")
}
}

Next, let's implement a simple optimistically locked update to our online field:

func optimisticUpdate(tx *ent.Tx, prev *ent.User, online bool) error {
// The next version number for the record must monotonically increase
// using the current timestamp is a common technique to achieve this.
nextVer := time.Now().UnixNano()
// We begin the update operation:
n := tx.User.Update().
// We limit our update to only work on the correct record and version:
Where(user.ID(prev.ID), user.Version(prev.Version)).
// We set the next version:
SetVersion(nextVer).
// We set the value we were passed by the user:
SetOnline(online).
SaveX(context.Background())
// SaveX returns the number of affected records. If this value is
// different from 1 the record must have been changed by another
// process.
if n != 1 {
return fmt.Errorf("update failed: user id=%d updated by another process", prev.ID)
}
return nil
}

Next, let's write a test to verify that if two processes try to edit the same record, only one will succeed:

func TestOCC(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// Create the user for the first time.
orig := client.User.Create().SetOnline(true).SaveX(ctx)
// Read another copy of the same user.
userCopy := client.User.GetX(ctx, orig.ID)
// Open a new transaction:
tx, err := client.Tx(ctx)
if err != nil {
log.Fatalf("failed creating transaction: %v", err)
}
// Try to update the record once. This should succeed.
if err := optimisticUpdate(tx, userCopy, false); err != nil {
tx.Rollback()
log.Fatal("unexpected failure:", err)
}
// Try to update the record a second time. This should fail.
err = optimisticUpdate(tx, orig, false)
if err == nil {
log.Fatal("expected second update to fail")
}
fmt.Println(err)
}

Running our test:

=== RUN TestOCC
update failed: user id=1 updated by another process
--- PASS: Test (0.00s)

Great! Using optimistic locking we can prevent two processes from stepping on each other's toes!

Pessimistic Locking#

As we've mentioned above, optimistic locking isn't always appropriate. For use cases where we prefer to delegate the responsibility for maintaining the integrity of the lock to the databases, some database engines (such as MySQL, Postgres, and MariaDB, but not SQLite) offer pessimistic locking capabilities. These databases support a modifier to SELECT statements that is called SELECT ... FOR UPDATE. The MySQL documentation explains:

A SELECT ... FOR UPDATE reads the latest available data, setting exclusive locks on each row it reads. Thus, it sets the same locks a searched SQL UPDATE would set on the rows.

Alternatively, users can use SELECT ... FOR SHARE statements, as explained by the docs, SELECT ... FOR SHARE:

Sets a shared mode lock on any rows that are read. Other sessions can read the rows, but cannot modify them until your transaction commits. If any of these rows were changed by another transaction that has not yet committed, your query waits until that transaction ends and then uses the latest values.

Ent has recently added support for FOR SHARE/ FOR UPDATE statements via a feature-flag called sql/lock. To use it, modify your generate.go file to include --feature sql/lock:

//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/lock ./schema

Next, let's implement a function that will use pessimistic locking to make sure only a single process can update our User object's online field:

func pessimisticUpdate(tx *ent.Tx, id int, online bool) (*ent.User, error) {
ctx := context.Background()
// On our active transaction, we begin a query against the user table
u, err := tx.User.Query().
// We add a predicate limiting the lock to the user we want to update.
Where(user.ID(id)).
// We use the ForUpdate method to tell ent to ask our DB to lock
// the returned records for update.
ForUpdate(
// We specify that the query should not wait for the lock to be
// released and instead fail immediately if the record is locked.
sql.WithLockAction(sql.NoWait),
).
Only(ctx)
// If we failed to acquire the lock we do not proceed to update the record.
if err != nil {
return nil, err
}
// Finally, we set the online field to the desired value.
return u.Update().SetOnline(online).Save(ctx)
}

Now, let's write a test that verifies that if two processes try to edit the same record, only one will succeed:

func TestPessimistic(t *testing.T) {
ctx := context.Background()
client := enttest.Open(t, dialect.MySQL, "root:pass@tcp(localhost:3306)/test?parseTime=True")
// Create the user for the first time.
orig := client.User.Create().SetOnline(true).SaveX(ctx)
// Open a new transaction. This transaction will acquire the lock on our user record.
tx, err := client.Tx(ctx)
if err != nil {
log.Fatalf("failed creating transaction: %v", err)
}
defer tx.Commit()
// Open a second transaction. This transaction is expected to fail at
// acquiring the lock on our user record.
tx2, err := client.Tx(ctx)
if err != nil {
log.Fatalf("failed creating transaction: %v", err)
}
defer tx.Commit()
// The first update is expected to succeed.
if _, err := pessimisticUpdate(tx, orig.ID, true); err != nil {
log.Fatalf("unexpected error: %s", err)
}
// Because we did not run tx.Commit yet, the row is still locked when
// we try to update it a second time. This operation is expected to
// fail.
_, err = pessimisticUpdate(tx2, orig.ID, true)
if err == nil {
log.Fatal("expected second update to fail")
}
fmt.Println(err)
}

A few things are worth mentioning in this example:

  • Notice that we use a real MySQL instance to run this test against, as SQLite does not support SELECT .. FOR UPDATE.
  • For the simplicity of the example, we used the sql.NoWait option to tell the database to return an error if the lock cannot be acquired. This means that the calling application needs to retry the write after receiving the error. If we don't specify this option, we can create flows where our application blocks until the lock is released and then proceeds without retrying. This is not always desirable but it opens up some interesting design options.
  • We must always commit our transaction. Forgetting to do so can result in some serious issues. Remember that while the lock is maintained, no one can read or update this record.

Running our test:

=== RUN TestPessimistic
Error 3572: Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.
--- PASS: TestPessimistic (0.08s)

Great! We have used MySQL's "locking reads" capabilities and Ent's new support for it to implement a locking mechanism that provides real mutual exclusion guarantees.

Conclusion#

We began this post by presenting the type of business requirements that lead application developers to reach out for locking techniques when working with databases. We continued by presenting two different approaches to achieving mutual exclusion when updating database records and demonstrated how to employ these techniques using Ent.

Have questions? Need help with getting started? Feel free to join our Slack channel.

For more Ent news and updates:

Automatic GraphQL Filter Generation

TL;DR#

We added a new integration to the Ent GraphQL extension that generates type-safe GraphQL filters (i.e. Where predicates) from an ent/schema, and allows users to seamlessly map GraphQL queries to Ent queries.

For example, to get all COMPLETED todo items, we can execute the following:

query QueryAllCompletedTodos {
todos(
where: {
status: COMPLETED,
},
) {
edges {
node {
id
}
}
}
}

The generated GraphQL filters follow the Ent syntax. This means, the following query is also valid:

query FilterTodos {
todos(
where: {
or: [
{
hasParent: false,
status: COMPLETED,
},
{
status: IN_PROGRESS,
hasParentWith: {
priorityLT: 1,
statusNEQ: COMPLETED,
},
}
]
},
) {
edges {
node {
id
}
}
}
}

Background#

Many libraries that deal with data in Go choose the path of passing around empty interface instances (interface{}) and use reflection at runtime to figure out how to map data to struct fields. Aside from the performance penalty of using reflection everywhere, the big negative impact on teams is the loss of type-safety.

When APIs are explicit, known at compile-time (or even as we type), the feedback a developer receives around a large class of errors is almost immediate. Many defects are found early, and development is also much more fun!

Ent was designed to provide an excellent developer experience for teams working on applications with large data-models. To facilitate this, we decided early on that one of the core design principles of Ent is "statically typed and explicit API using code generation". This means, that for every entity a developer defines in their ent/schema, explicit, type-safe code is generated for the developer to efficiently interact with their data. For example, In the Filesystem Example in the ent repository, you will find a schema named File:

// File holds the schema definition for the File entity.
type File struct {
ent.Schema
}
// Fields of the File.
func (File) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Bool("deleted").
Default(false),
field.Int("parent_id").
Optional(),
}
}

When the Ent code-gen runs, it will generate many predicate functions. For example, the following function which can be used to filter Files by their name field:

package file
// .. truncated ..
// Name applies the EQ predicate on the "name" field.
func Name(v string) predicate.File {
return predicate.File(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldName), v))
})
}

GraphQL is a query language for APIs originally created at Facebook. Similar to Ent, GraphQL models data in graph concepts and facilitates type-safe queries. Around a year ago, we released an integration between Ent and GraphQL. Similar to the gRPC Integration, the goal for this integration is to allow developers to easily create API servers that map to Ent, to mutate and query data in their databases.

Automatic GraphQL Filters Generation#

In a recent community survey, the Ent + GraphQL integration was mentioned as one of the most loved features of the Ent project. Until today, the integration allowed users to perform useful, albeit basic queries against their data. Today, we announce the release of a feature that we think will open up many interesting new use cases for Ent users: "Automatic GraphQL Filters Generation".

As we have seen above, the Ent code-gen maintains for us a suite of predicate functions in our Go codebase that allow us to easily and explicitly filter data from our database tables. This power was, until recently, not available (at least not automatically) to users of the Ent + GraphQL integration. With automatic GraphQL filter generation, by making a single-line configuration change, developers can now add to their GraphQL schema a complete set of "Filter Input Types" that can be used as predicates in their GraphQL queries. In addition, the implementation provides runtime code that parses these predicates and maps them into Ent queries. Let's see this in action:

Generating Filter Input Types#

In order to generate input filters (e.g. TodoWhereInput) for each type in your ent/schema package, edit the ent/entc.go configuration file as follows:

// +build ignore
package main
import (
"log"
"entgo.io/contrib/entgql"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
func main() {
ex, err := entgql.NewExtension(
entgql.WithWhereFilters(true),
entgql.WithConfigPath("../gqlgen.yml"),
entgql.WithSchemaPath("<PATH-TO-GRAPHQL-SCHEMA>"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

If you're new to Ent and GraphQL, please follow the Getting Started Tutorial.

Next, run go generate ./ent/.... Observe that Ent has generated <T>WhereInput for each type in your schema. Ent will update the GraphQL schema as well, so you don't need to autobind them to gqlgen manually. For example:

ent/where_input.go
// TodoWhereInput represents a where input for filtering Todo queries.
type TodoWhereInput struct {
Not *TodoWhereInput `json:"not,omitempty"`
Or []*TodoWhereInput `json:"or,omitempty"`
And []*TodoWhereInput `json:"and,omitempty"`
// "created_at" field predicates.
CreatedAt *time.Time `json:"createdAt,omitempty"`
CreatedAtNEQ *time.Time `json:"createdAtNEQ,omitempty"`
CreatedAtIn []time.Time `json:"createdAtIn,omitempty"`
CreatedAtNotIn []time.Time `json:"createdAtNotIn,omitempty"`
CreatedAtGT *time.Time `json:"createdAtGT,omitempty"`
CreatedAtGTE *time.Time `json:"createdAtGTE,omitempty"`
CreatedAtLT *time.Time `json:"createdAtLT,omitempty"`
CreatedAtLTE *time.Time `json:"createdAtLTE,omitempty"`
// "status" field predicates.
Status *todo.Status `json:"status,omitempty"`
StatusNEQ *todo.Status `json:"statusNEQ,omitempty"`
StatusIn []todo.Status `json:"statusIn,omitempty"`
StatusNotIn []todo.Status `json:"statusNotIn,omitempty"`
// .. truncated ..
}
todo.graphql
"""
TodoWhereInput is used for filtering Todo objects.
Input was generated by ent.
"""
input TodoWhereInput {
not: TodoWhereInput
and: [TodoWhereInput!]
or: [TodoWhereInput!]
"""created_at field predicates"""
createdAt: Time
createdAtNEQ: Time
createdAtIn: [Time!]
createdAtNotIn: [Time!]
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time
"""status field predicates"""
status: Status
statusNEQ: Status
statusIn: [Status!]
statusNotIn: [Status!]
# .. truncated ..
}

Next, to complete the integration we need to make two more changes:

1. Edit the GraphQL schema to accept the new filter types:

type Query {
todos(
after: Cursor,
first: Int,
before: Cursor,
last: Int,
orderBy: TodoOrder,
where: TodoWhereInput,
): TodoConnection
}

2. Use the new filter types in GraphQL resolvers:

func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder, where *ent.TodoWhereInput) (*ent.TodoConnection, error) {
return r.client.Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
ent.WithTodoFilter(where.Filter),
)
}

Filter Specification#

As mentioned above, with the new GraphQL filter types, you can express the same Ent filters you use in your Go code.

Conjunction, disjunction and negation#

The Not, And and Or operators can be added using the not, and and or fields. For example:

{
or: [
{
status: COMPLETED,
},
{
not: {
hasParent: true,
status: IN_PROGRESS,
}
}
]
}

When multiple filter fields are provided, Ent implicitly adds the And operator.

{
status: COMPLETED,
textHasPrefix: "GraphQL",
}

The above query will produce the following Ent query:

client.Todo.
Query().
Where(
todo.And(
todo.StatusEQ(todo.StatusCompleted),
todo.TextHasPrefix("GraphQL"),
)
).
All(ctx)

Edge/Relation filters#

Edge (relation) predicates can be expressed in the same Ent syntax:

{
hasParent: true,
hasChildrenWith: {
status: IN_PROGRESS,
}
}

The above query will produce the following Ent query:

client.Todo.
Query().
Where(
todo.HasParent(),
todo.HasChildrenWith(
todo.StatusEQ(todo.StatusInProgress),
),
).
All(ctx)

Implementation Example#

A working example exists in github.com/a8m/ent-graphql-example.

Wrapping Up#

As we've discussed earlier, Ent has set creating a "statically typed and explicit API using code generation" as a core design principle. With automatic GraphQL filter generation, we are doubling down on this idea to provide developers with the same explicit, type-safe development experience on the RPC layer as well.

Have questions? Need help with getting started? Feel free to join our Slack channel.

For more Ent news and updates:

Ent + gRPC is Ready for Usage

A few months ago, we announced the experimental support for generating gRPC services from Ent Schema definitions. The implementation was not complete yet but we wanted to get it out the door for the community to experiment with and provide us with feedback.

Today, after much feedback from the community, we are happy to announce that the Ent + gRPC integration is "Ready for Usage", this means all of the basic features are complete and we anticipate that most Ent applications can utilize this integration.

What have we added since our initial announcement?

  • Support for "Optional Fields" - A common issue with Protobufs is that the way that nil values are represented: a zero-valued primitive field isn't encoded into the binary representation. This means that applications cannot distinguish between zero and not-set for primitive fields. To support this, the Protobuf project supports some "Well-Known-Types" called "wrapper types" that wrap the primitive value with a struct. This wasn't previously supported but now when entproto generates a Protobuf message definition, it uses these wrapper types to represent "Optional" ent fields:

    // Code generated by entproto. DO NOT EDIT.
    syntax = "proto3";
    package entpb;
    import "google/protobuf/wrappers.proto";
    message User {
    int32 id = 1;
    string name = 2;
    string email_address = 3;
    google.protobuf.StringValue alias = 4;
    }
  • Multi-edge support - when we released the initial version of
    protoc-gen-entgrpc, we only supported generating gRPC service implementations for "Unique" edges (i.e reference at most one entity). Since a recent version, the plugin supports the generation of gRPC methods to read and write entities with O2M and M2M relationships.

  • Partial responses - By default, edge information is not returned by the Get method of the service. This is done deliberately because the amount of entities related to an entity is unbound.

    To allow the caller of to specify whether or not to return the edge information or not, the generated service adheres to Google AIP-157 (Partial Responses). In short, the Get<T>Request message includes an enum named View, this enum allows the caller to control whether or not this information should be retrieved from the database or not.

    message GetUserRequest {
    int32 id = 1;
    View view = 2;
    enum View {
    VIEW_UNSPECIFIED = 0;
    BASIC = 1;
    WITH_EDGE_IDS = 2;
    }
    }

Getting Started#

  • To help everyone get started with the Ent + gRPC integration, we have published an official Ent + gRPC Tutorial (and a complimentary GitHub repo).
  • Do you need help getting started with the integration or have some other question? Join us on Slack.
For more Ent news and updates:

Announcing the "Schema Import Initiative" and protoc-gen-ent

Migrating to a new ORM is not an easy process, and the transition cost can be prohibitive to many organizations. As much as we developers are enamoured by "Shiny New Things", the truth is that we rarely get a chance to work on a truly "green-field" project. Most of our careers, we operate in contexts where many technical and business constraints (a.k.a legacy systems) dictate and limit our options for moving forward. Developers of new technologies that want to succeed must offer interoperability capability and integration paths to help organizations seamlessly transition to a new way of solving an existing problem.

To help lower the cost of transitioning to Ent (or simply experimenting with it), we have started the "Schema Import Initiative" to help support many use cases for generating Ent schemas from external resources. The centrepiece of this effort is the schemast package (source code, docs) which enables developers to easily write programs that generate and manipulate Ent schemas. Using this package, developers can program in a high-level API, relieving them from worrying about code parsing and AST manipulations.

Protobuf Import Support#

The first project to use this new API, is protoc-gen-ent, a protoc plugin to generate Ent schemas from .proto files (docs). Organizations that have existing schemas defined in Protobuf can use this tool to generate Ent code automatically. For example, taking a simple message definition:

syntax = "proto3";
package entpb;
option go_package = "github.com/yourorg/project/ent/proto/entpb";
message User {
string name = 1;
string email_address = 2;
}

And setting the ent.schema.gen option to true:

syntax = "proto3";
package entpb;
+import "options/opts.proto";
option go_package = "github.com/yourorg/project/ent/proto/entpb";
message User {
+ option (ent.schema).gen = true; // <-- tell protoc-gen-ent you want to generate a schema from this message
string name = 1;
string email_address = 2;
}

Developers can invoke the standard protoc (protobuf compiler) command to use this plugin:

protoc -I=proto/ --ent_out=. --ent_opt=schemadir=./schema proto/entpb/user.proto

To generate Ent schemas from these definitions:

package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{field.String("name"), field.String("email_address")}
}
func (User) Edges() []ent.Edge {
return nil
}

To start using protoc-gen-ent today, and read about all of the different configuration options, head over to the documentation!

Join the Schema Import Initiative#

Do you have schemas defined elsewhere that you would like to automatically import in to Ent? With the schemast package, it is easier than ever to write the tool that you need to do that. Not sure how to start? Want to collaborate with the community in planning and building out your idea? Reach out to our great community via our Slack channel or start a discussion on GitHub!

For more Ent news and updates:

Generate a fully-working Go gRPC server in two minutes with Ent

ent + gRPC

Introduction#

Having entity schemas defined in a central, language-neutral format has many benefits as the scale of software engineering organizations increase. To do this, many organizations use Protocol Buffers as their interface definition language (IDL). In addition, gRPC, a Protobuf-based RPC framework modeled after Google's internal Stubby is becoming increasingly popular due to its efficiency and code-generation capabilities.

Being an IDL, gRPC does not prescribe any specific guidelines on implementing the data access layer so implementations vary greatly. Ent is a natural candidate for building the data access layer in any Go application and so there is great potential in integrating the two technologies together.

Today we announce an experimental version of entproto, a Go package, and a command-line tool to add Protobuf and gRPC support for ent users. With entproto, developers can set up a fully working CRUD gRPC server in a few minutes. In this post, we will show exactly how to do just that.

Setting Up#

The final version of this tutorial is available on GitHub, you can clone it if you prefer following along that way.

Let's start by initializing a new Go module for our project:

mkdir ent-grpc-example
cd ent-grpc-example
go mod init ent-grpc-example

Next we use go run to invoke the ent code generator to initialize a schema:

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

Our directory should now look like:

.
鈹溾攢鈹 ent
鈹偮犅 鈹溾攢鈹 generate.go
鈹偮犅 鈹斺攢鈹 schema
鈹偮犅 鈹斺攢鈹 user.go
鈹溾攢鈹 go.mod
鈹斺攢鈹 go.sum

Next, let's add the entproto package to our project:

go get -u entgo.io/contrib/entproto

Next, we will define the schema for the User entity. Open ent/schema/user.go and edit:

package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema"
)
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique(),
field.String("email_address").
Unique(),
}
}

In this step, we added two unique fields to our User entity: name and email_address. The ent.Schema is just the definition of the schema, to create usable production code from it we need to run Ent's code generation tool on it. Run:

go generate ./...

Notice the a bunch of new files were created from our schema definition now:

鈹溾攢鈹 ent
鈹偮犅 鈹溾攢鈹 client.go
鈹偮犅 鈹溾攢鈹 config.go
// .... many more
鈹偮犅 鈹溾攢鈹 user
鈹偮犅 鈹溾攢鈹 user.go
鈹偮犅 鈹溾攢鈹 user_create.go
鈹偮犅 鈹溾攢鈹 user_delete.go
鈹偮犅 鈹溾攢鈹 user_query.go
鈹偮犅 鈹斺攢鈹 user_update.go
鈹溾攢鈹 go.mod
鈹斺攢鈹 go.sum

At this point, we can open a connection to a database, run a migration to create the users table, and start reading and writing data to it. This is covered on the Setup Tutorial, so let's cut to the chase and learn about generating Protobuf definitions and gRPC servers from our schema.

Generating Go Protobufs with entproto#

As ent and Protobuf schemas are not identical, we must supply some annotations on our schema to help entproto figure out exactly how to generate Protobuf definitions (called "Messages" in protobuf lingo).

The first thing we need to do is to add an entproto.Message() annotation. This is our opt-in to Protobuf schema generation, we don't necessarily want to generate proto messages or gRPC service definitions from all of our schema entities, and this annotation gives us that control. To add it, append to ent/schema/user.go:

func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
}
}

Next, we need to annotate each field and assign it a field number. Recall that when defining a protobuf message type, each field must be assigned a unique number. To do that, we add an entproto.Field annotation on each field. Update the Fields in ent/schema/user.go:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique().
Annotations(
entproto.Field(2),
),
field.String("email_address").
Unique().
Annotations(
entproto.Field(3),
),
}
}

Notice that we did not start our field numbers from 1, this is because ent implicitly creates the ID field for the entity, and that field is automatically assigned the number 1. We can now generate our protobuf message type definitions. To do that, we will add to ent/generate.go a go:generate directive that invokes the entproto command-line tool. It should now look like this:

package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
//go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema

Let's re-generate our code:

go generate ./...

Observe that a new directory was created which will contain all protobuf related generated code: ent/proto. It now contains:

ent/proto
鈹斺攢鈹 entpb
鈹溾攢鈹 entpb.proto
鈹斺攢鈹 generate.go

Two files were created. Let's look at their contents:

// Code generated by entproto. DO NOT EDIT.
syntax = "proto3";
package entpb;
option go_package = "ent-grpc-example/ent/proto/entpb";
message User {
int32 id = 1;
string user_name = 2;
string email_address = 3;
}

Nice! A new .proto file containing a message type definition that maps to our User schema was created!

package entpb
//go:generate protoc -I=.. --go_out=.. --go-grpc_out=.. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --entgrpc_out=.. --entgrpc_opt=paths=source_relative,schema_path=../../schema entpb/entpb.proto

A new generate.go file was created with an invocation to protoc, the protobuf code generator instructing it how to generate Go code from our .proto file. For this command to work, we must first install protoc as well as 3 protobuf plugins: protoc-gen-go (which generates Go Protobuf structs), protoc-gen-go-grpc (which generates Go gRPC service interfaces and clients), and protoc-gen-entgrpc (which generates an implementation of the service interface). If you do not have these installed, please follow these directions:

After installing these dependencies, we can re-run code-generation:

go generate ./...

Observe that a new file named ent/proto/entpb/entpb.pb.go was created which contains the generated Go structs for our entities.

Let's write a test that uses it to make sure everything is wired correctly. Create a new file named pb_test.go and write:

package main
import (
"testing"
"ent-grpc-example/ent/proto/entpb"
)
func TestUserProto(t *testing.T) {
user := entpb.User{
Name: "rotemtam",
EmailAddress: "rotemtam@example.com",
}
if user.GetName() != "rotemtam" {
t.Fatal("expected user name to be rotemtam")
}
if user.GetEmailAddress() != "rotemtam@example.com" {
t.Fatal("expected email address to be rotemtam@example.com")
}
}

To run it:

go get -u./... # install deps of the generated package
go test ./...

Hooray! The test passes. We have successfully generated working Go Protobuf structs from our Ent schema. Next, let's see how to automatically generate a working CRUD gRPC server from our schema.

Generating a Fully Working gRPC Server from our Schema#

Having Protobuf structs generated from our ent.Schema can be useful, but what we're really interested in is getting an actual server that can create, read, update, and delete entities from an actual database. To do that, we need to update just one line of code! When we annotate a schema with entproto.Service, we tell the entproto code-gen that we are interested in generating a gRPC service definition, from the protoc-gen-entgrpc will read our definition and generate a service implementation. Edit ent/schema/user.go and modify the schema's Annotations:

func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
+ entproto.Service(), // <-- add this
}
}

Now re-run code-generation:

go generate ./...

Observe some interesting changes in ent/proto/entpb:

ent/proto/entpb
鈹溾攢鈹 entpb.pb.go
鈹溾攢鈹 entpb.proto
鈹溾攢鈹 entpb_grpc.pb.go
鈹溾攢鈹 entpb_user_service.go
鈹斺攢鈹 generate.go

First, entproto added a service definition to entpb.proto:

service UserService {
rpc Create ( CreateUserRequest ) returns ( User );
rpc Get ( GetUserRequest ) returns ( User );
rpc Update ( UpdateUserRequest ) returns ( User );
rpc Delete ( DeleteUserRequest ) returns ( google.protobuf.Empty );
}

In addition, two new files were created. The first, ent_grpc.pb.go, contains the gRPC client stub and the interface definition. If you open the file, you will find in it (among many other things):

// UserServiceClient is the client API for UserService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type UserServiceClient interface {
Create(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)
Get(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
Update(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
Delete(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}

The second file, entpub_user_service.go contains a generated implementation for this interface. For example, an implementation for the Get method:

// Get implements UserServiceServer.Get
func (svc *UserService) Get(ctx context.Context, req *GetUserRequest) (*User, error) {
get, err := svc.client.User.Get(ctx, int(req.GetId()))
switch {
case err == nil:
return toProtoUser(get), nil
case ent.IsNotFound(err):
return nil, status.Errorf(codes.NotFound, "not found: %s", err)
default:
return nil, status.Errorf(codes.Internal, "internal error: %s", err)
}
}

Not bad! Next, let's create a gRPC server that can serve requests to our service.

Creating the Server#

Create a new file cmd/server/main.go and write:

package main
import (
"context"
"log"
"net"
_ "github.com/mattn/go-sqlite3"
"ent-grpc-example/ent"
"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
)
func main() {
// Initialize an ent client.
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 migration tool (creating tables, etc).
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Initialize the generated User service.
svc := entpb.NewUserService(client)
// Create a new gRPC server (you can wire multiple services to a single server).
server := grpc.NewServer()
// Register the User service with the server.
entpb.RegisterUserServiceServer(server, svc)
// Open port 5000 for listening to traffic.
lis, err := net.Listen("tcp", ":5000")
if err != nil {
log.Fatalf("failed listening: %s", err)
}
// Listen for traffic indefinitely.
if err := server.Serve(lis); err != nil {
log.Fatalf("server ended: %s", err)
}
}

Notice that we added an import of github.com/mattn/go-sqlite3, so we need to add it to our module:

go get -u github.com/mattn/go-sqlite3

Next, let's run the server, while we write a client that will communicate with it:

go run -mod=mod ./cmd/server

Creating the Client#

Let's create a simple client that will make some calls to our server. Create a new file named cmd/client/main.go and write:

package main
import (
"context"
"fmt"
"log"
"math/rand"
"time"
"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
func main() {
rand.Seed(time.Now().UnixNano())
// Open a connection to the server.
conn, err := grpc.Dial(":5000", grpc.WithInsecure())
if err != nil {
log.Fatalf("failed connecting to server: %s", err)
}
defer conn.Close()
// Create a User service Client on the connection.
client := entpb.NewUserServiceClient(conn)
// Ask the server to create a random User.
ctx := context.Background()
user := randomUser()
created, err := client.Create(ctx, &entpb.CreateUserRequest{
User: user,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("failed creating user: status=%s message=%s", se.Code(), se.Message())
}
log.Printf("user created with id: %d", created.Id)
// On a separate RPC invocation, retrieve the user we saved previously.
get, err := client.Get(ctx, &entpb.GetUserRequest{
Id: created.Id,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("failed retrieving user: status=%s message=%s", se.Code(), se.Message())
}
log.Printf("retrieved user with id=%d: %v", get.Id, get)
}
func randomUser() *entpb.User {
return &entpb.User{
Name: fmt.Sprintf("user_%d", rand.Int()),
EmailAddress: fmt.Sprintf("user_%d@example.com", rand.Int()),
}
}

Our client creates a connection to port 5000, where our server is listening, then issues a Create request to create a new user, and then issues a second Get request to retrieve it from the database. Let's run our client code:

go run ./cmd/client

Observe the output:

2021/03/18 10:42:58 user created with id: 1
2021/03/18 10:42:58 retrieved user with id=1: id:1 name:"user_730811260095307266" email_address:"user_7338662242574055998@example.com"

Amazing! With a few annotations on our schema, we used the super-powers of code generation to create a working gRPC server in no time!

Caveats and Limitations#

entproto is still experimental stage and lacks some basic functionality. For example, many applications will probably want a List or Find method on their service, but these are not yet supported. In addition, some other issues we plan to tackle in the near future:

  • Currently only "unique" edges are supported (O2O, O2M).
  • The generated "mutating" methods (Create/Update) currently set all fields, disregarding zero/null values and field nullability.
  • All fields are copied from the gRPC request to the ent client, support for configuring some fields to be unsettable via the service by adding a field/edge annotation is also planned.

Next Steps#

We believe that ent + gRPC can be a great way to build server applications in Go. For example, to set granular access control to the entities managed by our application, developers can already use Privacy Policies that work out-of-the-box with the gRPC integration. To run any arbitrary Go code on the different lifecycle events of entities, developers can utilize custom Hooks.

Do you want to build gRPC servers with ent? If you want some help setting up or want the integration to support your use case, please reach out to us via our Discussions Page on GitHub or in the #ent channel on the Gophers Slack.

For more Ent news and updates:

Announcing Edge-field Support in v0.7.0

Over the past few months, there has been much discussion in the Ent project issues about adding support for the retrieval of the foreign key field when retrieving entities with One-to-One or One-to-Many edges. We are happy to announce that as of v0.7.0 ent supports this feature.

Before Edge-field Support#

Prior to merging this branch, a user that wanted to retrieve the foreign-key field for an entity needed to use eager-loading. Suppose our schema looked like this:

// ent/schema/user.go:
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique().
NotEmpty(),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("pets", Pet.Type).
Ref("owner"),
}
}
// ent/schema/pet.go
// Pet holds the schema definition for the Pet entity.
type Pet struct {
ent.Schema
}
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
}
}
// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Unique().
Required(),
}
}

The schema describes two related entities: User and Pet, with a One-to-Many edge between them: a user can own many pets and a pet can have one owner.

When retrieving pets from the data storage, it is common for developers to want to access the foreign-key field on the pet. However, because this field is created implicitly from the owner edge it was automatically accessible when retrieving an entity. To retrieve this from the storage a developer needed to do something like:

func Test(t *testing.T) {
ctx := context.Background()
c := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
defer c.Close()
// Create the User
u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)
// Create the Pet
p := c.Pet.
Create().
SetOwner(u). // Associate with the user
SetName("donut").
SaveX(ctx)
petWithOwnerId := c.Pet.Query().
Where(pet.ID(p.ID)).
WithOwner(func(query *ent.UserQuery) {
query.Select(user.FieldID)
}).
OnlyX(ctx)
fmt.Println(petWithOwnerId.Edges.Owner.ID)
// Output: 1
}

Aside from being very verbose, retrieving the pet with the owner this way was inefficient in-terms of database queries. If we execute the query with the .Debug() we can see the DB queries ent generates to satisfy this call:

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`pet_owner` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2
SELECT DISTINCT `users`.`id` FROM `users` WHERE `users`.`id` IN (?)

In this example, Ent first retrieves the Pet with an ID of 1, then redundantly fetches the id field from the users table for users with an ID of 1.

With Edge-field Support#

Edge-field support greatly simplifies and improves the efficiency of this flow. With this feature, developers can define the foreign key field as part of the schemas Fields(), and by using the .Field(..) modifier on the edge definition instruct Ent to expose and map the foreign column to this field. So, in our example schema, we would modify it to be:

// user.go stays the same
// pet.go
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id"), // <-- explictly add the field we want to contain the FK
}
}
// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Field("owner_id"). // <-- tell ent which field holds the reference to the owner
Unique().
Required(),
}
}

In order to update our client code we need to re-run code generation:

go generate ./...

We can now modify our query to be much simpler:

func Test(t *testing.T) {
ctx := context.Background()
c := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
defer c.Close()
u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)
p := c.Pet.Create().
SetOwner(u).
SetName("donut").
SaveX(ctx)
petWithOwnerId := c.Pet.GetX(ctx, p.ID) // <-- Simply retrieve the Pet
fmt.Println(petWithOwnerId.OwnerID)
// Output: 1
}

Running with the .Debug() modifier we can see that the DB queries make more sense now:

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`owner_id` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2

Hooray 馃帀!

Migrating Existing Schemas to Edge Fields#

If you are already using Ent with an existing schema, you may already have O2M relations whose foreign-key columns already exist in your database. Depending on how you configured your schema, chances are that they may be stored in a column by a different name than the field you are now adding. For instance, you want to create an owner_id field, but Ent auto-created the column foreign-key column as pet_owner.

To check what column name Ent is using for this field you can look in the ./ent/migrate/schema.go file:

PetsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString},
{Name: "pet_owner", Type: field.TypeInt, Nullable: true}, // <-- this is our FK
}

To allow for a smooth migration, you must explicitly tell Ent to keep using the existing column name. You can do this by using the StorageKey modifier (either on the field or on the edge). For example:

// In schema/pet.go:
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id").
StorageKey("pet_owner"), // <-- explicitly set the column name
}
}

In the near future we plan to implement Schema Versioning, which will store the history of schema changes alongside the code. Having this information will allow ent to support such migrations in an automatic and predictable way.

Wrapping Up#

Edge-field support is readily available and can be installed by go get -u entgo.io/ent@v0.7.0.

Many thanks 馃檹 to all the good people who took the time to give feedback and helped design this feature properly: Alex Snast, Ruben de Vries, Marwan Sulaiman, Andy Day, Sebastian Fekete and Joe Harvey.

For more Ent news and updates:#

Introducing ent

The state of Go in Facebook Connectivity Tel Aviv#

20 months ago, I joined Facebook Connectivity (FBC) team in Tel Aviv after ~5 years of programming in Go and embedding it in a few companies.
I joined a team that was working on a new project and we needed to choose a language for this mission. We compared a few languages and decided to go with Go.

Since then, Go continued to spread across other FBC projects and became a big success with around 15 Go engineers in Tel Aviv alone. New services are now written in Go.

The motivation for writing a new ORM in Go#

Most of my work in my 5 years before Facebook was on infra tooling and micro-services without too much data-model work. A service that was needed to do a little amount of work with an SQL database used one of the existing open-source solutions, but one that had worked with a complicated data model was written in a different language with a robust ORM. For example, Python with SQLAlchemy.

At Facebook we like to think about our data-model in graph concepts. We've had a good experience with this model internally.
The lack of a proper Graph-based ORM for Go, led us to write one here with the following principles:

  • Schema As Code - defining types, relations and constraints should be in Go code (not struct tags), and should be validated using a CLI tool. We have good experience with a similar tool internally at Facebook.
  • Statically typed and explicit API using codegen - API with interface{}s everywhere affects developers efficiency; especially project newbies.
  • Queries, aggregations and graph traversals should be simple - developers don鈥檛 want to deal with raw SQL queries nor SQL terms.
  • Predicates should be statically typed. No strings everywhere.
  • Full support for context.Context - This helps us to get full visibility in our traces and logs systems, and it鈥檚 important for other features like cancellation.
  • Storage agnostic - we tried to keep the storage layer dynamic using codegen templates, since the development initially started on Gremlin (AWS Neptune) and switched later to MySQL.

Open-sourcing ent#

ent is an entity framework (ORM) for Go, built with the principles described above. ent makes it possible to define any data model or graph-structure in Go code easily; The schema configuration is verified by entc (the ent codegen) that generates an idiomatic and statically-typed API that keeps Go developers productive and happy. It supports MySQL, MariaDB, PostgreSQL, SQLite, and Gremlin-based graph databases.

We鈥檙e open-sourcing ent today, and invite you to get started 鈫 entgo.io/docs/getting-started.