Views

Ent supports working with database views. Unlike regular Ent types (schemas), which are usually backed by tables, views act as “virtual tables” and their data results from a query. The following examples demonstrate how to define a VIEW in Ent. For more details on the different options, follow the rest of the guide.

  • Builder Definition
  • Raw Definition
  • External Definition

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // Annotations of the CleanUser.
  6. func (CleanUser) Annotations() []schema.Annotation {
  7. return []schema.Annotation{
  8. entsql.ViewFor(dialect.Postgres, func(s *sql.Selector) {
  9. s.Select("name", "public_info").From(sql.Table("users"))
  10. }),
  11. }
  12. }
  13. // Fields of the CleanUser.
  14. func (CleanUser) Fields() []ent.Field {
  15. return []ent.Field{
  16. field.String("name"),
  17. field.String("public_info"),
  18. }
  19. }

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // Annotations of the CleanUser.
  6. func (CleanUser) Annotations() []schema.Annotation {
  7. return []schema.Annotation{
  8. // Alternatively, you can use raw definitions to define the view.
  9. // But note, this definition is skipped if the ViewFor annotation
  10. // is defined for the dialect we generated migration to (Postgres).
  11. entsql.View(`SELECT name, public_info FROM users`),
  12. }
  13. }
  14. // Fields of the CleanUser.
  15. func (CleanUser) Fields() []ent.Field {
  16. return []ent.Field{
  17. field.String("name"),
  18. field.String("public_info"),
  19. }
  20. }

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // View definition is specified in a separate file (`schema.sql`),
  6. // and loaded using Atlas' `composite_schema` data-source.
  7. // Fields of the CleanUser.
  8. func (CleanUser) Fields() []ent.Field {
  9. return []ent.Field{
  10. field.String("name"),
  11. field.String("public_info"),
  12. }
  13. }

Views - 图1key differences between tables and views

  • Views are read-only, and therefore, no mutation builders are generated for them. If you want to define insertable/updatable views, define them as regular schemas and follow the guide below to configure their migrations.
  • Unlike ent.Schema, ent.View does not have a default ID field. If you want to include an id field in your view, you can explicitly define it as a field.
  • Hooks cannot be registered on views, as they are read-only.
  • Atlas provides built-in support for Ent views, for both versioned migrations and testing. However, if you are not using Atlas and want to use views, you need to manage their migrations manually since Ent does not offer schema migrations for them. :::

Introduction

Views defined in the ent/schema package embed the ent.View type instead of the ent.Schema type. Besides fields, they can have edges, interceptors, and annotations to enable additional integrations. For example:

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // Fields of the CleanUser.
  6. func (CleanUser) Fields() []ent.Field {
  7. return []ent.Field{
  8. // Note, unlike real schemas (tables, defined with ent.Schema),
  9. // the "id" field should be defined manually if needed.
  10. field.Int("id"),
  11. field.String("name"),
  12. field.String("public_info"),
  13. }
  14. }

Once defined, you can run go generate ./ent to create the assets needed to interact with this view. For example:

  1. client.CleanUser.Query().OnlyX(ctx)

Note, the Create/Update/Delete builders are not generated for ent.Views.

Migration and Testing

After defining the view schema, we need to inform Ent (and Atlas) about the SQL query that defines this view. If not configured, running an Ent query, such as the one defined above, will fail because there is no table named clean_users.

The rest of the document, assumes you use Ent with Atlas Pro, as Ent does not have migration support for views or other database objects besides tables and relationships. However, using Atlas or its Pro subscription is not mandatory. Ent does not require a specific migration engine, and as long as the view exists in the database, the client should be able to query it. :::

To configure our view definition (AS SELECT ...), we have two options:

  1. Define it within the ent/schema in Go code.
  2. Keep the ent/schema independent of the view definition and create it externally. Either manually or automatically using Atlas.

Let’s explore both options:

Go Definition

This example demonstrates how to define an ent.View with its SQL definition (AS ...) specified in the Ent schema.

