Basics
Ecto adalah sebuah project resmi Elixir yang memberikan sebuah wrapper (pembungkus) terhadap database dan bahasa query yang terintegrasi. Dengan Ecto kita bisa membuat migrasi, mendefinisikan model, melakukan insert dan update data, dan melakukan query.
Setup
Untuk mulai kita perlu menginclude Ecto dan sebuah adapter database dalam mix.exs
project kita. Anda bisa menemukan daftar adapter database yang didukung di bagian Usage section dari README Ecto. Sebagai contoh kita akan gunakan Postgresql:
defp deps do
[{:ecto, "~> 1.0"}, {:postgrex, ">= 0.0.0"}]
end
Sekarang kita bisa tambahkan Ecto dan adapter kita ke application:
def application do
[applications: [:ecto, :postgrex]]
end
Repository
Akhirnya kita perlu membuat repositori project kita, wrapper untuk databasenya. Ini bisa dilakukan lewat task mix ecto.gen.repo -r FriendsApp.Repo
. Kita akan membahas task mix Ecto nanti. Repo bisa ditemukan di lib/<project name>/repo.ex
:
defmodule FriendsApp.Repo do
use Ecto.Repo, otp_app: :example_app
end
Supervisor
Setelah kita membuat Repo, kita perlu mensetup pohon supervisor (supervisor tree) kita, yang biasanya ditemukan di lib/<project name>.ex
.
Penting dicatat bahwa kita mensetup Repo sebagai sebuah supervisor dengan supervisor/3
dan bukan worker/3
. Jika anda membuat app dengan flag --sup
sebagian besarnya sudah dibuat:
defmodule FriendsApp.App do
use Application
def start(_type, _args) do
import Supervisor.Spec
children = [
supervisor(FriendsApp.Repo, [])
]
opts = [strategy: :one_for_one, name: FriendsApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Untuk info lebih lanjut tentang supervisor lihatlah pelajaran OTP Supervisors.
Konfigurasi
Untuk mengkonfigurasi Ecto kita perlu menambahkan sebuah bagian ke config/config.exs
kita. Di sini kita akan menspesifikasikan repositori, adapter, database, dan informasi terkait account:
config :example_app, FriendsApp.Repo,
adapter: Ecto.Adapters.Postgres,
database: "example_app",
username: "postgres",
password: "postgres",
hostname: "localhost"
Mix Task
Ecto menyertakan sejumlah task mix yang membantu untuk bekerja dengan database kita:
mix ecto.create # Membuat database untuk repo
mix ecto.drop # Menghapus database untuk repo
mix ecto.gen.migration # Membuat migrasi baru untuk repo
mix ecto.gen.repo # Membuat repo baru
mix ecto.migrate # Menjalankan migrasi pada repo
mix ecto.rollback # Menjalankan balik migrasi dari repo
Migrasi
Cara terbaik membuat migrasi adalah dengan task mix ecto.gen.migration <name>
. Jika anda sudah kenal ActiveRecord maka ini akan tampak familiar.
Mari mulai dengan melihat sebuah migrasi untuk tabel users:
defmodule FriendsApp.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add(:username, :string, unique: true)
add(:encrypted_password, :string, null: false)
add(:email, :string)
add(:confirmed, :boolean, default: false)
timestamps
end
create(unique_index(:users, [:username], name: :unique_usernames))
end
end
Secara default Ecto membuat sebuah primary key yang auto-increment bernama id
. Di sini kita menggunakan callback default change/0
tetapi Ecto juga mendukung up/0
dan down/0
jika anda perlu mengendalikan secara lebih rinci.
Sebagaimana yang anda mungkin sudah terka, menambahkan timestamps
ke migrasi anda akan membuat dan mengelola inserted_at
dan updated_at
.
Untuk menjalankan migrasi kita yang baru jalankanlah mix ecto.migrate
.
Untuk info lebih lanjut tentang migrasi silakan lihat di bagian Ecto.Migration dari dokumentasi.
Model
Sekarang setelah kita membuat migrasi kita dapat melanjutkan ke model. Model mendefinisikan schema kita, metode pembantu, dan changeset. Kita akan bahas changeset lebih jauh di bagian berikutnya.
Untuk sementara ini mari lihat seperti apa model dari migrasi kita:
defmodule FriendsApp.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field(:username, :string)
field(:encrypted_password, :string)
field(:email, :string)
field(:confirmed, :boolean, default: false)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
timestamps
end
@required_fields ~w(username encrypted_password email)
@optional_fields ~w()
def changeset(user, params \\ :empty) do
user
|> cast(params, @required_fields, @optional_fields)
|> unique_constraint(:username)
end
end
Schema yang kita definisikan dalam model kita merepresentasikan apa yang kita spesifikasikan di migrasi kita. Sebagai tambahan atas field-field database kita kita juga memasukkan dua virtual field. Virtual field tidak disimpan ke database tapi bisa jadi berguna untuk hal-hal seperti validasi. Kita akan lihat tentang virtual field di bagian Changesets.
Query
Sebelum kita bisa melakukan query pada repository kita kita perlu mengimpor API Query. Untuk saat ini kita hanya perlu mengimpor from/2
:
import Ecto.Query, only: [from: 2]
Dokumentasi resmi bisa ditemukan di Ecto.Query.
Dasar
Ecto menyediakan DSL Query yang sangat bagus yang memungkinkan kita mengekspresikan query dengan jelas. Untuk menemukan username dari semua akun yang sudah dikonfirmasikan kita dapat gunakan seperti ini:
alias FriendsApp.{Repo, User}
query =
from(
u in User,
where: u.confirmed == true,
select: u.username
)
Repo.all(query)
Selain all/2
, Repo menyediakan sejumlah callback termasuk one/2
, get/3
, insert/2
, dan delete/2
. Daftar lengkap callback bisa ditemukan di Ecto.Repo#callbacks.
Count
query =
from(
u in User,
where: u.confirmed == true,
select: count(u.id)
)
Group By
Untuk mengelompokkan user berdasar status konfirmasinya kita bisa masukkan opsi group_by
:
query =
from(
u in User,
group_by: u.confirmed,
select: [u.confirmed, count(u.id)]
)
Repo.all(query)
Order By
Mengurutkan user berdasarkan tanggal pembuatannya:
query =
from(
u in User,
order_by: u.inserted_at,
select: [u.username, u.inserted_at]
)
Repo.all(query)
Untuk mengurutkannya secara menurun (DESC
):
query =
from(
u in User,
order_by: [desc: u.inserted_at],
select: [u.username, u.inserted_at]
)
Join
Dengan asumsi kita punya profil yang terkait dengan user kita, mari dapatkan semua profil akun yang sudah terkonfirmasi:
query =
from(
p in Profile,
join: u in assoc(p, :user),
where: u.confirmed == true
)
Fragment
Terkadang, seperti saat kita butuh fungsi database yang khusus, API Query tidaklah cukup. Fungsi fragment/1
ada untuk tujuan ini:
query =
from(
u in User,
where: fragment("downcase(?)", u.username) == ^username,
select: u
)
Contoh tambahan query dapat ditemukan di deskripsi modul Ecto.Query.API.
Changeset
Dalam bagian sebelumnya kita pelajari cara mendapatkan data, tetapi bagaimana dengan menambahkan dan mengubahnya? Untuk itu kita perlu Changeset.
Changeset mengurus pemfilteran, validasi, dan menangani batasan ketika mengubah sebuah model.
Untuk contoh ini kita akan fokus pada changeset untuk membuat user. Untuk memulai kita perlu mengubah model kita:
defmodule FriendsApp.User do
use Ecto.Schema
import Ecto.Changeset
import Comeonin.Bcrypt, only: [hashpwsalt: 1]
schema "users" do
field(:username, :string)
field(:encrypted_password, :string)
field(:email, :string)
field(:confirmed, :boolean, default: false)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
timestamps
end
@required_fields ~w(username email password password_confirmation)
@optional_fields ~w()
def changeset(user, params \\ :empty) do
user
|> cast(params, @required_fields, @optional_fields)
|> validate_length(:password, min: 8)
|> validate_password_confirmation()
|> unique_constraint(:username, name: :email)
|> put_change(:encrypted_password, hashpwsalt(params[:password]))
end
defp validate_password_confirmation(changeset) do
case get_change(changeset, :password_confirmation) do
nil ->
password_incorrect_error(changeset)
confirmation ->
password = get_field(changeset, :password)
if confirmation == password, do: changeset, else: password_mismatch_error(changeset)
end
end
defp password_mismatch_error(changeset) do
add_error(changeset, :password_confirmation, "Password tidak cocok")
end
defp password_incorrect_error(changeset) do
add_error(changeset, :password, "tidak valid")
end
end
Kita sudah mengubah fungsi changeset/2
kita dan menambahkan tiga fungsi penolong baru: validate_password_confirmation/1
, password_mismatch_error/1
, dan password_incorrect_error/1
.
Sebagaimana diduga, changeset/2
membuat sebuah changeset baru untuk kita. Di dalamnya kita menggunakan cast/4
untuk mengubah parameter kita ke sebuah changeset dari serangkaian field yang dibutuhkan (required) dan yang opsional. Lelau kita memvalidasi panjang password changeset tersebut, kita gunakan fungsi kita sendiri untuk memvalidasi kecocokan konfirmasi password, dan kita memvalidasi keunikan username. Akhirnya kita mengubah field database password. Untuk ini kita gunakan put_change/3
untuk mengubah sebuah value dalam changeset tersebut.
Menggunakan User.changeset/2
adalah relatif sederhana:
alias FriendsApp.{User, Repo}
pw = "passwords should be hard"
changeset =
User.changeset(%User{}, %{
username: "doomspork",
email: "sean@seancallan.com",
password: pw,
password_confirmation: pw
})
case Repo.insert(changeset) do
{:ok, model} -> # Inserted with success
{:error, changeset} -> # Something went wrong
end
Beres! Sekarang anda sudah siap menyimpan data.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!