1. Introduction to ent
Ent is an entity framework developed by Facebook specifically for the Go language. It simplifies the process of building and maintaining large-scale data model applications. The ent framework mainly follows the following principles:
- Easily model the database schema as a graph structure.
- Define the schema in the form of Go language code.
- Implement static types based on code generation.
- Writing database queries and graph traversal is very simple.
- Easily extend and customize using Go templates.
2. Environment Setup
To start using the ent framework, make sure that the Go language is installed in your development environment.
If your project directory is outside of GOPATH
, or if you are not familiar with GOPATH
, you can use the following command to create a new Go module project:
go mod init entdemo
This will initialize a new Go module and create a new go.mod
file for your entdemo
project.
3. Defining the First Schema
3.1. Creating Schema Using ent CLI
First, you need to run the following command in the root directory of your project to create a schema named User using the ent CLI tool:
go run -mod=mod entgo.io/ent/cmd/ent new User
The above command will generate the User schema in the entdemo/ent/schema/
directory:
File entdemo/ent/schema/user.go
:
package schema
import "entgo.io/ent"
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return nil
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return nil
}
3.2. Adding Fields
Next, we need to add field definitions to the User Schema. Below is an example of adding two fields to the User entity.
File modified entdemo/ent/schema/user.go
:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
This piece of code defines two fields for the User model: age
and name
, where age
is a positive integer and name
is a string with a default value of “unknown”.
3.3. Generating Database Entities
After defining the schema, you need to run the go generate
command to generate the underlying database access logic.
Run the following command in the root directory of your project:
go generate ./ent
This command will generate the corresponding Go code based on the previously defined schema, resulting in the following file structure:
ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (several files omitted for brevity)
├── schema
│ └── user.go
├── tx.go
├── user
│ ├── user.go
│ └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go
4.1. Initializing Database Connection
To establish a connection to the MySQL database, we can use the Open
function provided by the ent
framework. First, import the MySQL driver and then provide the correct connection string to initialize the database connection.
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/go-sql-driver/mysql" // Import the MySQL driver
)
func main() {
// Use ent.Open to establish a connection with the MySQL database.
// Remember to replace the placeholders "your_username", "your_password", and "your_database" below.
client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
// Run the automatic migration tool
ctx := context.Background()
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Additional business logic can be written here
}
4.2. Creating Entities
Creating a User entity involves building a new entity object and persisting it to the database using the Save
or SaveX
method. The following code demonstrates how to create a new User entity and initialize two fields age
and name
.
// The CreateUser function is used to create a new User entity
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Use client.User.Create() to build the request for creating a User,
// then chain the SetAge and SetName methods to set the values of entity fields.
u, err := client.User.
Create().
SetAge(30). // Set user age
SetName("a8m"). // Set user name
Save(ctx) // Call Save to save the entity to the database
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", u)
return u, nil
}
In the main
function, you can call the CreateUser
function to create a new user entity.
func main() {
// ...Omitted database connection establishment code
// Create a user entity
u, err := CreateUser(ctx, client)
if err != nil {
log.Fatalf("failed creating user: %v", err)
}
log.Printf("created user: %#v\n", u)
}
4.3. Querying Entities
To query entities, we can use the query builder generated by ent
. The following code demonstrates how to query a user named “a8m”:
// The QueryUser function is used to query the user entity with a specified name
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Use client.User.Query() to build the query for User,
// then chain the Where method to add query conditions, such as querying by username
u, err := client.User.
Query().
Where(user.NameEQ("a8m")). // Add query condition, in this case, the name is "a8m"
Only(ctx) // The Only method indicates that only one result is expected
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}
In the main
function, you can call the QueryUser
function to query the user entity.
func main() {
// ...Omitted database connection establishment and user creation code
// Query the user entity
u, err := QueryUser(ctx, client)
if err != nil {
log.Fatalf("failed querying user: %v", err)
}
log.Printf("queried user: %#v\n", u)
}
5.1. Understanding Edges and Inverse Edges
In the ent
framework, the data model is visualized as a graph structure, where entities represent nodes in the graph, and the relationships between entities are represented by edges. An edge is a connection from one entity to another, for example, a User
can own multiple Cars
.
Inverse Edges are reverse references to edges, logically representing the reverse relationship between entities, but not creating a new relationship in the database. For example, through the inverse edge of a Car
, we can find the User
who owns this car.
The key significance of edges and inverse edges lies in making navigation between associated entities very intuitive and straightforward.
Tip: In
ent
, edges correspond to traditional database foreign keys and are used to define relationships between tables.
5.2. Defining Edges in the Schema
First, we will use the ent
CLI to create the initial schema for Car
and Group
:
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
Next, in the User
schema, we define the edge with Car
to represent the relationship between users and cars. We can add an edge cars
pointing to the Car
type in the user entity, indicating that a user can have multiple cars:
// entdemo/ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
After defining the edges, we need to run go generate ./ent
again to generate the corresponding code.
5.3. Operating on Edge Data
Creating cars associated with a user is a simple process. Given a user entity, we can create a new car entity and associate it with the user:
import (
"context"
"log"
"entdemo/ent"
// Make sure to import the schema definition for Car
_ "entdemo/ent/schema"
)
func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
user, err := client.User.Get(ctx, userID)
if err != nil {
log.Fatalf("failed getting user: %v", err)
return err
}
// Create a new car and associate it with the user
_, err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
SetOwner(user).
Save(ctx)
if err != nil {
log.Fatalf("failed creating car for user: %v", err)
return err
}
log.Println("car was created and associated with the user")
return nil
}
Similarly, querying a user’s cars is straightforward. If we want to retrieve a list of all the cars owned by a user, we can do the following:
func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
user, err := client.User.Get(ctx, userID)
if err != nil {
log.Fatalf("failed getting user: %v", err)
return err
}
// Query all cars owned by the user
cars, err := user.QueryCars().All(ctx)
if err != nil {
log.Fatalf("failed querying cars: %v", err)
return err
}
for _, car := range cars {
log.Printf("car: %v, model: %v", car.ID, car.Model)
}
return nil
}
Through the above steps, we have not only learned how to define edges in the schema but also demonstrated how to create and query data related to edges.
6. Graph Traversal and Querying
6.1. Understanding Graph Structures
In ent
, graph structures are represented by entities and the edges between them. Each entity is equivalent to a node in the graph, and the relationships between entities are represented by edges, which can be one-to-one, one-to-many, many-to-many, etc. This graph structure makes complex queries and operations on a relational database simple and intuitive.
6.2. Traversing Graph Structures
Writing Graph Traversal code mainly involves querying and associating data through the edges between entities. Below is a simple example demonstrating how to traverse the graph structure in ent
:
import (
"context"
"log"
"entdemo/ent"
)
// GraphTraversal is an example of traversing the graph structure
func GraphTraversal(ctx context.Context, client *ent.Client) error {
// Query the user named "Ariel"
a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
if err != nil {
log.Fatalf("Failed querying user: %v", err)
return err
}
// Traverse all the cars belonging to Ariel
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
log.Fatalf("Failed querying cars: %v", err)
return err
}
for _, car := range cars {
log.Printf("Ariel has a car with model: %s", car.Model)
}
// Traverse all the groups Ariel is a member of
groups, err := a8m.QueryGroups().All(ctx)
if err != nil {
log.Fatalf("Failed querying groups: %v", err)
return err
}
for _, g := range groups {
log.Printf("Ariel is a member of group: %s", g.Name)
}
return nil
}
The above code is a basic example of graph traversal, which first queries a user and then traverses the user’s cars and groups.
7. Visualizing Database Schema
7.1. Installing the Atlas Tool
To visualize the database schema generated by ent
, we can use the Atlas tool. The installation steps for Atlas are very simple. For example, on macOS, you can install it using brew
:
brew install ariga/tap/atlas
Note: Atlas is a universal database migration tool that can handle table structure version management for various databases. Detailed introduction to Atlas will be provided in later chapters.
7.2. Generating ERD and SQL Schema
Using Atlas to view and export schemas is very straightforward. After installing Atlas, you can use the following command to view the Entity-Relationship Diagram (ERD):
atlas schema inspect -d [database_dsn] --format dot
Or directly generate the SQL Schema:
atlas schema inspect -d [database_dsn] --format sql
Where [database_dsn]
points to the data source name (DSN) of your database. For example, for an SQLite database, it might be:
atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot
The output generated by these commands can be further transformed into views or documents using the respective tools.
8. Schema Migration
8.1. Automatic Migration and Versioned Migration
ent supports two schema migration strategies: automatic migration and versioned migration. Automatic migration is the process of inspecting and applying schema changes at runtime, suitable for development and testing. Versioned migration involves generating migration scripts and requires careful review and testing before production deployment.
Tip: For automatic migration, refer to the content in section 4.1.
8.2. Performing Versioned Migration
The process of versioned migration involves generating migration files through Atlas. Below are the relevant commands:
To generate migration files:
atlas migrate diff -d ent/schema/path --dir migrations/dir
Subsequently, these migration files can be applied to the database:
atlas migrate apply -d migrations/dir --url database_dsn
Following this process, you can maintain a history of database migrations in the version control system and ensure thorough review before each migration.
Tip: Refer to the sample code at https://github.com/ent/ent/tree/master/examples/start