1. Model and Field Basics

1.1. Introduction to Model Definition

In an ORM framework, a model is used to describe the mapping relationship between entity types in the application and database tables. The model defines the properties and relationships of the entity, as well as the database-specific configurations associated with them. In the ent framework, models are typically used to describe entity types in a graph, such as User or Group.

Model definitions typically include descriptions of the entity's fields (or properties) and edges (or relationships), as well as some database-specific options. These descriptions can help us define the structure, properties, and relationships of the entity, and can be used to generate the corresponding database table structure based on the model.

1.2. Field Overview

Fields are the part of the model that represents entity properties. They define the properties of the entity, such as name, age, date, etc. In the ent framework, field types include various basic data types, such as integer, string, boolean, time, etc., as well as some SQL-specific types, such as UUID, []byte, JSON, etc.

The table below shows the field types supported by the ent framework:

Type Description
int Integer type
uint8 Unsigned 8-bit integer type
float64 Floating point type
bool Boolean type
string String type
time.Time Time type
UUID UUID type
[]byte Byte array type (SQL only)
JSON JSON type (SQL only)
Enum Enum type (SQL only)
Other Other types (e.g., Postgres Range)

2. Field Property Details

2.1. Data Types

The data type of an attribute or field in an entity model determines the form of data that can be stored. This is a crucial part of the model definition in the ent framework. Below are some commonly used data types in the ent framework.

import (
    "time"

    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)

// User schema.
type User struct {
    ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age"),             // Integer type
        field.String("name"),         // String type
        field.Bool("active"),         // Boolean type
        field.Float("score"),         // Floating point type
        field.Time("created_at"),     // Timestamp type
    }
}
  • int: Represents integer values, which can be int8, int16, int32, int64, etc.
  • string: Represents string data.
  • bool: Represents boolean values, typically used as flags.
  • float64: Represents floating point numbers, also can use float32.
  • time.Time: Represents time, typically used for timestamps or date data.

These field types will be mapped to the corresponding types supported by the underlying database. Additionally, ent supports more complex types such as UUID, JSON, enums (Enum), and support for special database types like []byte (SQL only) and Other (SQL only).

2.2. Default Values

Fields can be configured with default values. If the corresponding value is not specified when creating an entity, the default value will be used. The default value can be a fixed value or a dynamically generated value from a function. Use the .Default method to set a static default value, or use .DefaultFunc to set a dynamically generated default value.

// User schema.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Time("created_at").
            Default(time.Now),  // A fixed default value of time.Now
        field.String("role").
            Default("user"),   // A constant string value
        field.Float("score").
            DefaultFunc(func() float64 {
                return 10.0  // A default value generated by a function
            }),
    }
}

2.3. Field Optionality and Zero Values

By default, fields are required. To declare an optional field, use the .Optional() method. Optional fields will be declared as nullable fields in the database. The Nillable option allows fields to be explicitly set to nil, distinguishing between the zero value of a field and an unset state.

// User schema.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // Optional field is not required
        field.Int("age").Optional().Nillable(), // Nillable field can be set to nil
    }
}

When using the defined model above, the age field can accept both nil values to indicate unset, as well as non-nil zero values.

2.4. Field Uniqueness

Unique fields ensure that there are no duplicate values in the database table. Use the Unique() method to define a unique field. When establishing data integrity as a critical requirement, such as for user emails or usernames, unique fields should be used.

// User schema.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // Unique field to avoid duplicate email addresses
    }
}

This will create a unique constraint in the underlying database to prevent the insertion of duplicate values.

2.5. Field Indexing

Field indexing is used to improve the performance of database queries, especially in large databases. In the ent framework, the .Indexes() method can be used to create indexes.

import "entgo.io/ent/schema/index"

// User schema.
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // Create an index on the 'email' field
        index.Fields("name", "age").Unique(), // Create a unique composite index
    }
}

Indexes can be utilized for frequently queried fields, but it's important to note that too many indexes may lead to decreased write operation performance. Therefore, the decision to create indexes should be balanced based on actual circumstances.

2.6. Custom Tags

In the ent framework, you can use the StructTag method to add custom tags to the generated entity struct fields. These tags are very useful for implementing operations such as JSON encoding and XML encoding. In the example below, we will add custom JSON and XML tags for the name field.

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // Add custom tags using the StructTag method
            // Here, set the JSON tag for the name field to 'username' and ignore it when the field is empty (omitempty)
            // Also, set the XML tag for encoding to 'name'
            StructTag(`json:"username,omitempty" xml:"name"`),
    }
}

When encoding with JSON or XML, the omitempty option indicates that if the name field is empty, then this field will be omitted from the encoding result. This is very useful for reducing the size of the response body when writing APIs.

This also demonstrates how to set multiple tags for the same field simultaneously. JSON tags use the json key, XML tags use the xml key, and they are separated by spaces. These tags will be used by library functions such as encoding/json and encoding/xml when parsing struct for encoding or decoding.

3. Field Validation and Constraints

Field validation is an important aspect of database design to ensure data consistency and validity. In this section, we will delve into using built-in validators, custom validators, and various constraints to improve the integrity and quality of the data in the entity model.

3.1. Built-in Validators

The framework provides a series of built-in validators for performing common data validity checks on different types of fields. Using these built-in validators can simplify the development process and quickly define valid data ranges or formats for fields.

Here are some examples of built-in field validators:

  • Validators for numeric types:
    • Positive(): Validates if the field's value is a positive number.
    • Negative(): Validates if the field's value is a negative number.
    • NonNegative(): Validates if the field's value is a non-negative number.
    • Min(i): Validates if the field's value is greater than a given minimum value i.
    • Max(i): Validates if the field's value is less than a given maximum value i.
  • Validators for string type:
    • MinLen(i): Validates the minimum length of a string.
    • MaxLen(i): Validates the maximum length of a string.
    • Match(regexp.Regexp): Validates if the string matches the given regular expression.
    • NotEmpty: Validates if the string is not empty.

Let's take a look at a practical code example. In this example, a User model is created, which includes a non-negative integer type age field and an email field with a fixed format:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("email").
            Match(regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)),
    }
}

3.2. Custom Validators

While built-in validators can handle many common validation requirements, sometimes you may need more complex validation logic. In such cases, you can write custom validators to meet specific business rules.

A custom validator is a function that receives a field value and returns an error. If the returned error is not empty, it indicates validation failure. The general format of a custom validator is as follows:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // Verify if the phone number meets the expected format
                matched, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !matched {
                    return errors.New("Incorrect phone number format")
                }
                return nil
            }),
    }
}

As shown above, we have created a custom validator to validate the format of a phone number.

3.3. Constraints

Constraints are rules that enforce specific rules on a database object. They can be used to ensure the correctness and consistency of data, such as preventing the input of invalid data or defining the relationships of data.

Common database constraints include:

  • Primary key constraint: Ensures that each record in the table is unique.
  • Unique constraint: Ensures that the value of a column or a combination of columns is unique in the table.
  • Foreign key constraint: Defines the relationships between tables, ensuring referential integrity.
  • Check constraint: Ensures that a field value meets a specific condition.

In the entity model, you can define constraints to maintain data integrity as follows:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // Unique constraint to ensure the username is unique in the table.
        field.String("email").
            Unique(), // Unique constraint to ensure the email is unique in the table.
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // Foreign key constraint, creating a unique edge relationship with another user.
    }
}

In summary, field validation and constraints are crucial for ensuring good data quality and avoiding unexpected data errors. Utilizing the tools provided by the ent framework can make this process simpler and more reliable.