Kueri dalam Rails, Bagian 2
() translation by (you can also view the original English article)
Di artikel kedua ini, kami akan sedikit lebih dalam ke pertanyaan Rekaman Aktif di Rails. Jika Anda masih baru dalam SQL, saya akan menambahkan contoh yang cukup sederhana sehingga Anda dapat ikut serta dan mengambil sintaks sedikit saat kita melanjutkan.
Yang sedang berkata, itu pasti akan membantu jika Anda menjalankan tutorial SQL cepat sebelum Anda kembali untuk terus membaca. Kalau tidak, luangkan waktu Anda untuk memahami pertanyaan SQL yang kami gunakan, dan saya berharap bahwa pada akhir seri ini ia tidak akan merasa mengintimidasi lagi.
Sebagian besar benar-benar mudah, tetapi sintaksnya agak aneh jika Anda baru saja mulai dengan pengkodean—terutama di Ruby. Bertahanlah, itu bukan ilmu roket!
Topik
- Includes & Eager Loading
- Joining Tables
- Eager Loading
- Scopes
- Aggregations
- Dynamic Finders
- Specific Fields
- Custom SQL
Includes & Eager Loading
Kueri ini mencakup lebih dari satu tabel basis data untuk dikerjakan dan mungkin yang paling penting untuk diambil dari artikel ini. Itu bermuara pada ini: alih-alih melakukan beberapa pertanyaan untuk informasi yang tersebar di beberapa tabel, includes
mencoba untuk menjaga ini ke minimum. Konsep kunci di balik ini disebut "eager loading" dan berarti bahwa kita memuat objek terkait ketika kita menemukan.
Jika kami melakukannya dengan mengulangi kumpulan objek dan kemudian mencoba mengakses catatan terkait dari tabel lain, kami akan mengalami masalah yang disebut "masalah permintaan N + 1". Misalnya, untuk masing-masing agent.handler
dalam koleksi agen, kami akan memecat pertanyaan yang terpisah untuk agen dan penangannya. Itulah yang perlu kita hindari karena ini tidak berskala sama sekali. Sebagai gantinya, kami melakukan hal berikut:
Rails
1 |
agents = Agent.includes(:handlers) |
Jika sekarang kita beralih ke sekumpulan agen seperti itu—dengan diskon bahwa kita belum membatasi jumlah catatan yang dikembalikan untuk saat ini—kita akan berakhir dengan dua pertanyaan bukannya mungkin satu trilyun.
SQL
1 |
SELECT "agents".* FROM "agents" |
2 |
SELECT "handlers".* FROM "handlers" WHERE "handlers"."id" IN (1, 2) |
Agen yang satu ini dalam daftar memiliki dua penangan, dan ketika kami sekarang meminta objek agen untuk penangannya, tidak ada permintaan basis data tambahan yang perlu dipecat. Kita bisa mengambil langkah ini lebih jauh, tentu saja, dan ingin memuat beberapa catatan tabel terkait. Jika kami perlu memuat tidak hanya penangan tetapi juga misi terkait agen untuk alasan apa pun, kami dapat menggunakan includes
seperti ini.
Rails
1 |
agents = Agent.includes(:handlers, :mission) |
Sederhana! Hanya berhati-hatilah menggunakan versi tunggal dan jamak untuk menyertakan. Mereka bergantung pada asosiasi model Anda. Asosiasi has_many
menggunakan bentuk jamak, sedangkan belongs_to
atau has_one
membutuhkan versi tunggal, tentu saja. Jika Anda perlu, Anda juga bisa memasukkan klausa where
untuk menentukan kondisi tambahan, tetapi cara yang lebih disukai menentukan kondisi untuk tabel terkait yang ingin dimuat adalah dengan menggunakan gabungan joins
.
Satu hal yang perlu diingat tentang eager loading adalah bahwa data yang akan ditambahkan akan dikirim kembali sepenuhnya ke Rekaman Aktif—yang pada gilirannya membangun objek Ruby termasuk atribut-atribut ini. Ini berbeda dengan "hanya" bergabung dengan data, di mana Anda akan mendapatkan hasil virtual yang dapat Anda gunakan untuk perhitungan, misalnya, dan akan lebih sedikit menguras memori daripada memasukkan.
Joining Tables
Joining tables adalah alat lain yang memungkinkan Anda menghindari mengirimkan terlalu banyak permintaan yang tidak perlu ke dalam pipa. Skenario umum adalah menggabungkan dua tabel dengan satu kueri yang mengembalikan semacam catatan gabungan. joins
hanyalah metode finder Rekaman Aktif yang memungkinkan Anda—dalam istilah SQL—JOIN
tabel. Kueri ini dapat mengembalikan catatan yang digabungkan dari beberapa tabel, dan Anda mendapatkan tabel virtual yang menggabungkan catatan dari tabel ini. Ini cukup tepat ketika Anda membandingkannya dengan mengirim semua jenis kueri untuk setiap tabel. Ada beberapa jenis data yang tumpang tindih yang bisa Anda dapatkan dengan pendekatan ini.
Gabung dalam adalah modus operandi default untuk joins
. Ini cocok dengan semua hasil yang cocok dengan id tertentu dan perwakilannya sebagai kunci asing dari objek atau tabel lain. Dalam contoh di bawah ini, sederhananya: beri saya semua misi tempat id
misi muncul sebagai mission_id
di tabel agen. "agents"."mission_id" = "missions"."id"
. Batin bergabung dengan mengecualikan hubungan yang tidak ada.
Rails
1 |
Mission.joins(:agents) |
SQL
1 |
SELECT "missions".* FROM "missions" INNER JOIN "agents" ON "agents"."mission_id" = "mission"."id" |
Jadi kami mencocokkan misi dan agen yang menyertainya—dalam satu permintaan! Tentu, kami bisa mendapatkan misi pertama, beralih satu per satu, dan meminta agen mereka. Tapi kemudian kita akan kembali ke "N + 1 masalah permintaan" yang mengerikan. Tidak terima kasih!
Yang juga bagus tentang pendekatan ini adalah bahwa kami tidak akan mendapatkan kasus nol dengan gabungan batin; kami hanya mendapatkan catatan yang dikembalikan yang cocok dengan id mereka ke kunci asing di tabel terkait. Jika kita perlu menemukan misi, misalnya, yang tidak memiliki agen, kita akan membutuhkan penggabung luar. Karena saat ini melibatkan penulisan OUTER JOIN
SQL Anda sendiri, kami akan membahasnya di artikel terakhir. Kembali ke penggabungan standar, Anda juga dapat bergabung dengan beberapa tabel terkait.
Rails
1 |
Mission.joins(:agents, :expenses, :handlers) |
Dan Anda dapat menambahkan beberapa klausa where
ini untuk menentukan pencari Anda lebih banyak lagi. Di bawah ini, kami hanya mencari misi yang dieksekusi oleh James Bond dan hanya agen yang termasuk misi 'Moonraker' dalam contoh kedua.
1 |
Mission.joins(:agents).where( agents: { name: 'James Bond' }) |
SQL
1 |
SELECT "missions".* FROM "missions" INNER JOIN "agents" ON "agents"."mission_id" = "missions"."id" WHERE "agents"."name" = ? [["name", "James Bond"]] |
Rails
1 |
Agent.joins(:mission).where( missions: { mission_name: 'Moonraker' }) |
SQL
1 |
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."mission_name" = ? [["mission_name", "Moonraker"]] |
Dengan joins
, Anda juga harus memperhatikan penggunaan tunggal dan jamak asosiasi model Anda. Karena kelas Mission
saya has_many :agents
, kita dapat menggunakan jamak. Di sisi lain, untuk kelas Agent
adalah belongs_to :mission
, hanya versi tunggal yang bekerja tanpa meledak. Detail kecil yang penting: bagian where
yang lebih sederhana. Karena Anda memindai beberapa baris dalam tabel yang memenuhi kondisi tertentu, bentuk jamak selalu masuk akal.
Scopes
Lingkup adalah cara praktis untuk mengekstraksi kebutuhan kueri umum ke dalam metode bernama Anda sendiri. Dengan begitu mereka sedikit lebih mudah untuk diedarkan dan juga mungkin lebih mudah untuk dipahami jika orang lain harus bekerja dengan kode Anda atau jika Anda perlu mengunjungi kembali pertanyaan tertentu di masa depan. Anda dapat mendefinisikannya untuk model tunggal tetapi menggunakannya juga untuk asosiasi mereka.
Langit adalah batas sesungguhnya — joins
, includes
, dan where
semua permainan adil! Karena cakupan juga mengembalikan objek ActiveRecord::Relations
, Anda dapat rantai mereka dan memanggil lingkup lain di atasnya tanpa ragu-ragu. Mengekstraksi cakupan seperti itu dan merantai mereka untuk pertanyaan yang lebih kompleks sangat berguna dan membuat yang lebih lama semakin mudah dibaca. Lingkup didefinisikan melalui sintaks "stabby lambda":
Rails
1 |
class Mission < ActiveRecord::Base |
2 |
has_many: agents |
3 |
|
4 |
scope :successful, -> { where(mission_complete: true) } |
5 |
end |
6 |
|
7 |
Mission.successful |
1 |
class Agent < ActiveRecord::Base |
2 |
|
3 |
belongs_to :mission |
4 |
|
5 |
scope :licenced_to_kill, -> { where(licence_to_kill: true) } |
6 |
scope :womanizer, -> { where(womanizer: true) } |
7 |
scope :gambler, -> { where(gambler: true) } |
8 |
end |
9 |
|
10 |
# Agent.gambler |
11 |
# Agent.womanizer |
12 |
# Agent.licenced_to_kill |
13 |
# Agent.womanizer.gambler |
14 |
|
15 |
Agent.licenced_to_kill.womanizer.gambler |
SQL
1 |
SELECT "agents".* FROM "agents" WHERE "agents"."licence_to_kill" = ? AND "agents"."womanizer" = ? AND "agents"."gambler" = ? [["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]] |
Seperti yang dapat Anda lihat dari contoh di atas, menemukan James Bond jauh lebih baik ketika Anda hanya bisa meringkas cakupan. Dengan begitu Anda dapat mencampur dan mencocokkan berbagai pertanyaan dan tetap DRY pada saat yang sama. Jika Anda membutuhkan ruang lingkup melalui asosiasi, mereka juga siap membantu Anda:
1 |
Mission.last.agents.licenced_to_kill.womanizer.gambler |
1 |
SELECT "missions".* FROM "missions" ORDER BY "missions"."id" DESC LIMIT 1 |
2 |
SELECT "agents".* FROM "agents" WHERE "agents"."mission_id" = ? AND "agents"."licence_to_kill" = ? AND "agents"."womanizer" = ? AND "agents"."gambler" = ? [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]] |
Anda juga dapat mendefinisikan kembali default_scope
ketika Anda melihat sesuatu seperti Mission.all
.
1 |
class Mission < ActiveRecord::Base |
2 |
default_scope { where status: "In progress" } |
3 |
end
|
4 |
|
5 |
Mission.all
|
SQL
1 |
SELECT "missions".* FROM "missions" WHERE "missions"."status" = ? [["status", "In progress"]] |
Aggregations
Bagian ini tidak terlalu maju dalam hal pemahaman yang terlibat, tetapi Anda akan membutuhkannya lebih sering daripada tidak dalam skenario yang dapat dianggap sedikit lebih maju daripada rata-rata pencari Anda—seperti .all
, .first
, .find_by_id
atau apa pun. Pemfilteran berdasarkan perhitungan dasar, misalnya, kemungkinan besar adalah sesuatu yang pemula tidak dapat langsung hubungi. Apa yang sebenarnya kita lihat di sini?
sum
count
minimum
maximum
average
Mudah, kan? Yang keren adalah bahwa alih-alih mengulangi kumpulan objek yang dikembalikan untuk melakukan perhitungan ini, kita dapat membiarkan Rekaman Aktif melakukan semua pekerjaan ini untuk kita dan mengembalikan hasil ini dengan kueri—dalam satu permintaan lebih disukai. Bagus ya
count
Rails
1 |
Mission.count |
2 |
|
3 |
# => 24 |
SQL
1 |
SELECT COUNT(*) FROM "missions" |
average
Rails
1 |
Agent.average(:number_of_gadgets).to_f |
2 |
|
3 |
# => 3.5 |
SQL
1 |
SELECT AVG("agents"."number_of_gadgets") FROM "agents" |
Karena kita sekarang tahu bagaimana kita dapat menggunakan joins
, kita dapat mengambil satu langkah lebih jauh dan hanya meminta rata-rata gadget yang dimiliki agen pada misi tertentu, misalnya.
Rails
1 |
Agent.joins(:mission).where(missions: {name: 'Moonraker'}).average(:number_of_gadgets).to_f |
2 |
|
3 |
# => 3.4 |
SQL
1 |
SELECT AVG("agents"."number_of_gadgets") FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."name" = ? [["name", "Moonraker"]] |
Mengelompokkan jumlah rata-rata gadget berdasarkan nama misi menjadi sepele pada saat itu. Lihat lebih lanjut tentang pengelompokan di bawah ini:
Rails
1 |
Agent.joins(:mission).group('missions.name').average(:number_of_gadgets) |
SQL
1 |
SELECT AVG("agents"."number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" GROUP BY missions.name |
sum
Rails
1 |
Agent.sum(:number_of_gadgets) |
2 |
|
3 |
Agent.where(licence_to_kill: true).sum(:number_of_gadgets) |
4 |
|
5 |
Agent.where.not(licence_to_kill: true).sum(:number_of_gadgets) |
SQL
1 |
SELECT SUM("agents"."number_of_gadgets") FROM "agents" |
2 |
|
3 |
SELECT SUM("agents"."number_of_gadgets") FROM "agents" WHERE "agents"."licence_to_kill" = ? [["licence_to_kill", "t"]] |
4 |
|
5 |
SELECT SUM("agents"."number_of_gadgets") FROM "agents" WHERE ("agents"."licence_to_kill" != ?) [["licence_to_kill", "t"]] |
maximum
Rails
1 |
Agent.maximum(:number_of_gadgets) |
2 |
|
3 |
Agent.where(licence_to_kill: true).maximum(:number_of_gadgets) |
SQL
1 |
SELECT MAX("agents"."number_of_gadgets") FROM "agents" |
2 |
|
3 |
SELECT MAX("agents"."number_of_gadgets") FROM "agents" WHERE "agents"."licence_to_kill" = ? [["licence_to_kill", "t"]] |
minimum
Rails
1 |
Agent.minimum(:iq) |
2 |
|
3 |
Agent.where(licence_to_kill: true).minimum(:iq) |
SQL
1 |
SELECT MIN("agents"."iq") FROM "agents" |
2 |
|
3 |
SELECT MIN("agents"."iq") FROM "agents" WHERE "agents"."licence_to_kill" = ? [["licence_to_kill", "t"]] |
Perhatian!
Semua metode agregasi ini tidak membiarkan Anda melakukan hal-hal lain—itu adalah terminal. Urutan penting untuk melakukan perhitungan. Kami tidak mendapatkan objek ActiveRecord::Relation
kembali dari operasi ini, yang membuat musik berhenti pada saat itu—kami malah mendapatkan hash atau angka. Contoh di bawah ini tidak akan berfungsi:
Rails
1 |
Agent.maximum(:number_of_gadgets).where(licence_to_kill: true) |
2 |
|
3 |
Agent.sum(:number_of_gadgets).where.not(licence_to_kill: true) |
4 |
|
5 |
Agent.joins(:mission).average(:number_of_gadgets).group('missions.name') |
Grouped
Jika Anda ingin perhitungan dipecah dan diurutkan ke dalam grup logis, Anda harus menggunakan klausa GROUP
dan tidak melakukan ini di Ruby. Yang saya maksud dengan itu adalah Anda harus menghindari iterasi di atas grup yang berpotensi menghasilkan banyak pertanyaan.
Rails
1 |
Agent.joins(:mission).group('missions.name').average(:number_of_gadgets) |
2 |
|
3 |
# => { "Moonraker"=> 4.4, "Octopussy"=> 4.9 } |
SQL
1 |
SELECT AVG("agents"."number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" GROUP BY missions.name |
Contoh ini menemukan semua agen yang dikelompokkan ke misi tertentu dan mengembalikan hash dengan jumlah rata-rata gadget yang dihitung sebagai nilainya—dalam satu permintaan! Ya! Hal yang sama berlaku untuk perhitungan lain juga, tentu saja. Dalam hal ini, sangat masuk akal untuk membiarkan SQL melakukan pekerjaannya. Jumlah kueri yang kami gunakan untuk agregasi ini terlalu penting.
Dynamic Finders
Untuk setiap atribut pada model Anda, ucapkan name
, email_address
, favorite_gadget
dan sebagainya, Rekaman Aktif memungkinkan Anda menggunakan metode pencari yang sangat mudah dibaca yang secara dinamis dibuat untuk Anda. Kedengarannya samar, saya tahu, tapi itu tidak berarti apa pun selain find_by_id
atau find_by_favorite_gadget
. Bagian find_by
adalah standar, dan Rekaman Aktif hanya menyelipkan nama atribut untuk Anda. Anda bahkan dapat menambahkan !
jika Anda ingin pencari itu meningkatkan kesalahan jika tidak ada yang dapat ditemukan. Bagian yang sakit adalah, Anda bahkan dapat menghubungkan metode pencari dinamis ini bersama-sama. Seperti ini:
Rails
1 |
Agent.find_by_name('James Bond') |
2 |
|
3 |
Agent.find_by_name_and_licence_to_kill('James Bond', true) |
SQL
1 |
SELECT "agents".* FROM "agents" WHERE "agents"."name" = ? LIMIT 1 [["name", "James Bond"]] |
2 |
|
3 |
SELECT "agents".* FROM "agents" WHERE "agents"."name" = ? AND "agents"."licence_to_kill" = ? LIMIT 1 [["name", "James Bond"], ["licence_to_kill", "t"]] |
Tentu saja Anda bisa menjadi gila dengan ini, tapi saya pikir itu kehilangan pesona dan kegunaannya jika Anda melampaui dua atribut:
Rails
1 |
Agent.find_by_name_and_licence_to_kill_and_womanizer_and_gambler_and_number_of_gadgets('James Bond', true, true, true, 3) |
2 |
SQL
1 |
SELECT "agents".* FROM "agents" WHERE "agents"."name" = ? AND "agents"."licence_to_kill" = ? AND "agents"."womanizer" = ? AND "agents"."gambler" = ? AND "agents"."number_of_gadgets" = ? LIMIT 1 [["name", "James Bond"], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"], ["number_of_gadgets", 3]] |
Dalam contoh ini, tetap baik untuk melihat cara kerjanya di bawah tenda. Setiap _and_
baru menambahkan operator SQL AND
untuk secara logis mengikat atribut. Secara keseluruhan, manfaat utama finder dinamis adalah keterbacaan—memasukkan terlalu banyak atribut dinamis akan kehilangan keuntungan dengan cepat. Saya jarang menggunakan ini, mungkin sebagian besar ketika saya bermain-main di konsol, tetapi pasti baik untuk mengetahui bahwa Rails menawarkan tipuan kecil yang rapi ini.
Specific Fields
Rekaman Aktif memberi Anda opsi untuk mengembalikan objek yang sedikit lebih fokus tentang atribut yang dibawanya. Biasanya, jika tidak ditentukan sebaliknya, permintaan akan meminta semua bidang dalam satu baris melalui *
(SELECT "agents".*
), Dan kemudian Rekaman Aktif membangun objek Ruby dengan set atribut lengkap. Namun, Anda dapat select
hanya bidang tertentu yang harus dikembalikan oleh kueri dan membatasi jumlah atribut yang perlu 'dibawa-bawa' oleh objek Ruby Anda.
Rails
1 |
Agent.select("name") |
2 |
|
3 |
=> #<ActiveRecord::Relation [#<Agent 7: nil, name: "James Bond">, #<Agent id: 8, name: "Q">, ...]> |
SQL
1 |
SELECT "agents"."name" FROM "agents" |
Rails
1 |
Agent.select("number, favorite_gadget") |
2 |
|
3 |
=> #<ActiveRecord::Relation [#<Agent id: 7, number: '007', favorite_gadget: 'Walther PPK'>, #<Agent id: 8, name: "Q", favorite_gadget: 'Broom Radio'>, ... ]> |
SQL
1 |
SELECT "agents"."number", "agents"."favorite_gadget" FROM "agents" |
Seperti yang Anda lihat, objek yang dikembalikan hanya akan memiliki atribut yang dipilih, ditambah id mereka tentu saja—yaitu diberikan dengan objek apa pun. Tidak ada bedanya jika Anda menggunakan string, seperti di atas, atau simbol—kueri akan sama.
Rails
1 |
Agent.select(:number_of_kills) |
2 |
|
3 |
Agent.select(:name, :licence_to_kill) |
Peringatan: Jika Anda mencoba mengakses atribut pada objek yang belum Anda pilih dalam kueri, Anda akan menerima MissingAttributeError
. Karena id
akan secara otomatis disediakan untuk Anda, Anda dapat meminta id tanpa memilihnya.
Custom SQL
Terakhir, Anda dapat menulis SQL khusus sendiri melalui find_by_sql
. Jika Anda cukup percaya diri dalam SQL-Fu Anda sendiri dan perlu beberapa panggilan khusus ke database, metode ini mungkin sangat berguna di kali. Tapi ini cerita lain. Jangan lupa untuk memeriksa metode pembungkus Rekaman Aktif terlebih dahulu dan hindari menciptakan kembali roda tempat Rails mencoba menemui Anda lebih dari setengah jalan.
Rails
1 |
Agent.find_by_sql("SELECT * FROM agents") |
2 |
|
3 |
Agent.find_by_sql("SELECT name, licence_to_kill FROM agents") |
Tidak mengejutkan, ini menghasilkan:
SQL
1 |
SELECT * FROM agents |
2 |
|
3 |
SELECT name, licence_to_kill FROM agents |
Karena cakupan dan metode kelas Anda sendiri dapat digunakan secara bergantian untuk kebutuhan pencari kustom Anda, kami dapat mengambil langkah ini lebih jauh untuk kueri SQL yang lebih kompleks.
Rails
1 |
class Agent < ActiveRecord::Base |
2 |
|
3 |
...
|
4 |
|
5 |
def self.find_agent_names |
6 |
query = <<-SQL |
7 |
SELECT name |
8 |
FROM agents |
9 |
SQL
|
10 |
self.find_by_sql(query) |
11 |
end
|
12 |
end
|
Kita bisa menulis metode kelas yang merangkum SQL di dalam dokumen Here. Ini memungkinkan kita menulis string multi-line dengan cara yang sangat mudah dibaca dan kemudian menyimpan string SQL di dalam variabel yang dapat kita gunakan kembali dan masukkan ke find_by_sql
. Dengan begitu kami tidak memplester banyak kode kueri di dalam pemanggilan metode. Jika Anda memiliki lebih dari satu tempat untuk menggunakan kueri ini, itu DRY juga.
Karena ini seharusnya ramah pemula dan bukan tutorial SQL saja, saya membuat contoh sangat minimalis karena suatu alasan. Namun, teknik untuk pertanyaan yang jauh lebih kompleks sama saja. Sangat mudah untuk membayangkan memiliki kueri SQL khusus di sana yang membentang lebih dari sepuluh baris kode.
Lakukan sebanyak yang Anda inginkan—secara masuk akal! Itu bisa menjadi penyelamat. Sepatah kata tentang sintaksis di sini. Bagian SQL
hanyalah pengidentifikasi di sini untuk menandai awal dan akhir string. Saya yakin Anda tidak akan terlalu membutuhkan metode ini—mari berharap! Ini benar-benar memiliki tempatnya, dan Rails tidak akan sama tanpanya—dalam kasus yang jarang terjadi, Anda benar-benar ingin menyempurnakan SQL Anda sendiri dengannya.
Pikiran terakhir
Saya harap Anda mendapat sedikit lebih nyaman menulis kueri dan membaca SQL mentah ol yang ditakuti. Sebagian besar topik yang kami bahas dalam artikel ini sangat penting untuk menulis kueri yang berhubungan dengan logika bisnis yang lebih kompleks. Luangkan waktu Anda untuk memahami ini dan bermain-main sedikit dengan pertanyaan di konsol.
Saya cukup yakin bahwa ketika Anda meninggalkan tanah tutorial, cepat atau lambat kredit Rails Anda akan naik secara signifikan jika Anda mengerjakan proyek kehidupan nyata pertama Anda dan perlu membuat kueri khusus sendiri. Jika Anda masih sedikit malu dengan topik itu, saya katakan cukup bersenang-senang dengannya—itu benar-benar bukan ilmu roket!