Phoenix Example Application
The following tutorial shows how to implement an Elixir app using the Phoenix framework with the Ecto ORM. The app connects to a YugabyteDB cluster, creates a sample table, populates it with a few records, and changes data transactionally.
The source for the application can be found in the yugabytedb-simple-elixir-app repository.
Prerequisites
This tutorial assumes that you have:
-
YugabyteDB up and running. Download and install YugabyteDB by following the steps in Quick start.
-
The latest versions of Elixir, Erlang VM, IEx and Mix (tested with Elixir 1.17.1 and Erlang/OTP 26 erts-14.2.5).
Create Phoenix project
In this section you create a Phoenix project and configure it to work with an existing YugabyteDB cluster.
First, use the mix
tool to create a project template and pull all required dependencies:
-
Generate the project template and navigate to its root folder:
mix phx.new sample_yugabytedb_phoenix_app && cd sample_yugabytedb_phoenix_app
-
Pull the required dependencies, unless you've already done this during the project generation:
mix deps.get
Phoenix uses PostgreSQL as a default database option. Open the config/dev.exs
file to confirm that the Ecto ORM is configured to work PostgreSQL:
# Configure your database
config :sample_yugabytedb_phoenix_app, SampleYugabytedbPhoenixApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "sample_yugabytedb_phoenix_app_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
As YugabyteDB is built on PostgreSQL, you only need to update the config
settings. Both Ecto and the underlying Postgrex driver will work with YugabyteDB as they do with vanilla PostgreSQL.
Assuming that your YugabyteDB cluster is running locally, do the following:
-
Open the
config/dev.exs
file -
Update the database settings to the following:
config :sample_yugabytedb_phoenix_app, SampleYugabytedbPhoenixApp.Repo, username: "yugabyte", password: "yugabyte", hostname: "localhost", port: 5433, database: "yugabyte", stacktrace: true, show_sensitive_data_on_connection_error: true, pool_size: 10
-
Add the
migration_lock: false
setting to the database configuration. Ecto acquires exclusive table locks while running database migrations, but this type of lock is not currently supported by YugabyteDB. Track GitHub issue 5384 to understand details and get updates.config :sample_yugabytedb_phoenix_app, SampleYugabytedbPhoenixApp.Repo, <!-- other database specific settings --> pool_size: 10, migration_lock: false
-
Have Ecto connecting to YugabyteDB to complete and confirm the configuration settings:
mix ecto.create
The output should be as follows:
Compiling 15 files (.ex)
Generated sample_yugabytedb_phoenix_app app
The database for SampleYugabytedbPhoenixApp.Repo has already been created
Generate sample schema
In this section, you use Phoenix and Ecto to generate a sample database schema for the Account
entity and to apply database migrations.
-
Generate the schema for the
Account
entity. Note, theid
field of thebigint
type will be added to the generated table automatically:mix phx.gen.schema Account account name:string balance:decimal
-
Open the
lib/sample_yugabytedb_phoenix_app/account.ex
to see the generated Ecto schema:defmodule SampleYugabytedbPhoenixApp.Account do use Ecto.Schema import Ecto.Changeset schema "account" do field :name, :string field :balance, :decimal timestamps(type: :utc_datetime) end <!-- other logic --> end
-
Run the database migration file that was also generated automatically and placed under the
priv/repo/migrations
directory:mix ecto.migrate
The output should be as follows:
Compiling 1 file (.ex) Generated sample_yugabytedb_phoenix_app app 10:58:16.251 [info] == Running 20240725145331 SampleYugabytedbPhoenixApp.Repo.Migrations.CreateAccount.change/0 forward 10:58:16.252 [info] create table account 10:58:16.443 [info] == Migrated 20240725145331 in 0.1s
Next, connect to your YugabyteDB cluster using psql, ysqlsh, or another SQL tool to confirm the tables were successfully created:
yugabyte=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | account | table | yugabyte
public | account_id_seq | sequence | yugabyte
public | schema_migrations | table | yugabyte
(3 rows)
Persist and query data
In this final section, you use Phoenix APIs and the IEx interactive shell to persist and query data in YugabyteDB.
-
Start an IEx session in your terminal:
iex -S mix
-
Create aliases for the
Repo
andAccount
modules:alias SampleYugabytedbPhoenixApp.{Repo, Account}
-
Insert two user accounts into the database:
Repo.insert(%Account{name: "John Smith", balance: 1000}) Repo.insert(%Account{name: "Mary Black", balance: 1500})
-
Fetch the accounts from the database:
Repo.all(Account)
The output should be as follows:
[debug] QUERY OK source="account" db=25.7ms queue=47.5ms idle=1922.1ms SELECT a0."id", a0."name", a0."balance", a0."inserted_at", a0."updated_at" FROM "account" AS a0 [] ↳ :elixir.eval_external_handler/3, at: src/elixir.erl:386 [ %SampleYugabytedbPhoenixApp.Account{ __meta__: #Ecto.Schema.Metadata<:loaded, "account">, id: 101, name: "Mary Black", balance: Decimal.new("1500"), inserted_at: ~U[2024-07-25 15:11:12Z], updated_at: ~U[2024-07-25 15:11:12Z] }, %SampleYugabytedbPhoenixApp.Account{ __meta__: #Ecto.Schema.Metadata<:loaded, "account">, id: 1, name: "John Smith", balance: Decimal.new("1000"), inserted_at: ~U[2024-07-25 15:11:12Z], updated_at: ~U[2024-07-25 15:11:12Z] } ]
Note that the id
of the Mary Black
account is 101
and not 2
. This happens because sequences in YugabyteDB cache 100
IDs per connection by default. This means that records were inserted into the database using different database connections.
yugabyte=# \d account_id_seq
Sequence "public.account_id_seq"
Type | Start | Minimum | Maximum | Increment | Cycles? | Cache
--------+-------+---------+---------------------+-----------+---------+-------
bigint | 1 | 1 | 9223372036854775807 | 1 | no | 100
Owned by: public.account.id
For details on how sequences work in YugabyteDB and how to scale them, refer to the Scaling Sequences with Server-Level Caching in YugabyteDB.
Finally, use the Ecto.Query
interface to query specific data:
import Ecto.Query
Repo.all(from a in Account, where: a.balance >= 1100,
select: %{a.name => a.balance})
The output should be as follows:
[%{"Mary Black" => Decimal.new("1500")}]
Congratulations! You've successfully completed the tutorial.
Now, explore the final version of the application to see how work with YugabyteDB using the same APIs but programmatically.
Next steps
-
Watch Chris McCord, the creator of the Phoenix framework, joining YugabyteDB Community Open Hours and demonstrating how to build multi-region applications using Phoenix, Fly.io and YugabyteDB.
-
Create a sample Elixir app using the Postgrex driver.