() translation by (you can also view the original English article)
Dalam salah satu artikel saya sebelumnya saya menulis tentang tabel Erlang Term Stroge (atau hanya ETS), yang memungkinkan data acak tupel disimpan di memori. Kami juga membahas disk berbasis ETS (DETS), yang memberikan fungsionalitas sedikit lebih teratas, namun memungkinkan anda menyimpan konten anda ke file.
Terkadang, Anda mungkin memerlukan solusi yang lebih kuat untuk menyimpan data. Temui Mnesia - sistem manajemen basis data terdistribusi real-time yang awalnya diperkenalkan di Erlang. Mnesia memiliki model data relasional / objek hibrida dan memiliki banyak fitur bagus, termasuk replikasi dan pencarian data yang cepat.
Pada artikel ini, Anda akan belajar:
- Cara membuat skema Mnesia dan memulai keseluruhan sistem.
- Jenis tabel apa saja yang tersedia dan cara membuatnya.
- Bagaimana melakukan operasi CRUD dan apa bedanya antara fungsi "dirty" dan "transactional".
- Bagaimana memodifikasi tabel dan menambahkan indeks sekunder.
- Cara menggunakan paket Amnesia untuk mempermudah bekeria dengan database dan tabel.
Ayo kita mulai, haruskah kita?
Pengantar Mnesia
Jadi, seperti yang sudah djelaskankan di atas, Mnesia adalah objek dan model data relasional yang sangat baik. Ini memiliki bahasa query DMBS dan mendukung transaksi atom, sama seperti solusi populer lainnya (Postgres atau MySQL, misalnya). Tabel Mnesia dapat disimpan di disk dan memori, namun program dapat ditulis tanpa mengetahui lokasi data sebenarnya. Selain itu, Anda bisa meniru data Anda di beberapa node. Perhatikan juga bahwa Mnesia berjalan dengan contoh BEAM yang sama seperti kode lainnya.
Karena Mnesia adalalah modul Erlang, Anda harus mengaksesnya menggunakan atom:
1 |
:mnesia |
Meski dimungkinkan untuk membuat contoh seperti ini:
1 |
alias :mnesia, as: Mnesia |
Data di Mnesia disusun ke dalam tabel yang memiliki nama sendiri diwakili sebagai atom (yang sangat mirip dengan ETS). Tabel dapat memiliki salah satu dari jenis berikut:
-
:set
-the default type. Anda tidak dapat memiliki beberapa baris dengan primary key yang persis sama. (sebentar lagi kita akan tau bagaimana mendifinisikan primary key). Baris tidak diperintahkan dengan cara tertentu. -
:ordered_set
-sama seperti:set
, namun datanya diurutkan menurut primary key. Nantinya kita akan melihat bahwa beberapa read operations akan berbeda dengan tabel:ordered-set
. -
:bag
- beberapa baris mungkin memiliki kunci yang sama, namun barisnya tetap tidak dapat sepenuhnya identik.
Tabel memiliki properti lain yang mungkin ditemukan di dokumen resmi (kami akan membahas beberapa di antaranya di bagian selanjutnya). Namun, sebelum mulai membuat tabel, kita membutuhkan sebuah skema, jadi mari kita lanjutkan ke bagian selanjutnya dan tambahkan satu.
Membuat Skema dan Tabel
Untuk membuat skema baru, kita akan menggunakan metode dengan nama yang cukup mengejutkan: create-schema / 1 . Pada dasarnya, ini akan membuat database baru untuk kita di disk. Menjadi sebuah kesimpulan sebagai argumen:
1 |
:mnesia.create_schema([node()]) |
Sebuah node adalah Erlang VM yang menangani komunikasi, memori, dan hal-hal lainnya. Nodes dapat terhubung satu sama lain dan tidak terbatas pada satu PC-Anda juga bisa terhubung ke nodes lain lewat internet.
Setelah Anda menjalankan kode di atas, sebuah direktori baru bernama Mnesia.nonode@nohost akan dibuat yang akan berisi database Anda. nonode@nohost adalah nama nodes di sini. Sebelum kita bisa membuat tabel lainnya, sementara, Mnesia telah harus dimulai. ini sederhana tinggal memanggil start/0
function:
1 |
:mnesia.start() |
Mnesia harus dimulai pada semua node yang berpartisipasi, masing-masing biasanya memiliki folder tempat file akan ditulis (dalam kasus kami, folder ini dinamai Mnesia.nonode@nohost). Semua nodes yang menyusun sistem Mnesia ditulis ke skema, kemudian Anda dapat menambahkan atau menghapus nodes individu. Apalagi saat memulai informasi skema nodes exchange untuk memastikan semuanya baik-baik saja.
Jika Mnesia mulai berhasil, atom :ok
akan dikembalikan sebagai hasilnya. Anda kemudian bisa menghentikan sistem dengan memanggil stop/0
:
1 |
:mnesia.stop() # => :stopped |
Sekarang kita bisa membuat tabel baru. Paling tidak, kita harus memberi nama dan daftar atribut untuk catatan (menganggapnya sebagai kolom):
1 |
:mnesia.create_table(:user, [attributes: [:id, :name, :surname]]) |
2 |
# => {:atomic, :ok} |
Jika sistem tidak berjalan, tabel tidak akan dibuat dan kesalahan {:aborted, {:node_not_running, :nonode@nohost}}
akan dikembalikan. Juga, jika tabel sudah ada, Anda akan mendapatkan error {(aborted, {: already_exists,: user}}
.
Daftar tabel baru kita:user
, dan memiliki 3 atribut: :id
, :name
, dan :surname
. Perhatikan bahwa atribut pertama dalam daftar selalu digunakan sebagai primary key, dan kita dapat menggunakannya untuk mencari record dengan cepat. Nanti di artikelnya, kita akan melihat bagaimana menulis queries kompleks dan menambahkan secondary indexes.
Ingat juga bahwa tipe default untuk tabel adalah :set
, tapi ini mungkin bisa diubah dengan mudah:
1 |
:mnesia.create_table(:user, [ |
2 |
attributes: [:id, :name, :surname], |
3 |
type: :bag |
4 |
])
|
Anda bahkan bisa membuat tabel Anda hanya bisa dibaca dengan mengaturnya :accsess_mode
menjadi :read_only:
1 |
:mnesia.create_table(:user, [ |
2 |
attributes: [:id, :name, :surname], |
3 |
type: :bag, |
4 |
access_mode: read_only |
5 |
])
|
Setelah skema dan tabel dibuat, direktori akan memiliki file schema.DAT serta beberapa file .log. Mari kita lanjutkan ke bagian selanjutnya dan masukkan beberapa data ke tabel baru kita!
Penulisan Operasi
Untuk memasukkan data kedalam tabel, Anda butuh memanfaatkan sebuah fungsi write/1
. Contohnya mari masukkan nama user baru Jhon Doe:
1 |
:mnesia.write({:user, 1, "John", "Doe"}) |
Perhatikan bahwa kami telah menentukan nama tabel dan semua atribut pengguna untuk disimpan. Coba jalankan kode ... dan gagal total dengan error {:aborted:: no_transaction}
. Mengapa ini terjadi? Maka, ini dikarenakan write/1
harus dilaksanakan dalam sebuah transaksi. Jika, untuk beberapa alasan, Anda tidak ingin melakukan transaction, operasi tulis dapat dilakukan dengan cara "kotor" dengan menggunakan dirty_write/1
:
1 |
:mnesia.dirty_write({:user, 1, "John", "Doe"}) # => :ok |
Pendekatan ini biasanya tidak disarankan, jadi mari kita bangun transaksi sederhana dengan bantuan fungsi transaction
:
1 |
:mnesia.transaction(fn -> |
2 |
:mnesia.write({:user, 1, "John", "Doe"}) |
3 |
end) # => {:atomic, :ok} |
transaction
menerima fungsi anonim yang memiliki satu atau lebih operasi yang dikelompokkan. Perhatikan bahwa dalam kasus ini hasilnya adalah {:atomic: ok}
, tidak hanya :ok
seperti halnya dengan fungsi dirty_write
. Manfaat utama di sini adalah jika terjadi kesalahan selama bertransaksi, semua operasi digulirkan kembali.
Sebenarnya, itu adalah prinsip atomicity, yang mengatakan bahwa semua operasi harus terjadi atau tidak ada operasi yang harus terjadi jika terjadi kesalahan. Misalkan, misalnya, Anda membayar gaji karyawan Anda, dan tiba-tiba ada yang tidak beres. Operasi berhenti, dan Anda pasti tidak ingin berakhir dalam situasi ketika beberapa karyawan mendapatkan gaji mereka dan beberapa lainnya tidak. Saat itulah transaksi atom sangat berguna.
Fungsi transaction
memerlukan sebanyak mungkin operasi penulisan yang diperlukan:
1 |
write_data = fn -> |
2 |
:mnesia.write({:user, 2, "Kate", "Brown"}) |
3 |
:mnesia.write({:user, 3, "Will", "Smith"}) |
4 |
end
|
5 |
|
6 |
:mnesia.transaction(write_data) # => {:atomic, :ok} |
Menariknya, data bisa diperbaharui dengan menggunakan fungsi write
juga. Berikan saja kunci dan nilai baru yang sama untuk atribut lainnya:
1 |
update_data = fn -> |
2 |
:mnesia.write({:user, 2, "Kate", "Smith"}) |
3 |
:mnesia.write({:user, 3, "Will", "Brown"}) |
4 |
end
|
5 |
|
6 |
:mnesia.transaction(update_data) |
Catatan, bagaimanapun juga, hal ini tak akan bekerja untuk tabel tipe :bag
. Karena tabel tersebut mengizinkan beberapa catatan untuk memiliki tombol yang sama, Anda akan hanya berakhir dengan dua catatan: [{:user, 2, "Kate", "Brown"}, {: pengguna, 2, "Kate", "Smith"}]
. Namun, :bag
tabel tidak mengizinkan sepenuhnya identik catatan ada.
Membaca options
Baiklah, sekarang kita memiliki data didalam tabel kita, kenapa tak kita coba membacanya? Sama seperti penulisan operasional, kalian dapat melakukan pembacaan dengan cara "kotor" atau "transactional". "Cara kotor" lebih sederhana tentu saja (tapi itulah sisi gelap Force, Luke!):
1 |
:mnesia.dirty_read({:user, 2}) # => [{:user, 2, "Kate", "Smith"}] |
Jadi dirty_read
mengembalikan daftar catatan yang ditemukan berdasarkan kunci yang diberikan. Jika tabel adalah :set
atau :ordered_set
, daftar hanya memiliki satu elemen. Untuk tabel :bag, tentu saja datanya hanya memiliki banyak elemen Jika tidak ada catatan yang ditemukan, daftar itu akan kosong.
Sekarang mari kita coba melakukan operasi yang sama tapi menggunakan pendekatan transaksional:
1 |
read_data = fn -> |
2 |
:mnesia.read({:user, 2}) |
3 |
end
|
4 |
|
5 |
:mnesia.transaction(read_data) => {:atomic, [{:user, 2, "Kate", "Brown"}]} |
Hebat!
Apakah ada fungsi berguna lainnya untuk membaca data? Tentu saja! Misalnya, kalian dapat mengambil catatan pertama atau terakhir dari tabel:
1 |
:mnesia.dirty_first(:user) # => 2 |
2 |
:mnesia.dirty_last(:user) # => 2 |
Keduanya dirty_first
dan dirty_last
memiliki rekan transactional mereka, yaitu first
dan last
, yang harus dirangkum dalam transaction. Semua fungsi ini mengembalikan hasil kunci, namun perhatikan bahwa dalam kedua kasus tersebut kita mendapatkan 2
sebagai hasilnya walaupun kita memiliki dua catatan dengan tombol 2
dan 3
. Mengapa ini terjadi?
Tampaknya untuk tabel :set
dan :bag
, fungsi dirty_first
dan dirty_last
(termasuk yang first
dan yang last
) adalah sinonim karena datanya tidak diurutkan sesuai urutan tertentu. Meskipun jika, Anda memilik tabel :ordered_set
, catatan akan diurutkan berdasarkan kunci mereka, dan hasilnya adalah:
1 |
:mnesia.dirty_first(:user) # => 2 |
2 |
:mnesia.dirty_last(:user) # => 3 |
Hal ini juga memungkinkan untuk meraih tombol berikutnya atau sebelumnya dengan menggunakan dirty_next
dan dirty_prev
(atau next
dan prev
):
1 |
:mnesia.dirty_next(:user, 2) => 3 |
2 |
:mnesia.dirty_next(:user, 3) => :"$end_of_table" |
Jika tidak ada catatan lagi, sebuah atom khusus :$ end_of_table"
dikembalikan. Juga termasuk, jika tabelnya adalah :set
atau :bag
, dirty_next
dan dirty_prev
adalah sinonim.
Terakhir, kalian mungkin akan mendapatkan seluruh kunci dari penggunaan tabel dirty_all_keys/1
atau all_keys/1
:
1 |
:mnesia.dirty_all_keys(:user) # => [3, 2] |
Penghapusan Operasional
Cara untuk menghapus data yang tercatat dalam tabel, menggunakan dirty_delete
atau delete
:
1 |
:mnesia.dirty_delete({:user, 2}) # => :ok |
Kode ini akan menghapus seluruh catatan yang telah diberikan kunci.
Serupa, kalian bisa menghapus seluruh tabel:
1 |
:mnesia.delete_table(:user) |
Tidak ada rekan "dirty" untuk method ini. Jelas, setelah tabel dihapus, Anda tidak dapat menulis apapun lagi, dan error {:aborted, {: no_exists,: user}}
akan dikembalikan.
Terakhir, jika kalian malas untuk menghapus, keseluruhan skema dapat dihapus dengan menggunakan delete_schema/1
:
1 |
:mnesia.delete_schema([node()]) |
Operasi ini akan mengembalikan error {:error, {'Mnesia is not stopped everywhere', [:nonode@nohost]}}
jika Mnesia tidak dihentikan, jadi jangan lupa untuk melakukannya:
1 |
:mnesia.stop() |
2 |
:mnesia.delete_schema([node()]) |
Membaca Pengoperasian Lebih Kompleks
Sekarang setelah kita melihat dasar-dasar kerja dengan Mnesia, mari kita menelusuri sedikit lebih dalam dan melihat bagaimana cara menulis pertanyaan lanjutan. Pertama, ada fungsi match_object
dan dirty_match_object
yang dapat digunakan untuk mencari catatan berdasarkan salah satu atribut yang disediakan:
1 |
:mnesia.dirty_match_object({:user, :_, "Kate", "Brown"}) |
2 |
# => [{:user, 2, "Kate", "Brown"}] |
Atribut yang tidak kalian pedulikan ditandai dengan :_
atom. Kalian mungkin hanya menetapkan surname, misalnya
1 |
:mnesia.dirty_match_object({:user, :_, :_, "Brown"}) |
2 |
# => [{:user, 2, "Kate", "Brown"}] |
Anda juga dapat memberikan kriteria pencarian khusus menggunakan select dan dirty_select. Untuk melihat langkahnya, pertama-tama kita harus mengisi tabel dengan nilai berikut:
1 |
write_data = fn -> |
2 |
:mnesia.write({:user, 2, "Kate", "Brown"}) |
3 |
:mnesia.write({:user, 3, "Will", "Smith"}) |
4 |
:mnesia.write({:user, 4, "Will", "Smoth"}) |
5 |
:mnesia.write({:user, 5, "Will", "Smath"}) |
6 |
end
|
7 |
|
8 |
:mnesia.transaction(write_data) |
Sekarang yang ingin saya lakukan adalah menemukan semua catatan yang memiliki Will
sebagai nama dan kuncinya kurang dari 5
, yang berarti daftar yang dihasilkan hanya berisi "Will Smith" dan "Will Smoth". Berikut adalah kode yang sesuai:
1 |
:mnesia.dirty_select( |
2 |
:user, |
3 |
[{
|
4 |
{:user, :"$1", :"$2", :"$3"}, |
5 |
[
|
6 |
{:<, :"$1", 5}, |
7 |
{:==, :"$2", "Will"} |
8 |
],
|
9 |
[:"$$"] |
10 |
}]
|
11 |
) # => [[3, "Will", "Smith"], [4, "Will", "Smoth"]] |
Hal-hal ini sedikit lebih rumit, jadi mari kita bahas selangkah demi selangkah.
- Pertama, kami memiliki bagian
{:user,: "$ 1",: "$ 2",: "$ 3"}
. Disini kami menyediakan nama tabel dan daftar parameter positional. Mereka harus ditulis dalam bentuk yang tampak aneh ini sehingga kita bisa memanfaatkannya nanti.$1
sesuai dengan:id
,$2
namanya
, dan$3
namasurname
. - Selanjutnya, ada daftar fungsi penjaga yang harus diterapkan pada parameter yang diberikan.
{:<,: "$ 1", 5}
berarti kami hanya ingin memilih catatan yang atributnya ditandai sebagai$1
(yaitu,:id
) kurang dari5
.{: ==,: "$ 2", " Will "}
, pada gilirannya, berarti kita memilih catatan dengan:name
ganti ke"Will"
. - Terakhir,
[:"$$"]
berarti kami ingin menyertakan semua bidang pada hasilnya. Kalian mungkin mengatakan[:"$ 2"]
hanya menampilkan namanya. Sebagai catatan, hasilnya berisi daftar daftar:[[3, "Will", "Smith"], [4, "Will", "Smoth"]]
.
Anda mungkin juga menandai beberapa atribut sebagai atribut yang tidak Anda minati dengan menggunakan :_
atom. Contohnya, mari kita abaikan surname:
1 |
:mnesia.dirty_select( |
2 |
:user, |
3 |
[{
|
4 |
{:user, :"$1", :"$2", :_}, |
5 |
[
|
6 |
{:<, :"$1", 5}, |
7 |
{:==, :"$2", "Will"} |
8 |
],
|
9 |
[:"$$"] |
10 |
}]
|
11 |
) # => [[3, "Will"], [4, "Will"]] |
Pada kasus ini bagaimanapun surname tak akan tercantum dalam hasil.
Memodifikasi Tabel
Melakukan Transformasi
Misalkan sekarang kita ingin memodifikasi tabel kita dengan menambahkan field baru. Hal ini dapat dilakukan dengan menggunakan fungsi transform_table
, yang menerima nama tabel, sebuah fungsi untuk diterapkan pada semua record, dan daftar atribut baru:
1 |
:mnesia.transform_table( |
2 |
:user, |
3 |
fn ({:user, id, name, surname}) -> |
4 |
{:user, id, name, surname, :rand.uniform(1000)} |
5 |
end, |
6 |
[:id, :name, :surname, :salary] |
7 |
)
|
Dalam contoh ini kita menambahkan atribut baru bernama :salary
(ini diberikan dalam argumen terakhir). Sedangkan untuk fungsi transform (argumen kedua), kita setting atribut baru ini menjadi nilai acak. Anda juga dapat memodifikasi atribut lain di dalam fungsi transform ini. Proses perubahan data ini dikenal sebagai "migration", dan konsep ini harus familiar bagi developers yang berasal dari dunia Rails
Sekarang Anda hanya bisa mengambil informasi tentang atribut tabel dengan menggunakan table_info
:
1 |
:mnesia.table_info(:user, :attributes) # => [:id, :name, :surname, :salary] |
:salary
atribut ada disana! Dan, tentu saja, data Anda juga ada di tempat:
1 |
:mnesia.dirty_read({:user, 2}) # => [{:user, 2, "Kate", "Brown", 778}] |
Anda dapat menemukan contoh yang sedikit lebih rumit untuk menggunakan fungsi create_table
dan transform_table
di situs web ElixirSchool.
Memasukkan Indeks
Mnesia memungkinkan Anda membuat atribut yang diindeks dengan menggunakan fungsi add_table_index
. Sebagai contoh, mari kita membuat atribut :surname
diindeks:
1 |
:mnesia.add_table_index(:user, :surname) # => {:atomic, :ok} |
Jika indeksnya telah muncul, kalian akan mendapati {:aborted, {:already_exists, :user, 4}}
.
Karena dokumentasi untuk fungsi ini menyatakan, indeks tidak tersedia secara gratis. Secara khusus, mereka menempati ruang tambahan (sebanding dengan ukuran tabel) dan membuat operasi insert sedikit lebih lambat. Di sisi lain, mereka memungkinkan Anda untuk mencari data lebih cepat, jadi itu trade-off yang adil
Anda dapat mencari berdasarkan bidang yang diindeks dengan menggunakan fungsi dirty_index_read
atau index_read
:
1 |
:mnesia.dirty_index_read(:user, "Smith", :surname) |
2 |
# => [{:user, 3, "Will", "Smith"}] |
Di sini kita menggunakan indeks sekunder :surname
untuk mencari pengguna.
Penggunaan Amnesia
Mungkin agak membosankan untuk mengerjakan modul Mmnesia secara langsung, tapi untungnya ada paket pihak ketiga bernama Amnesia (duh!) Yang memungkinkan Anda melakukan operasi sepele dengan lebih mudah.
Misalnya, Anda dapat menentukan database dan tabel Anda seperti ini:
1 |
use Amnesia |
2 |
|
3 |
defdatabase Demo do |
4 |
deftable User, [{ :id, autoincrement }, :name, :surname, :email], index: [:email] do |
5 |
end
|
6 |
end
|
Ini akan menentukan database yang disebut Demo
dengan User
tabel. Pengguna akan memberi nama nama, surname, e-mail (bidang yang diindeks), dan sebuah id (primary key diatur ke autoincrement).
Selanjutnya, Anda dapat dengan mudah membuat skema menggunakan tugas mix terintegrasi:
1 |
mix amnesia.create -d Demo --disk |
Dalam kasus ini, database akan berbasis disk, namun ada beberapa opsi lain yang tersedia yang mungkin Anda tetapkan. Juga ada tugas drop yang akan, jelas, destroy database dan semua data:
1 |
mix amnesia.drop -d Demo
|
Mungkin juga untuk menghancurkan database dan skema:
1 |
mix amnesia.drop -d Demo --schema |
Memiliki database dan skema di tempat, adalah mungkin untuk melakukan berbagai operasi terhadap table. Misalnya membuat catatan baru:
1 |
Amnesia.transaction do |
2 |
will_smith = %User{name: "Will", surname: "Smith", email: "will@smith.com"} |> User.write |
3 |
end |
Atau dapatkan pengguna dengan id:
1 |
Amnesia.transaction do |
2 |
will_smith = User.read(1) |
3 |
end
|
Selain itu, Anda dapat menentukan tabel Message
sambil membuat relasi ke tabel User
dengan user_id
sebagai foreign key:
1 |
deftable Message, [:user_id, :content] do |
2 |
end
|
Tabel mungkin memiliki banyak fungsi helper di dalam, misalnya untuk membuat pesan atau mendapatkan semua pesan:
1 |
deftable User, [{ :id, autoincrement }, :name, :surname, :email], index: [:email] do |
2 |
def add_message(self, content) do |
3 |
%Message{user_id: self.id, content: content} |> Message.write
|
4 |
end
|
5 |
|
6 |
def messages(self) do |
7 |
Message.read(self.id) |
8 |
end
|
9 |
end
|
Anda sekarang dapat menemukan pengguna, membuat pesan untuk mereka, atau membuat daftar semua pesan mereka dengan mudah:
1 |
Amnesia.transaction do |
2 |
will_smith = User.read(1) |
3 |
|
4 |
will_smith |> User.add_message "hi!" |
5 |
|
6 |
will_smith |> User.messages |
7 |
end
|
Cukup mudah bukan? Beberapa contoh penggunaan lainnya dapat ditemukan di situs resmi Amnesia
Kesimpulan
Pada artikel ini, kami membahas tentang sistem manajenen database Mnesia yang tersedia untuk Erlang dan Elixir . Kami telah membahas konsep utama DBMS ini dan telah melihat bagaimana membuat skema, database, dan tabel, serta melakukan semua operasi besar: create, read, update, dan destroy. Selain itu, Anda telah belajar bagaimana bekerja dengan indeks, bagaimana mengubah tabel, dan bagaimana menggunakan paket Amnesia untuk mempermudah bekerja dengan database.
Saya sangat berharap, artikel ini bermanfaat dan Anda juga berkeinginan untuk mencoba tindakan Mnesia. Seperti biasa, saya ucapkan terima kasih karna Anda telah bersamaku, dan sampai jumpa!