Okewole Oluwatobi * learning to become a Back-end engineer

An introduction to ent

The Facebook-developed entity framework for Go

10 min read 2907

Introduction to ent

Database systems are an integral part of software development. A software developer needs to be skilled in working with databases irrespective of the programming language of choice. Most programming languages have various tools/packages that make working with database management systems easy for developers. Some of these tools are native to the programming language, others are built/maintained by the community of developers around the language and made available for free use.

The lack of a graph-based ORM(Object-relational mapping) for the Go programming language led a team of developers at Facebook to create ent. Ent is an entity framework typically used for modeling data in a graph-based structure. The ent framework prides itself on its ability to model data as Go code unlike many other ORM’s that model data as struct tags. Due to the graph-based structure of the ent framework, querying data stored in the database can be done with ease and takes the form of graph traversal. ent comes with a command-line tool which we can use to automatically generate code schema and get a visual representation of the schema.

In this post, we will explore all the cool features of the ent framework and build a simple CRUD API that leverages the various functionalities of ent.

Prerequisites

To follow along while reading this article, you will need:

Getting started with ent

The first step in working with the ent framework is to install it into our project. To install ent, run the following command go get github.com/facebook/ent/cmd/entc. The command will install entc the command-line tool for the ent package.

Throughout this article, we will build a simple CRUD(Create, Read, Update, and Delete) API that leverages ent. The API will contain five endpoints, the purpose of building this API is to show how to perform common create, read, update, and delete operations on a database using ent.

To get started, create the needed files and folders to match the tree structure below:

├── handlers/
│ ├── handler.go
├── database/
│ ├── db.go
└── main.go
  • The main.go file will contain all of the logic related to creating the server for the API. We will be using fiber, the express style framework for Go to quickly wire up our API endpoints. This article is a great start on fiber
  • The db.go file in the database directory will contain code related to creating a database connection and a client
  • The handler.go file will house the API handlers

In the next section, we will start building the API and explore ent.

A deep dive into ent

To get started with the project, run go mod init in the root directory of the project. This will initialize a new project with Go modules. Next, we have to install fiber, the framework we will use in building the API, by running the following command in the root directory of the project github.com/gofiber/fiber/v2.

In building the API for an imaginary note-taking application, we will need the following endpoints:

We made a custom demo for .
No really. Click here to check it out.

  • /api/v1/createnote
  • /api/v1/readnote/
  • /api/v1/searchnote/:title
  • /api/v1/updatenote/:id
  • /api/v1/deletenote/:id

In the main.go file, add the following lines of code:

package main

import (
   "fmt"

   "github.com/gofiber/fiber/v2"
)

func Routes(app *fiber.App){
   api := app.Group("/api/v1")

   api.Get("/", func(c *fiber.Ctx) error {
      return c.SendString("Hello, World!")
   })
}

func main() {
   app := fiber.New()

   Routes(app)

   err := app.Listen(":3000")
   if err != nil {
      fmt.Println("Unable to start server")
   }
}

The above code creates a simple web server. At the moment only one endpoint is wired up, in the coming sections we will be working in the handler.go files to ensure that all the API endpoints are functional. For now, you can run the above file and visit localhost:3000/api/v1/ on your browser. If everything went well, you should see “hello world” printed out.

Creating a schema

Creating a schema with ent is easy, thanks to entc the command-line tool we installed up above. For our API, we will create a schema called notes, to create the schema run entc init Notes in the root of the project directory. This command will automatically generate our Notes schema. The code related to the schema can be found in ent/schema/notes.go. At this point, the schema is empty and does not contain any fields. For our API, our schema will have four fields:

  • Title
  • Content
  • Private
  • Created_at

To define fields in our schema, we use the fields sub package provided by ent, inside the Field function. We invoke the type of the field, passing in the name of the desired schema field like this:

field.String("Title")

For our API, we will specify the title, content, and private fields as properties of our schema. ent currently supports all Go numeric types, string, bool, and time.Time! After adding the fields to the schema our notes.go file should look like this:

package schema

import (
   "time"

   "github.com/facebook/ent"
   "github.com/facebook/ent/schema/field"
)

// Notes holds the schema definition for the Notes entity.
type Notes struct {
   ent.Schema
}

// Fields of the Notes.
func (Notes) Fields() []ent.Field {
   return []ent.Field{
      field.String("Title").
         Unique(),
      field.String("Content"),
      field.Bool("Private").
         Default(false),
      field.Time("created_at").
         Default(time.Now),
   }
}

// Edges of the Notes.
func (Notes) Edges() []ent.Edge {
   return nil
}

The field subpackage also provides helper functions for verifying the field input as seen in the snippet above. A comprehensive list of all the built-in validators can be found here. Now that we have added the necessary fields, we can go ahead and generate some assets for working with the database.