The main advantage of this approach is that the CREATE VIEW correctness is checked during migration, not during queries. For example, if one of the ent.Fields is defined in your ent/schema does not exist in your SQL definition, PostgreSQL will return the following error:

  1. create "clean_users" view: pq: CREATE VIEW specifies more column names than columns

Here’s an example of a view defined along with its fields and its SELECT query:

  • Builder Definition
  • Raw Definition

Using the entsql.ViewFor API, you can use a dialect-aware builder to define the view. Note that you can have multiple view definitions for different dialects, and Atlas will use the one that matches the dialect of the migration.

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // Annotations of the CleanUser.
  6. func (CleanUser) Annotations() []schema.Annotation {
  7. return []schema.Annotation{
  8. entsql.ViewFor(dialect.Postgres, func(s *sql.Selector) {
  9. s.Select("id", "name", "public_info").From(sql.Table("users"))
  10. }),
  11. }
  12. }
  13. // Fields of the CleanUser.
  14. func (CleanUser) Fields() []ent.Field {
  15. return []ent.Field{
  16. // Note, unlike real schemas (tables, defined with ent.Schema),
  17. // the "id" field should be defined manually if needed.
  18. field.Int("id"),
  19. field.String("name"),
  20. field.String("public_info"),
  21. }
  22. }

Alternatively, you can use raw definitions to define the view. But note, this definition is skipped if the ViewFor annotation is defined for the dialect we generated migration to (Postgres in this case).

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // Annotations of the CleanUser.
  6. func (CleanUser) Annotations() []schema.Annotation {
  7. return []schema.Annotation{
  8. entsql.View(`SELECT id, name, public_info FROM users`),
  9. }
  10. }
  11. // Fields of the CleanUser.
  12. func (CleanUser) Fields() []ent.Field {
  13. return []ent.Field{
  14. // Note, unlike real schemas (tables, defined with ent.Schema),
  15. // the "id" field should be defined manually if needed.
  16. field.Int("id"),
  17. field.String("name"),
  18. field.String("public_info"),
  19. }
  20. }

Let’s simplify our configuration by creating an atlas.hcl file with the necessary parameters. We will use this config file in the usage section below:

atlas.hcl

  1. env "local" {
  2. src = "ent://ent/schema"
  3. dev = "docker://postgres/16/dev?search_path=public"
  4. }

The full example exists in Ent repository.

External Definition

This example demonstrates how to define an ent.View, but keeps its definition in a separate file (schema.sql) or create manually in the database.

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.View
  4. }
  5. // Fields of the CleanUser.
  6. func (CleanUser) Fields() []ent.Field {
  7. return []ent.Field{
  8. field.Int("id"),
  9. field.String("name"),
  10. field.String("public_info"),
  11. }
  12. }

After defining the view schema in Ent, the SQL CREATE VIEW definition needs to be configured (or created) separately to ensure it exists in the database when queried by the Ent runtime.

For this example, we will use Atlas’ composite_schema data source to build a schema graph from our ent/schema package and an SQL file describing this view. Let’s create a file named schema.sql and paste the view definition in it:

schema.sql

  1. -- Create "clean_users" view
  2. CREATE VIEW "clean_users" ("id", "name", "public_info") AS SELECT id,
  3. name,
  4. public_info
  5. FROM users;

Next, we create an atlas.hcl config file with a composite_schema that includes both our ent/schema and the schema.sql file:

atlas.hcl

  1. data "composite_schema" "app" {
  2. # Load the ent schema first with all its tables.
  3. schema "public" {
  4. url = "ent://ent/schema"
  5. }
  6. # Then, load the views defined in the schema.sql file.
  7. schema "public" {
  8. url = "file://schema.sql"
  9. }
  10. }
  11. env "local" {
  12. src = data.composite_schema.app.url
  13. dev = "docker://postgres/15/dev?search_path=public"
  14. }

The full example exists in Ent repository.

Usage

