Setting up Models with Ecto and Adding Routing . Part 2


Phoenix Elixir stutorial

Other articles in the series

  • Making our UI user friendly - Part 15
  • Deploying Phoenix VueJS application using Docker - Part 14
  • Fixing failing Elixir tests - Part 13
  • Adding new features to Order Management - Part 12
  • Add Order Management - Part 11
  • Refactoring and adding tests for Phoenix - Part 10
  • Refactoring VueJS with Typescript- Part 9
  • Add Customer Management - Part 8
  • Writing tests with Jest and Typescript - Part 7
  • Adding Vuex to Vue using Typescript - Part 6
  • Building our Homepage view component - Part 5
  • Add Multi-language support to Vue Typescript - Part 4
  • Generate Vue Typescript Application - Part 3
  • Setting up Models with Ecto and Adding Routing . Part 2
  • Setting up Your Phoenix Application - Part 1
  • Tutorial Series for building a VueJS (Typescript) and Phoenix(Elixir) Shop Management Application - Part 0

  • The back-end related code for this post is available here.

    Generate tables using Ecto

    It is time to make our models and tables using Ecto. Ecto is the library phoenix offers for working with databases. It includes a DSL in which we will be writing our queries. Ecto queries are composable and prevent SQL injections. Lets start with the Products table. If you are unsure how the table structure should look like, refer previous blog posts. The phx.gen.json task generates the migrations, tables/models, the view and the controllers. The views generated will be emit JSON. Don’t worry, we will discuss it all :D.

    mix phx.gen.json Inventory Product products price:float stock:integer name:string tax:float

    Here the Inventory is called as the context module. The phoenix docs explains context as follows. “The context is an Elixir module that serves as an API boundary for the given resource”. In simpler terms, it is like a place, where we put things which belong together. For example, OrderManagement deals with orders. An order can consist of multiple order items. If we think, these two should be considered together/offers a functionality together, we put them in the same context, say OrderManagement. Having a context is beneficial due to several reasons. One reason is that we establish an API boundary. If some module needs to talk with our module, it should use the API. This keeps our modules self-contained and prevents unnecessary coupling with other modules. See this link for more info about bounded contexts.

    The Product here is our model, with all the fields for data. products will be name of the database table and is normally the plural form of Product. The others price:float stock:integer name:string tax:float are the fields/columns in the model/database table with their types. Lets see what files are created by this task.

    In the lib/ms/inventory we see a new product.ex file.

    # defmodule makes a Module in Elixir. Modules are like packages in other languages like Java. 
    defmodule Ms.Inventory.Product do
      # use is the keyword used for making use of macros. It also executes __using__ function in the Ecto.Schema module. 
      # It injects code to our module at compile time. 
      use Ecto.Schema
      # import is like import in other languages like Java/Python. It import the Ecto.Changeset into our module so we can use the changeset() function
      import Ecto.Changeset
    
      schema "products" do
        field :name, :string
        field :price, :float
        field :stock, :integer
        field :tax, :float
        field :details, :map
        belongs_to(:brand, Ms.Inventory.Brand)
    
        # Inserts updated and created timestamp fields to the database
        timestamps()
      end
    
       # Functions in Elixir and declared using def.
       @doc false
      def changeset(product, attrs) do
        product
        # cast is like telling we needs these fields from the attrs. [cast/4](https://hexdocs.pm/ecto/Ecto.Changeset.html#cast/4)
        |> cast(attrs, [:price, :stock, :name, :tax])
        |> validate_required([:price, :stock, :name, :tax]) # validate is for making sure these values should exist inorder to create a product. We can also add more requirements these values should satisfy.
      end
    end

    Ecto Schema

    Schema is like a structure of the Project in database. This is similar with the typescript interface we defined earlier. changeset is a function, which tranforms the incoming data to the model structure, before being stored into the database. We can add validation for data or other functionality inside the changesets. Please read the comments in the above to code to learn more.

    Now we will look at the migration file in the priv/repo/migrations/_create_products.exs.

    defmodule Ms.Repo.Migrations.CreateProducts do
      use Ecto.Migration
    
      def change do
        create table(:products) do
          add :price, :float
          add :stock, :integer
          add :name, :string
          add :tax, :string
    
          timestamps()
        end
    
      end
    end

    As you can see it has a one to one correspondence with the schema in product.ex file. The add :price, :float is a way of telling that we need to add a field named price with a type float. The change function in a migration file will be called when we do the mix ecto.migrate. Since they are ran sequentially based on the name of file with date in it, we should we careful when editing the migration files manually. Views are formally rendering the schema/model we generated. Since we used mix phx.gen.JSON our generated views produce JSON output. Phoenix also ships with other tasks, which will generate HTML for us. Lets take a look at the views file generated at ms_web/views/product_view.ex

    defmodule MsWeb.ProductView do
      use MsWeb, :view
      alias MsWeb.ProductView
    
      # Elixir have a very good support for pattern matching. In this case the calls to render function, from controller will be automatically picked based on the arguments. Also the order in which the functions are defined is important, as they are searched in top to down order. 
      def render("index.json", %{products: products}) do
        # render_many will render multiple products based on product.json, which is the last defined render function in this file.
        %{data: render_many(products, ProductView, "product.json")}
      end
    
      def render("show.json", %{product: product}) do
        # This function will render a single product using the last render function. 
        %{data: render_one(product, ProductView, "product.json")}
      end
    
      # This is our actual function, which does all the rendering. Other functions call this function. It simply generates a JSON file with the fields defined below. We will see exactly how it looks, when we use it in the frontend.
      def render("product.json", %{product: product}) do
        %{
          id: product.id,
          price: product.price,
          stock: product.stock,
          name: product.name,
          tax: product.tax,
        }
      end
    end

    Routing in Phoenix

    Now we will look at the generated controller at ms_web/controllers/product_controller.ex. Before we look at the controllers, we should look at phoenix routes, which will map the CRUD operations using REST on to the functions defined here. It can be seen in the lib/ms_web/router.ex file. Lets modify it as follows to support REST API, by adding endpoints and routes.

    defmodule MsWeb.Router do
      use MsWeb, :router
    
      # The pipeline determines how this request should be processed. A pipeline consists of multiple plugs. A plug is like a layer/function which takes a connection structure, which contains details about the request and outputs another plug. It can add things to current connection or delete things from there. For example the plug :protect_from_forgery adds some CSRF token to the request. 
    
      pipeline :api do
        plug :accepts, ["json"]
      end
    
      # Scope provides our API endpoints, this shows that we got an endpoint at /api.
      scope "/api", MsWeb do
        pipe_through :api
    
        # Nested scopes, this part is hit, when the url looks like https://localhost/api/v1
        scope "/v1" do
          # Resource is a shorthand for telling it should support GET, PUT, POST, DELETE and other methods. It automatically handles all the HTTP verbs, with their functions. We can easily see the urls, their HTTP verbs along with executed function by executing mix phx.routes. We explain it below. 
          resources "/products", ProductController
        end
      end
    end

    Listing Routes in Phoenix

    Lets now execute mix phx.routes. It will give out the following results

    product_path  GET     /api/v1/products           MsWeb.ProductController :index
    product_path  GET     /api/v1/products/:id/edit  MsWeb.ProductController :edit
    product_path  GET     /api/v1/products/new       MsWeb.ProductController :new
    product_path  GET     /api/v1/products/:id       MsWeb.ProductController :show
    product_path  POST    /api/v1/products           MsWeb.ProductController :create
    product_path  PATCH   /api/v1/products/:id       MsWeb.ProductController :update
                  PUT     /api/v1/products/:id       MsWeb.ProductController :update
    product_path  DELETE  /api/v1/products/:id       MsWeb.ProductController :delete
       websocket  WS      /socket/websocket          MsWeb.UserSocket

    Like mentioned before, all verbs were generated from the resources /products from router.ex file. If running mix fails for reason, always remember to run it from the directory, where our .mix file is present. Finally we will examine controller.

    Mapping Request to Phoenix Controllers

    Lets see how a request gets mapped to the functions in the controller. If we send a request like GET /api/v1/products/ it will hit the MsWeb.ProductController.index function. This can be see from the output of mix phx.routes. It always provides the conn(The plug with information about request) and _params(The parameters with the request). Any variable which starts with an _ in elixir is considered as a variable, whose value we are not interested in. Since index function doesn’t need any parameters, we can ignore it. We see that it just call Inventory.list_products() and renders the result via render function call. This will call the render method in the view file.

    defmodule MsWeb.ProductController do
      use MsWeb, :controller
    
      # alias allows accessing module Ms.Inventory with just name Inventory
      alias Ms.Inventory
      alias Ms.Inventory.Product
    
      action_fallback MsWeb.FallbackController
    
      def index(conn, _params) do
        products = Inventory.list_products()
        render(conn, "index.json", products: products)
      end
    
      def create(conn, %{"product" => product_params}) do
        with {:ok, %Product{} = product} <- Inventory.create_product(product_params) do
          conn
          |> put_status(:created)
          |> put_resp_header("location", Routes.product_path(conn, :show, product))
          |> render("show.json", product: product)
        end
      end
    
      def show(conn, %{"id" => id}) do
        product = Inventory.get_product!(id)
        render(conn, "show.json", product: product)
      end
    
      def update(conn, %{"id" => id, "product" => product_params}) do
        product = Inventory.get_product!(id)
    
        with {:ok, %Product{} = product} <- Inventory.update_product(product, product_params) do
          render(conn, "show.json", product: product)
        end
      end
    
      def delete(conn, %{"id" => id}) do
        product = Inventory.get_product!(id)
    
        with {:ok, %Product{}} <- Inventory.delete_product(product) do
          send_resp(conn, :no_content, "")
        end
      end
    end

    Now lets look at the list_products function. The file is too big, so lets discuss just the first function.

    defmodule Ms.Inventory do
      @moduledoc """
      The Inventory context.
      """
    
      import Ecto.Query, warn: false
      alias Ms.Repo
    
      alias Ms.Inventory.Product
    
      @doc """
      Returns the list of products.
    
      ## Examples
    
          iex> list_products()
          [%Product{}, ...]
    
      """
      def list_products do
        Repo.all(Product)
      end
    
      @doc """
      Gets a single product.
    
      Raises `Ecto.NoResultsError` if the Product does not exist.
    
      ## Examples
    
          iex> get_product!(123)
          %Product{}
    
          iex> get_product!(456)
          ** (Ecto.NoResultsError)
    
      """
      def get_product!(id), do: Repo.get!(Product, id)
    
      @doc """
      Creates a product.
    
      ## Examples
    
          iex> create_product(%{field: value})
          {:ok, %Product{}}
    
          iex> create_product(%{field: bad_value})
          {:error, %Ecto.Changeset{}}
    
      """
      def create_product(attrs \\ %{}) do
        %Product{}
        |> Product.changeset(attrs)
        |> Repo.insert()
      end
    
      @doc """
      Updates a product.
    
      ## Examples
    
          iex> update_product(product, %{field: new_value})
          {:ok, %Product{}}
    
          iex> update_product(product, %{field: bad_value})
          {:error, %Ecto.Changeset{}}
    
      """
      def update_product(%Product{} = product, attrs) do
        product
        |> Product.changeset(attrs)
        |> Repo.update()
      end
    
      @doc """
      Deletes a Product.
    
      ## Examples
    
          iex> delete_product(product)
          {:ok, %Product{}}
    
          iex> delete_product(product)
          {:error, %Ecto.Changeset{}}
    
      """
      def delete_product(%Product{} = product) do
        Repo.delete(product)
      end
    
      @doc """
      Returns an `%Ecto.Changeset{}` for tracking product changes.
    
      ## Examples
    
          iex> change_product(product)
          %Ecto.Changeset{source: %Product{}}
    
      """
      def change_product(%Product{} = product) do
        Product.changeset(product, %{})
      end
    end

    We see that list_products just calls Repo.all(Product). This is a function to query the database. What it means is that get me all the rows with Product schema. So now we know when we hit the index function, we will get all the data about that specific model. Similarly to get a row using an id, we use Repo.get(), and to delete a value we use Repo.delete(). For updation, we use Repo.update. Here we simply update the changeset with the new data and insert it to the database. Changesets are a very useful concept, for making database operations cleaner and also offers a standard interface to work with database operations.

    At this point, I believe we covered the required basics for working with Phoenix and Ecto. More concepts will be explained as we progress through the tutorial. Lets go ahead and generate the other tables. All generated files follow a similar structure.

    mix phx.gen.json Inventory Brand brands name:string details:map

    An astute, reader will notice that the Product model, we generated before, didn’t include a brand_id. It was on purpose, first we need to generate a brands table to provide a reference to it. Now somehow we need to integrate this into our already existing Product model. We can use the ecto.gen.migration task to do it. Lets create a new migration as follows.

    mix ecto.gen.migration add_product_details

    Now there will be a file with suffix _add_product_details.exs in the priv/migrations/ folder. Lets add the brand_id and some details to the migration. Edit the file as below.

    defmodule Ms.Repo.Migrations.AddProductDetails do
      use Ecto.Migration
    
      def change do
        alter table(:products) do
          add :brand_id, references(:brands, on_delete: :delete_all)
          add :details, :map
        end
      end
    end

    As you can see the brand_id references the brands table. It is a good time to run migrations for already generated models.

    mix ecto.migrate

    Now we need to modify the Product schema to include the brand_id and details. Lets change it as follows.

    defmodule Ms.Inventory.Product do
      use Ecto.Schema
      import Ecto.Changeset
    
      schema "products" do
        field :name, :string
        field :price, :float
        field :stock, :integer
        field :tax, :float
        field :details, :map # Newly added details
        belongs_to(:brand, Ms.Inventory.Brand) # Foreign key for brand
    
        timestamps()
      end
    
      @doc false
      def changeset(product, attrs) do
        product
        |> cast(attrs, [:price, :stock, :name, :tax, :details])
        |> validate_required([:price, :stock, :name, :tax])
      end
    end

    You will also notice we didn’t add brand_id to cast. It is because adding brand_id will require a brand_id to exist before we create a product. This will be fixed in later sections. Now lets generate the other tables.

    # The details:map is of type map, which in postgres is JSON. So details will be a JSON field.
    mix phx.gen.json CustomerManagement Customer customers name:string phone:string pincode:string details:map
    mix ecto.migrate
    
    # The argument, customer:references:customers says that the field *customer* is a reference to table customers.
    mix phx.gen.json OrderManagement Order orders customer:references:customers creationDate:utc_datetime message:string details:map
    mix ecto.migrate
    
    mix phx.gen.json OrderManagement OrderItem order_items product:references:products amount:integer unitPrice:float order:references:orders
    mix ecto.migrate
    
    mix phx.gen.json DeliveryManagement Delivery deliveries orderitem:references:order_items fare:float address:map details:map customer:references:customers
    mix ecto.migrate

    Now we will add paths for newly generated models as follows to the /api/v1/.

        scope "/v1" do
          resources "/products", ProductController
          resources "/brands", BrandController
          resources "/orders", OrderController
          resources "/customers", CustomerController
          resources "/order_items", OrderItemController
          resources "/deliveries", DeliveryController
        end

    Now we have a bare minimum backend, which will respond to our HTTP requests. As you notice our models have some limitations, which we will address in the coming up parts.