Elixir+Absinthe
An intoxicating GRAPHQL
What is GRAPHQL?
A query language for your API
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.There are descriptions from the official website. Actually GraphQL is the real leading role in this article. Someone might ask we’ve already own RESTful, why should we use GraphQL? With the rapid growth of the hardware, client sides’s performance are faster and faster. So more and more business logic migrate to client that let client side become complex. In today software engineering, RESTful’s regular interface way seems not flexible enough right now. For an example, A view display a data in database. Some states only display some specific fields and some have to display all. In RESTful may needs multiple interfaces and client also needs to send multiple request to finish the business logic. It may waste resources and hard to let feature extend. But right now GraphQL only provide one interface and let client decide what data it wants. This solution make Api simple and scalable.
Elixir? Absinthe?
Elixir is a web DSL (Domain Specific Language). It’s father and mother are Erlang and Ruby. This language provide best distributed design. WhatsApp、Discord and Nintendo switch NPNS(NPNS handle 20 billion messages a day) are used elixir. And it’s architecture and “Let it crash” theory influence me deeply. Absinthe is the GraphQL Web framework of elixir. Absinthe let everything easy.
Let us begin
I’ll focus on introducing how to build GraphQL Api, so I’ll skip all knowledges about elixir. Again, GraphQL is the real leading role.
Step 1: install elixir and phoenix
Step 2: install libraries
defp deps do
[
{:absinthe, "~> 1.4"},
{:absinthe_plug, "~> 1.4"},
{:absinthe_phoenix, "~> 1.4"},
{:new_relic_absinthe, "~> 0.0.3"},
{:dataloader, "~> 1.0"}
]
end
Step 3: Start to build GraphQL API
There are three important members in GraphQL
Schema: GraphQL server uses a schema to describe the shape of your data graph. This schema defines a hierarchy of types with fields that are populated from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute against your data graph.
Types: A GraphQL object type has a name and fields, but at some point those fields have to resolve to some concrete data. That’s where the scalar types come in: they represent the leaves of the query.
Resolver: A resolver is a function that’s responsible for populating the data for a single field in your schema. It can populate that data in any way you define, such as by fetching data from a back-end database or a third-party API.
OK,I know it’s convenience to write everything in schema, but I like form a good habit in the beginning.
Build your types files like this:
defmodule Schema.Type.AccountTypes do use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1] @doc "Foodie type"
object :foodie do
field :email, :string
field :name, :string
field :shop_branches, list_of(:shop_branch),
resolve: dataloader(Data)
end
After this I’ll introduce three actions in GraphQL:
Query: To query data from database
object :account_queries do
field :list_foodies, list_of(:foodie) do
resolve(&Resolvers.AccountResolver.list_foodies/3)
end
field :find_foodie, :foodie do
arg(:unique_id, non_null(:string))
resolve(&Resolvers.AccountResolver.find_foodie/3)
end
end
Mutation: Update data in database:
object :account_mutations do field :create_foodie, :foodie do
arg(:email, non_null(:string))
arg(:name, non_null(:string))
resolve(&Resolvers.AccountResolver.create_foodie/3)
end
end
Subscribe: Trigger another action after an action was executed.
Ex: Update view after updating data
OK, in the sample code, you can see where to use resolver. Let us see how to implement it.
defmodule Resolvers.AccountResolver do def list_foodies(_parent, _args, _resolutions) do
{:ok, Accounts.list_foodies()}
end
def find_foodie(_parent, args, _resolutions) do
{:ok, Accounts.find_foodie!(args[:unique_id])}
end
def create_foodie(_parent,%{} = args,_resolutions) do
new_args
|> Accounts.create_foodie
|> case do
{:ok, foodie} ->
{:ok, foodie}
{:error, changeset} ->
{:error, extract_error_msg(changeset)}
end
end
end
Nothing special, just how you handle database in the usual.
Context
In GraphQL, a context is an object shared by all the resolvers of a specific execution. It’s useful for keeping data such as authentication info, the current user, database connection, data sources and other things you need for running your business logic.
DataLoader
N+1 Problem:
Before introduce DataLoader, I have to tell about the performance issue in GraphQL: N+1 problem. Simply put, N + 1 is the number of db operations. For example:
Shops and Branches are has_many relationship. So when querying shops and their branches n+1 way will be:
Step1: query shops
Steps2: use shop_ids to search branches one by one.
If you are a backend master, you’ll discover step2 is where the issue is. That’s why Facebook develop DataLoader to fix it. So let us see how it works?
How DataLoader works:
There are two parts in DataLoader, Batching and Caching.
Batching: Gathering ids and when the times come, it search in DB once.
Caching: filter duplicated ids and reduce the number of searches.
In Absinthe, it provides an awesome tools let the codes are very simple:
def data() do
Dataloader.Ecto.new(PrjName.Repo, query: &query/2)
enddef query(queryable, params) do
case Map.get(params, :order_by) do
nil -> queryable
order_by -> from record in queryable, order_by: ^order_by
end
end
That’s why I said Absinthe let Graphql intoxicating. Fast and easy.
Finally in schema code:
defmodule Schema do
use Absinthe.Schema
import_types(Absinthe.Type.Custom)
import_types(Schema.Type.AccountTypes)
import_types(Schema.Type.InputTypes) query do
import_fields(:account_queries)
import_fields(:gourmet_queries)
import_fields(:location_queries)
end
mutation do
import_fields(:account_mutations)
import_fields(:gourmet_mutations)
import_fields(:location_mutations)
end
def context(ctx) do
loader = Dataloader.new()
|> Dataloader.add_source(Data, Data.data())
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
def middleware(middleware, _field, _object) do
[NewRelic.Absinthe.Middleware] ++ middleware
end
end
And then set up Router:
# GraphQL scope
scope "/api" do
pipe_through :api
forward "/gql_api", Absinthe.Plug,
schema: ProjectName.Schema.Schema
forward "/graphiql", Absinthe.Plug.GraphiQL,
schema: ProjectName.Schema.Schema,
interface: :simple
end
Demo:
References:
Graphql official webside:
https://graphql.org
Elixir/Phoenix official webside:
https://www.phoenixframework.org
Special thanks:
Elixir Taiwan
https://www.facebook.com/groups/elixir.tw