After setting up our schema, we can get its representation using the atlas schema inspect command, generate migrations for it, apply them to a database, and more. Below are a few commands to get you started with Atlas:

Inspect the Schema

The atlas schema inspect command is commonly used to inspect databases. However, we can also use it to inspect our ent/schema and print the SQL representation of it:

  1. atlas schema inspect \
  2. --env local \
  3. --url env://src \
  4. --format '{{ sql . }}'

The command above prints the following SQL. Note, the clean_users view is defined in the schema after the users table:

  1. -- Create "users" table
  2. CREATE TABLE "users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL, "public_info" character varying NOT NULL, "private_info" character varying NOT NULL, PRIMARY KEY ("id"));
  3. -- Create "clean_users" view
  4. CREATE VIEW "clean_users" ("id", "name", "public_info") AS SELECT id,
  5. name,
  6. public_info
  7. FROM users;

Generate Migrations For the Schema

To generate a migration for the schema, run the following command:

  1. atlas migrate diff \
  2. --env local

Note that a new migration file is created with the following content:

migrations/20240712090543.sql

  1. -- Create "users" table
  2. CREATE TABLE "users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "name" character varying NOT NULL, "public_info" character varying NOT NULL, "private_info" character varying NOT NULL, PRIMARY KEY ("id"));
  3. -- Create "clean_users" view
  4. CREATE VIEW "clean_users" ("id", "name", "public_info") AS SELECT id,
  5. name,
  6. public_info
  7. FROM users;

Apply the Migrations

To apply the migration generated above to a database, run the following command:

  1. atlas migrate apply \
  2. --env local \
  3. --url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"

Views - 图2Apply the Schema Directly on the Database

Sometimes, there is a need to apply the schema directly to the database without generating a migration file. For example, when experimenting with schema changes, spinning up a database for testing, etc. In such cases, you can use the command below to apply the schema directly to the database:

  1. atlas schema apply \
  2. --env local \
  3. --url "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable"

Or, when writing tests, you can use the Atlas Go SDK to align the schema with the database before running assertions:

  1. ac, err := atlasexec.NewClient(".", "atlas")
  2. if err != nil {
  3. log.Fatalf("failed to initialize client: %w", err)
  4. }
  5. // Automatically update the database with the desired schema.
  6. // Another option, is to use 'migrate apply' or 'schema apply' manually.
  7. if _, err := ac.SchemaApply(ctx, &atlasexec.SchemaApplyParams{
  8. Env: "local",
  9. URL: "postgres://postgres:pass@localhost:5432/database?search_path=public&sslmode=disable",
  10. }); err != nil {
  11. log.Fatalf("failed to apply schema changes: %w", err)
  12. }
  13. // Run assertions.
  14. u1 := client.User.Create().SetName("a8m").SetPrivateInfo("secret").SetPublicInfo("public").SaveX(ctx)
  15. v1 := client.CleanUser.Query().OnlyX(ctx)
  16. require.Equal(t, u1.ID, v1.ID)
  17. require.Equal(t, u1.Name, v1.Name)
  18. require.Equal(t, u1.PublicInfo, v1.PublicInfo)

Insertable/Updatable Views

If you want to define an insertable/updatable view, set it as regular type (ent.Schema) and add the entsql.Skip() annotation to it to prevent Ent from generating the CREATE TABLE statement for this view. Then, define the view in the database as described in the external definition section above.

ent/schema/user.go

  1. // CleanUser represents a user without its PII field.
  2. type CleanUser struct {
  3. ent.Schema
  4. }
  5. // Annotations of the CleanUser.
  6. func (CleanUser) Annotations() []schema.Annotation {
  7. return []schema.Annotation{
  8. entsql.Skip(),
  9. }
  10. }
  11. // Fields of the CleanUser.
  12. func (CleanUser) Fields() []ent.Field {
  13. return []ent.Field{
  14. field.Int("id"),
  15. field.String("name"),
  16. field.String("public_info"),
  17. }
  18. }