{{ .Title }}
++ {{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }} +
++ {{ .Body }} +
+ {{- end }} +diff --git a/doc/website/blog/2023-02-23-simple-cms-with-ent.mdx b/doc/website/blog/2023-02-23-simple-cms-with-ent.mdx new file mode 100644 index 000000000..139ffadbd --- /dev/null +++ b/doc/website/blog/2023-02-23-simple-cms-with-ent.mdx @@ -0,0 +1,789 @@ +--- +title: A beginner's guide to creating a web-app in Go using Ent +author: Rotem Tamir +authorURL: "https://github.com/rotemtam" +authorImageURL: "https://s.gravatar.com/avatar/36b3739951a27d2e37251867b7d44b1a?s=80" +authorTwitter: _rtam +image: "https://entgo.io/images/assets/cms-blog/share.png" +--- + +import InstallationInstructions from '../../md/components/_installation_instructions.mdx'; + +[Ent](https://entgo.io) is an open-source entity framework for Go. It is similar to more traditional ORMs, but has a +few distinct features that have made it very popular in the Go community. Ent was first open-sourced by +[Ariel](https://github.com/a8m) in 2019, when he was working at Facebook. Ent grew from the pains of managing the +development of applications with very large and complex data models and ran successfully inside Facebook for a year +before open-sourcing it. After graduating from Facebook Open Source, Ent joined the Linux Foundation in September 2021. + +This tutorial is intended for Ent and Go novices who want to start by building a simple project: a very minimal content management system. + +Over the last few years, Ent has become one of the fastest growing ORMs in Go: + + +
+ {{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }} +
++ {{ .Body }} +
+ {{- end }} ++ {{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }} +
+``` +Finally, we print the post body and close the loop. +``` ++ {{ .Body }} +
+{{- end }} +``` + +After defining the template, we need to make it available to our program. We embed this template into our binary using the `embed` package ([docs](https://pkg.go.dev/embed)): + +```go +var ( + //go:embed templates/* + resources embed.FS + tmpl = template.Must(template.ParseFS(resources, "templates/*")) +) +``` + +#### Server code + +We continue by defining a type named `server` and a constructor for it, `newServer`. This struct will have receiver methods for each HTTP handler we create and binds the Ent client we created at init to the server code. +```go +type server struct { + client *ent.Client +} + +func newServer(client *ent.Client) *server { + return &server{client: client} +} + +``` + +Next, let's define the handler for our blog home page. This page should contain a list of all available blog posts: + +```go +// index serves the blog home page +func (s *server) index(w http.ResponseWriter, r *http.Request) { + posts, err := s.client.Post. + Query(). + WithAuthor(). + All(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := tmpl.Execute(w, posts); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} +``` + +Let's zoom in on the Ent code here that is used to retrieve the posts from the database: +```go +// s.client.Post contains methods for interacting with Post entities +s.client.Post. + // Begin a query. + Query(). + // Retrieve the entities using the `Author` edge. (a `User` instance) + WithAuthor(). + // Run the query against the database using the request context. + All(r.Context()) +``` + +#### The router + +To manage the routes for our application, let's use `go-chi`, a popular routing library for Go. + +``` +go get -u github.com/go-chi/chi/v5 +``` + +We define the `newRouter` function that sets up our router: + +```go +// newRouter creates a new router with the blog handlers mounted. +func newRouter(srv *server) chi.Router { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Get("/", srv.index) + return r +} +``` + +In this function, we first instantiate a new `chi.Router`, then register two middlewares: +* `middleware.Logger` is a basic access logger that prints out some information on every request our server handles. +* `middleware.Recoverer` recovers from when our handlers panic, preventing a case where our entire server will crash because of an application error. + +Finally, we register the `index` function of the `server` struct to handle `GET` requests to the `/` path of our server. + +#### A unit test + +Before wiring everything together, let's write a simple unit test to check that our code works as expected. + +To simplify our tests we will install the SQLite driver for Go which allows us to use an in-memory database: +``` +go get -u github.com/mattn/go-sqlite3 +``` + +Next, we install `testify`, a utility library that is commonly used for writing assertions in tests. + +``` +go get github.com/stretchr/testify +``` + +With these dependencies installed, create a new file named `main_test.go`: + +```go +package main + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + _ "github.com/mattn/go-sqlite3" + "github.com/rotemtam/ent-blog-example/ent/enttest" + "github.com/stretchr/testify/require" +) + +func TestIndex(t *testing.T) { + // Initialize an Ent client that uses an in memory SQLite db. + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") + defer client.Close() + + // seed the database with our "Hello, world" post and user. + err := seed(context.Background(), client) + require.NoError(t, err) + + // Initialize a server and router. + srv := newServer(client) + r := newRouter(srv) + + // Create a test server using the `httptest` package. + ts := httptest.NewServer(r) + defer ts.Close() + + // Make a GET request to the server root path. + resp, err := ts.Client().Get(ts.URL) + + // Assert we get a 200 OK status code. + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Read the response body and assert it contains "Hello, world!" + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), "Hello, World!") +} +``` + +Run the test to verify our server works correctly: + +``` +go test ./... +``` + +Observe our test passes: +``` +ok github.com/rotemtam/ent-blog-example 0.719s +? github.com/rotemtam/ent-blog-example/ent [no test files] +? github.com/rotemtam/ent-blog-example/ent/enttest [no test files] +? github.com/rotemtam/ent-blog-example/ent/hook [no test files] +? github.com/rotemtam/ent-blog-example/ent/migrate [no test files] +? github.com/rotemtam/ent-blog-example/ent/post [no test files] +? github.com/rotemtam/ent-blog-example/ent/predicate [no test files] +? github.com/rotemtam/ent-blog-example/ent/runtime [no test files] +? github.com/rotemtam/ent-blog-example/ent/schema [no test files] +? github.com/rotemtam/ent-blog-example/ent/user [no test files] + +``` + +#### Putting everything together + +Finally, let's update our `main` function to put everything together: + +```go +func main() { + // Read the connection string to the database from a CLI flag. + var dsn string + flag.StringVar(&dsn, "dsn", "", "database DSN") + flag.Parse() + + // Instantiate the Ent client. + client, err := ent.Open("mysql", dsn) + if err != nil { + log.Fatalf("failed connecting to mysql: %v", err) + } + defer client.Close() + + ctx := context.Background() + // If we don't have any posts yet, seed the database. + if !client.Post.Query().ExistX(ctx) { + if err := seed(ctx, client); err != nil { + log.Fatalf("failed seeding the database: %v", err) + } + } + srv := newServer(client) + r := newRouter(srv) + log.Fatal(http.ListenAndServe(":8080", r)) +} +``` + +We can now run our application and stand amazed at our achievement: a working blog front page! + +``` + go run main.go -dsn "root:pass@tcp(localhost:3306)/test?parseTime=true" +``` + + + +### Step 4: Adding content + +:::info +You can follow the changes in this step in [this commit](https://github.com/rotemtam/ent-blog-example/commit/2e412ab2cda0fd251ccb512099b802174d917511). +::: + +No content mangement system would be complete without the ability, well, to manage content. Let's demonstrate how we can add support for publishing new posts on our blog. + +Let's start by creating the backend handler: +```go +// add creates a new blog post. +func (s *server) add(w http.ResponseWriter, r *http.Request) { + author, err := s.client.User.Query().Only(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := s.client.Post.Create(). + SetTitle(r.FormValue("title")). + SetBody(r.FormValue("body")). + SetAuthor(author). + Exec(r.Context()); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + http.Redirect(w, r, "/", http.StatusFound) +} +``` +As you can see, the handler currently loads the *only* user from the `users` table (since we have yet to create a user management system or login capabilities). `Only` will fail unless exactly one result is retrieved from the database. + +Next, our handler creates a new post, by setting the title and body fields to values retrieved from `r.FormValue`. This is where Go stores all of the form input passed to an HTTP request. + +After creating the handler, we should wire it to our router: +```go +// newRouter creates a new router with the blog handlers mounted. +func newRouter(srv *server) chi.Router { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Get("/", srv.index) + // highlight-next-line + r.Post("/add", srv.add) + return r +} +``` +Next, we can add an HTML `