We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Introduction
Let’s build a distributed chat service application using Elixir. It will be a simple project where users can join rooms and send messages to others, but it will also introduce some fascinating concepts and foundational building blocks in Elixir development.
This application will leverage key techniques such as LiveView, LiveComponent, Channels, PubSub, and Ecto. We’ll deploy it on AWS EC2 instances, and the application will be compiled locally using Docker.
This blog series will cover:
- Building the database with Ecto (This blog).
- Implementing chat functionality using LiveView, LiveComponent, PubSub, and Channels (https://ohhi.vn/blogs/topic?id=19).
- Compiling the application locally and deploying it on AWS EC2 (https://ohhi.vn/blogs/topic?id=18).
This is not an exhaustive guide and won’t cover every use case. It may not adhere to all security best practices, as the goal is to explore interesting concepts and provide a starting point for your development journey. Additionally, the application may not handle all error cases and could report errors unexpectedly.
Interested? Let’s get started!
Ecto Introduction
Ecto is split into 4 main components:
Ecto.Repo - repositories are wrappers around the data store. Via the repository, we can create, update, destroy and query existing entries. A repository needs an adapter and credentials to communicate to the database
Ecto.Schema - schemas are used to map external data into Elixir structs. We often use them to map database tables to Elixir data but they have many other use cases
Ecto.Query - written in Elixir syntax, queries are used to retrieve information from a given repository. Ecto queries are secure and composable
Ecto.Changeset - changesets provide a way to track and validate changes before they are applied to the data
In summary:
Ecto.Repo - where the data is Ecto.Schema - what the data is Ecto.Query - how to read the data Ecto.Changeset - how to change the data
Implement Database
The database of the application will be simple including 4 tables: Chat, User, Room and Member.
Overview of the table as bellow diagram:
Besides, we will use sqlite3 for communicating to the database instead of Postgres because when using sqlite3 we do not need to set up user name and password for it, and it is suitable for this small chat application.
We will name the application ChatService
and create a room directory under lib/chat_service
. In this directory, we will define the schemas and changesets for four tables within lib/chat_service/room
, such as: chat, member_info, room_info and user_info.
We will take a look at user_info.ex and room_info.ex:
room_info.ex
defmodule ChatService.Room.RoomInfo do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:room_id, :id, autogenerate: true}
schema "room" do
field :channel_id, :string
has_many :chat, ChatService.Room.ChatInfo, foreign_key: :room_id, references: :room_id
has_many :member, ChatService.Room.MemberInfo, foreign_key: :room_id, references: :room_id
timestamps()
end
def changeset(room, params \\ %{}) do
room
|> cast(params, [:channel_id])
|> validate_required([:channel_id])
|> unique_constraint(:room_id)
end
end
user_info.ex:
defmodule ChatService.Room.UserInfo do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:user_id, :string, autogenerate: false}
schema "user" do
has_many :chat, ChatService.Room.ChatInfo, foreign_key: :user_id, references: :user_id
has_many :member, ChatService.Room.MemberInfo, foreign_key: :user_id, references: :user_id
timestamps()
end
def changeset(chat, params \\ %{}) do
chat
|> cast(params, [:user_id])
|> unique_constraint(:user_id)
|> validate_required([:user_id])
end
def registration_changeset(user, params \\ %{}) do
cast(user, params, [])
end
end
As you can see, we will use @primary_key in the user
and room
schemas to specifically configure the schema’s primary key and
to indicate a one-to-many association with another schema we will use has_many
.
After creating all schemas and changesets, we will generate a migration using the command mix ecto.gen.migration create_room_table
. Once generated, you can check the priv/repo/migrations
directory, where you’ll find a new migration file with a timestamp like <time_stamp>_create_room_table.ex
.
In this migration file, we will create four tables, and we’ll also add unique indexes for the user, room, and member tables.
To migrate a repository we will use command mix ecto.migrate
.
API interaction with database
We will create a set of APIs to interact with the database in the lib/chat_service/room directory, and we’ll name this module room_action.ex, such as: add_room
, add_chat
, add_user
, etc.
defmodule ChatService.Room.RoomActions do
alias ChatService.Room.ChatInfo
alias ChatService.Room.RoomInfo
alias ChatService.Room.UserInfo
alias ChatService.Room.MemberInfo
alias ChatService.Repo
import Ecto.Query
@doc """
Add RoomInfo entry with channel id.
"""
@spec add_room(map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def add_room(attrs \\ %{}) do
%RoomInfo{}
|> RoomInfo.changeset(attrs)
|> Repo.insert()
end
@doc """
Add ChatInfo entry.
"""
@spec add_chat(map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def add_chat(attrs \\ %{}) do
%ChatInfo{}
|> ChatInfo.changeset(attrs, [:user_id, :room_id])
|> Repo.insert()
end
...
end
Next part, we will discover more technique to build the application such as: LiveView, LiveComponent, PubSub, and Channels. (Part 2)