ent automatically generates assets that include CRUD builders, and an entity object. To generate the assets, run the following command in the root of the project directory go generate./ent, You will notice that a bunch of files will be added to the /ent directory of our project. The added files contain code related to the generated assets. In the coming sections, we will learn how to use some of these generated assets to perform CRUD operations and continue building the notes API.

Visualizing a schema

entc, the command-line tool for the ent framework enables us to get a visual representation of the schema right in the terminal. To visualize the schema, simply run the following command entc describe./ent/schema in the root of the project directory, you should see a visual representation of the notes schema similar to the image below.

schema

Connecting to a database

ent provides us with functionality for connecting to a couple of databases including PostgreSQL. In the database.go file, we create an init function that connects to a database using the ent.Open function and returns a client of type ent.Client. The Open function takes in the name of the database and its connection string.

For the API we are building, we will be using a PostgreSQL database. To get started, we will spin up a docker instance of Postgres and connect to it from our local machine in three easy steps.

To follow along, you must have docker installed on your local machine.

  • Run the following command in your terminal:
    docker run -d -p 5432:5432 --name postgresDB -e POSTGRES_PASSWORD=mysecretpassword postgres

    The above command will download the official docker image for Postgres and ensure the container is running.

  • Create a database in the container by running the command below and inputting “CREATE DATABASE notesdb;” right after the above command has been executed:
    • docker exec -it my-postgres bash
  • Connect to the database container by running \c and input the password

Now that we have wired up the database container, the next thing that is needed is to import a driver for PostgreSQL as a side effect into our project. To install the driver, run go get github.com/lib/pq in the root of the project directory. With everything set up, add the following lines of code to the database.go file:

var EntClient *ent.Client
func init() {
//Open a connection to the database
   Client, err := ent.Open("postgres","host=localhost port=5432 user=postgres dbname=notesdb password=mysecretpassword sslmode=disable")
   if err != nil {
      log.Fatal(err)
   }

   fmt.Println("Connected to database successfully")
   defer Client.Close()
// AutoMigration with ENT
   if err := Client.Schema.Create(context.Background()); err != nil {
      log.Fatalf("failed creating schema resources: %v", err)
   }
   EntClient = Client
}

Saving to a database

Performing create operations/saving to a database is made easy with the ent framework. In this section, we will add the create note endpoint which will be responsible for saving new notes to the database.

To get started, in the handler.go file we create a function called createNotes that implements fibers handler interface. Inside the createNotes function, we parse the request body using the body parser function provided by fiber.

ent has helper methods that were automatically generated by entc, its command-line tool. We invoke the setTitle and setContent methods, passing in their respective values as type string. Lastly, to ensure the data is saved, we invoke the save method passing in a context value:

func CreateNote(c *fiber.Ctx) error{
//Parse the request body
   note := new(struct{
      Title string
      Content string
      Private bool
   })

   if err := c.BodyParser(&note); err != nil {
      c.Status(400).JSON("Error  Parsing Input")
      return err
   }
//Save to the database
   createdNote, err := database.EntClient.Notes.
      Create().                      
      SetTitle(note.Title).
      SetContent(note.Content).
      SetPrivate(note.Private).
      Save(context.Background())  

   if err != nil {
      c.Status(500).JSON("Unable to save note")
      return err
   }
//Send the created note back with the appropriate code.
   c.Status(200).JSON(createdNote)

   return nil
}

At this point, we are all set and have added the logic for creating a new entity. To register the above handler simply add the following line of code to the routes function we created up above in the main.go file:

api.Post("/createnote", handlers.CreateNote)

If we start the application and make a post request to localhost:3000/api/v1/createnote, passing in a title and content for a note you should see an output similar to the image below indicating that the note has been successfully created.

note created successfully

Reading from a database

Querying the database is made easy with ent. entc generates a package for each schema that contains useful assets for searching the database. On the client for interacting with the automatically generated builders, we invoke the Query function. This function returns a query builder for the schema, part of the builders include Where and Select.

In this section, we will code up the logic for two endpoints:

  1. /api/v1/readnotes/ – This endpoint will enable us to read all the notes in the database
  2. /searchnotes/:title – This endpoint enable us to search the database for a specific note by title

We will get started by building the /api/v1/readnotes/ endpoint. In the handlers.go file, we create a handler function called ReadNotes similar to the createnote function above that implements the fibers handler interface. In the ReadNotes function, we invoke Query on the EntClient variable. To specify that we want all the records that match the query, we invoke All on the query builder. At this point the complete ReadNotes function should look similar to this:

func ReadNote(c *fiber.Ctx) error{
   readNotes, err := database.EntClient.Notes.
      Query().
      All(context.Background())
   if err != nil {
      c.Status(500).JSON("No Notes Found")
      log.Fatal(err)
   }
   c.Status(200).JSON(readNotes)
   return nil
}

The ReadNotes handler function is ready, we can now go ahead and register it on the server by adding the following line to the Routes function in main.go:

api.Get("/readnotes", handlers.ReadNote)

We can now start up our application and visit the route /api/v1/readnotes/ to test it. If everything went well, you should see an array containing all the notes in the database as seen in the image below:

Read note

The readnotes endpoint for reading all the notes stored in the database has been wired up, next we will wire up the searchnotes endpoint that will search the database for any notes whose title match up with the search query. Just like we have done for every handler till this point, we create a function called SearchNotes.

In this function, we retrieve the search query which was passed as a request parameter using fibers built-in params method. Next, we invoke the Query builder method on the client as we did for the ReadNotes function. To specify the search query, we invoke another method called where, the where method adds a new predicate to the query builder. As an argument to the where method we can pass in the title predicate which was automatically generated by entc:

func SearchNotes(c *fiber.Ctx) error{
//extract search query
   query := c.Params("title")
   if query == "" {
      c.Status(400).JSON("No Search Query")
   }
 //Search the database
   createdNotes, err := database.EntClient.Notes.
      Query().
      Where(notes.Title(query)).
      All(context.Background())

   if err != nil {
      c.Status(500).JSON("No Notes Found")
      log.Fatal(err)
   }

   c.Status(200).JSON(createdNotes)
   return nil
}

Lastly, we can register the SearchNotes function by adding the following line of code to the main.go file:

api.Get("/searchnotes/:title", handlers.SearchNotes)

We are done with the searchnotes endpoint and can test it by starting up the application and visiting localhost:3000/api/v1/searchnotes/Lorem. If everything went well, you should see a note titled Lorem returned if it exists in the database.

Updating a record

When building an API, it is important to provide functionality for updating a record in a database as it fits your business logic. ent makes updating a record easy, thanks to all the generated assets that contain builder functions. In this section, we will build out the update route for our notes API and learn how to update a record with ent.

To get started, we head over to the handlers.go file and create a function called UpdateNotes. This function, like other functions in the handler.go file implements fiber’s handler interface. In the UpdateNotes function, we parse the request body to ensure that only the content field can be updated. Next, we retrieve the ID of the record to be updated from the query parameter by invoking the params function with the key. Since we retrieve query parameters with fibers as type string, we have to convert the retrieved parameter to Int that corresponds to the type stored in the database using the Atoi function available in the strconv package.

To update the record, we call the function UpdateOneId and pass in the ID we retrieved from the user up above. Calling the UpdateOneId function returns an update builder for the given ID. Next, we call the setContent function. The setContent was automatically generated based on the schema and fields we declared up above. The setContent function takes in the specified update to the content field of our schema. Finally, we can save the updated record by calling the Save function with a context:

func UpdateNote(c *fiber.Ctx) error{
//Parse the request Body
   note := new(struct{
      Content string
   })

   if err := c.BodyParser(&note); err != nil {
      c.Status(400).JSON("Error  Parsing Input")
      return err
   }
//Extract & Convert the request parameter
   idParam := c.Params("id")
   if idParam == "" {
      c.Status(400).JSON("No Search Query")
   }
   id, _ := strconv.Atoi(idParam)
//Update the note in the database
   UpdatedNotes, err := database.EntClient.Notes.
      UpdateOneID(id).
      SetContent(note.Content).
      Save(context.Background())

   if err != nil {
      c.Status(500).JSON("No Notes Found")
      log.Fatal(err)
   }

   c.Status(200).JSON(UpdatedNotes)
   return nil
}

With the UpdateNote function looking like this, we are good to go and can register the handler by adding the following line of code to the Routes function:

api.Put("/updatenote/:id", handlers.UpdateNote)

Making a put request to the above route and providing a valid record ID, updates the corresponding record.

Deleting a record

Deleting a record is similar to the update operation, however, when deleting a record with ent, different functions are used. To delete a record the DeleteOneId function that receives an ID returns a delete builder for the given user is used. We also invoke the Exec function. Exec takes in a context and executes the delete operation on the database:

func DeleteNotes(c *fiber.Ctx) error{
   idParam := c.Params("id")
   if idParam == "" {
      c.Status(400).JSON("No Search Query")
   }

   id, _ := strconv.Atoi(idParam)
//Delete the Record frm the databse
   err := database.EntClient.Notes.
      DeleteOneID(id).
      Exec(context.Background())

   if err != nil {
      c.Status(500).JSON("Unable to Perform Operation")
   }

   c.Status(200).JSON("Success")

   return nil
}

We can register the above handler function by adding the following line of code to the route function in the handler.go file:

api.Delete("/deletenote/:id", handlers.DeleteNotes)

The deletenote route is all set! You can now delete any note in the database by specifying its ID.

Conclusion

So far, we have built an API for a note-taking application using the ent framework to interact with a PostgreSQL database. We didn’t have to write any SQL queries or worry so much about the logic for performing the CRUD operations, thanks to ent and the assets generated by entc. This article aims to get you up and running with ent. I strongly recommend you check out the official documentation as a reference guide. The complete source code for this project can be found here.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Okewole Oluwatobi * learning to become a Back-end engineer

Leave a Reply