Saya harus mengatakan sesuatu kepada Anda, dan itu memakan waktu yang sangat lama untuk mengakui. Selama sebagian besar karir saya, saya berpikir bahwa menulis SQL mentah adalah tanda kegagalan. itu berarti Anda tidak bisa membuat ORM melakukan apa yang Anda inginkan. itu berarti abstraksi Anda telah rusak. insinyur senior menggunakan ORM. insinyur junior menulis SQL. Saya salah. bukan hanya salah. keyakinan itu secara aktif berbahaya. itu menyebabkan saya menulis kode yang tampak bersih dan efisien tetapi sebenarnya merupakan bencana kinerja. Dapatkah Anda menebak berapa banyak kueri SQL yang dibutuhkan untuk menghasilkan satu halaman dalam salah satu aplikasi kami? Dan jumlahnya adalah 47 putaran perjalanan ke database dan kembali untuk menampilkan dua puluh baris data. Kode yang dihasilkan itu benar-benar bersih. tipe aman. sepenuhnya diuji. dengan abstraksi yang indah. Dan kode diam-diam menghancurkan waktu tanggapan kita. dan tidak ada yang memperhatikan itu karena abstraksi telah menyembunyikan kejahatan dari kita. Satu hal yang tidak ada yang menempatkan dalam materi pemasaran ORM adalah bahwa itu sangat baik dalam menyembunyikan apa yang sebenarnya dilakukan dengan database. Mari kita bicara tentang apa yang sebenarnya Anda miliki ORM membawa banyak ke meja. dan itu adalah bagian dari masalah. beberapa hal itu sangat baik sehingga bisa menyakiti Anda tanpa Anda menyadarinya. ORM membawa manajemen skema - migrasi versi, rollback, dan skema diffing. Juga, ia membawa API yang bagus untuk operasi objek sederhana - `findById`, `create`, `update`, dan `delete`. Dan bagian itu juga fantastis. kode bersih, jenis keamanan nyata, dan tidak ada biaya kinerja yang berarti. Hal berikutnya - memetakan antara objek dan tabel. Dan ini adalah bagian inti dari ORM. Dan juga, ini adalah tempat di mana masalah dimulai. ORM membawa ilusi portabilitas database. dan ya di dunia ideal Anda dapat beralih dari MySQL ke PostgreSQL tanpa mengubah baris kode. tetapi di dunia nyata, Anda tidak pernah melakukannya. Anda memilih database dan Anda berpegang padanya. dan mengejar ilusi itu menyebabkan Anda menghindari fitur khusus database yang sebenarnya akan membuat aplikasi Anda cepat. Setiap abstraksi membuat kompromi. jika Anda mengerti mereka, Anda akan bertahan hidup. jika Anda tidak, Anda akan berakhir dengan empat puluh tujuh (atau bahkan lebih) pertanyaan untuk menunjukkan dua puluh baris. Masalah N+1 adalah gejala, bukan penyakit Hampir setiap pengembang yang telah bekerja dengan ORM telah mendengar tentang masalah kueri N + 1. hubungan beban yang malas di dalam loop, dan ini akan memicu satu kueri per iterasi. Masalah N+1 hanyalah bagian yang terlihat dari gunung es dari masalah ORM - mereka membuatnya terlalu mudah untuk menulis kode yang terlihat efisien tetapi adalah bencana di bawah topi. 47 kueri saya tidak semua N + 1 pola. Beberapa adalah pencarian berlebihan. Dalam beberapa dari mereka, ORM mengunggah entitas penuh ketika hanya dua bidang yang dibutuhkan. Beberapa dari mereka adalah kueri AUTOMATIK COUNT yang dikirim oleh perpustakaan paginasi. Dan tidak ada yang jelas dari membaca kode aplikasi! Anda dapat melihatnya hanya dengan melihat log kueri. Dan SQL mentah memberi Anda pengalaman yang berlawanan. Anda tahu persis pertanyaan apa yang dikirim ke database. Coba periksa dengan raw SQL ini. SELECT id, name, email FROM users WHERE account_id = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 20 OFFSET 0; Anda tahu kueri. dan Anda tahu kolom yang tepat yang ditarik. Anda dapat memikirkan indeks yang Anda butuhkan untuk kueri ini. dan juga, Anda dapat menyisipkannya langsung ke konsol database Anda dan menjalankan perintah EXPLAIN di atasnya. Dengan ORM, semua yang Anda ketahui adalah niat. implementasi sebenarnya akan terjadi nanti pada waktu runtime. dan pertanyaan nyata mungkin berubah seiring waktu dengan perubahan model Anda, dan Anda mungkin tidak memiliki kendali atas mereka. Di mana ORM benar-benar tidak dapat mengikuti Anda Terdapat kelas masalah di mana ORM tidak hanya lambat. ia hanya tidak dapat menangani mereka. dan konfigurasi tidak dapat memperbaiki itu. Fungsi Jendela Fungsi jendela - `RANK()`, `ROW_NUMBER()`, `LAG()`, `LEAD()`, total berjalan, rata-rata bergerak - adalah salah satu fitur paling kuat dari SQL modern. SELECT user_id, amount, order_date, SUM(amount) OVER ( PARTITION BY user_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total FROM orders WHERE account_id = ?; Pertanyaan ini akan menghasilkan total berjalan untuk akun dalam satu permintaan. itu cepat, elegan dan akurat. Jika Anda mencoba untuk menerapkan hal yang sama pada pembuat kueri ORM, Anda tidak akan dapat melakukannya. Jadi ada dua pilihan untuk melakukan perhitungan itu.Anda baik menarik semua baris ke dalam memori dan menghitungnya pada tingkat aplikasi, atau Anda menulis SQL mentah.Tidak ada pilihan ketiga. Operasi Bulk Bayangkan skenario ini. Anda perlu menyisipkan 10.000 catatan dari API eksternal dan perlu menyisipkan atau memperbarui mereka ke dalam database Anda. catatan baru harus disisipkan dan yang sudah ada harus diperbarui jika data yang masuk lebih baru. Pendekatan ORM : foreach ($incomingRecords as $data) { $record = $this->repository->findOneBy(['external_id' => $data['id']]); if ($record === null) { $record = new SyncRecord(); $record->setExternalId($data['id']); } $record->setPayload($data['payload']); $record->setSyncedAt(new \DateTimeImmutable()); $this->em->persist($record); } $this->em->flush(); Untuk memproses 10k catatan, kita harus mengeksekusi 10.000 kueri pilih dan 10.000 kueri pilih atau update. Pendekatan SQL : INSERT INTO sync_records (external_id, payload, synced_at) VALUES (?, ?, NOW()), (?, ?, NOW()), ... ON DUPLICATE KEY UPDATE payload = VALUES(payload), synced_at = VALUES(synced_at); Batch dalam potongan 500-1000 baris. Total waktu eksekusi: detik. 'ON DUPLICATE KEY UPDATE' adalah fitur MySQL. Ini tidak memiliki setara ORM. abstraksi hanya tidak dapat mencakup ini. Operasi JSON Jika Anda menyimpan JSON di database Anda, dan setelah MySQL 8 dan PostgreSQL membuatnya cukup berguna. dan banyak aplikasi melakukannya. Mengindeks pada nilai bidang JSON? Menggunakan 'JSON_TABLE' untuk mengkonversi array JSON menjadi baris? Ini adalah operasi nyata yang database menangani dengan baik. -- Find all products where attributes contain color = 'blue' SELECT * FROM products WHERE JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color')) = 'blue'; -- This works because we created a functional index: -- CREATE INDEX idx_color ON products((JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color')))); Secara teknis, Anda dapat mengubah bidang json internal menjadi bidang virtual di tabel, tetapi itu lebih seperti trik. Debugging: Pajak yang Tersembunyi Berikut adalah skenario yang mungkin Anda alami. Anda mungkin ingat permintaan yang lambat dalam produksi.Tidak terlalu lambat.Tapi cukup lambat bagi pelanggan untuk mengeluh tentang hal itu, atau untuk pemantauan untuk mulai menandai itu. Ini adalah rantai metode: lima belas panggilan mendalam, filter diterapkan secara kondisional berdasarkan parameter permintaan, beberapa joins ditambahkan di suatu tempat di kelas repository dasar, loading yang ingin dikonfigurasi dalam file model yang belum Anda buka dalam beberapa waktu. Anda mengaktifkan log query dan menangkap SQL. Ini adalah 60 baris. Ini memiliki subquery yang tidak Anda ingat menulis. Ada 'order by' pada kolom yang tidak diindeks. Sekarang Anda perlu mencari tahu dari mana subquery itu berasal. mana dari lima belas metode rantai panggilan yang diproduksi? apakah itu berasal dari kelas dasar atau anak? Ini adalah pajak tersembunyi dari kompleksitas ORM. Ketika sesuatu salah, Anda memecahkan dua lapisan secara bersamaan: logika aplikasi dan logika generasi kueri. // What you see in code review $users = $this->userRepository ->createQueryBuilder('u') ->leftJoin('u.account', 'a') ->andWhere('u.isActive = :active') ->andWhere('a.plan = :plan') ->setParameter('active', true) ->setParameter('plan', $plan) ->getQuery() ->getResult(); // What actually runs. And nobody reads until something breaks SELECT u.id, u.name, u.email, u.created_at, u.updated_at, u.is_active, u.account_id, a.id AS a_id, a.name AS a_name, a.plan AS a_plan, a.is_active AS a_is_active, a.created_at AS a_created_at, a.updated_at AS a_updated_at, a.meta AS a_meta FROM users u LEFT JOIN accounts a ON a.id = u.account_id WHERE u.is_active = 1 AND a.plan = 'enterprise' Perilaku 'SELECT *' - menarik setiap kolom dari setiap tabel yang bergabung ke dalam memori - adalah apa yang Anda bayar ketika Anda menggunakan hidrasi entitas penuh. Seringkali, Anda hanya membutuhkan tiga dari dua puluh kolom itu. ORM mengambil semua dua puluh karena ingin memberi Anda obyek yang sepenuhnya terhidrasi. Arsitektur yang benar-benar berfungsi Setelah sepuluh tahun membuat dan mengamati kesalahan-kesalahan ini, inilah struktur yang saya terapkan pada proyek-proyek yang saya kerjakan. ORM owns: - Semua definisi skema dan migrasi Manajemen Siklus Kehidupan Entitas (Create, Persist, Flush, Remove) - Pencarian sederhana: temukan dengan ID, temukan dengan filter tunggal, daftar entitas berpaginasi - Manajemen hubungan dalam set hasil yang terbatas, kecil Raw SQL owns: Pertanyaan yang ditujukan untuk laporan, dashboard, atau ekspor Semua operasi bulk: insert batch, update batch, upserts Aggregasi di berbagai tabel - Setiap kueri yang menggunakan fitur khusus database - Dan cukup sering, setiap kueri yang muncul di log kueri lambat Dalam Symfony, implementasi pemisahan ini bersih. repositori entitas menangani lapisan ORM. Sejumlah kelas query terpisah - saya menyebutnya "read repositories" atau "query repositories" - menangani lapisan SQL menggunakan DBAL secara langsung: // Entity repository - pure ORM, simple operations class UserRepository extends ServiceEntityRepository { public function findActiveById(int $id): ?User { return $this->findOneBy(['id' => $id, 'isActive' => true]); } public function save(User $user): void { $this->getEntityManager()->persist($user); $this->getEntityManager()->flush(); } } // Query repository - raw SQL, complex reads class UserAnalyticsRepository { public function __construct(private readonly Connection $db) {} public function getRetentionBySignupCohort(\DateTimeImmutable $from, \DateTimeImmutable $to): array { $sql = <<<SQL SELECT DATE_FORMAT(u.created_at, '%Y-%m') AS cohort, COUNT(DISTINCT u.id) AS total_users, COUNT(DISTINCT CASE WHEN u.last_active_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN u.id END) AS active_last_30d, ROUND( COUNT(DISTINCT CASE WHEN u.last_active_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN u.id END) / COUNT(DISTINCT u.id) * 100, 1 ) AS retention_pct FROM users u WHERE u.created_at BETWEEN :from AND :to GROUP BY cohort ORDER BY cohort ASC SQL; return $this->db->fetchAllAssociative($sql, [ 'from' => $from->format('Y-m-d'), 'to' => $to->format('Y-m-d'), ]); } } DBAL memberi Anda semua yang Anda butuhkan - pernyataan siap, manajemen koneksi, dukungan transaksi. Anda tidak menyerah pada keamanan untuk menulis SQL. Anda menyerah pada hidrasi objek, yang tidak Anda butuhkan untuk kueri ini. Perhatikan juga apa yang versi SQL mentah memberi Anda yang versi ORM tidak bisa: kueri ada di sana. Setiap pengembang di tim dapat membacanya, mengerti, menyalin ke konsol database, menjalankan 'EXPLAIN' di atasnya, dan menyesuaikannya. Keterampilan yang Tidak Pernah Optional Beberapa orang berpikir bahwa SQL adalah keterampilan warisan, sesuatu yang Anda jatuh kembali ketika alat modern gagal Anda, seperti mengetahui cara menggunakan peta fisik karena ponsel Anda meninggal. Saya pikir ini mengarah ke belakang. SQL adalah bahasa database Anda berbicara. database melakukan lebih banyak pekerjaan daripada komponen lain dalam sebagian besar aplikasi backend. Memahami apa yang Anda minta untuk dilakukan bukanlah keahlian opsional. Para pengembang yang paling saya hormati selama karier saya berbagi sesuatu yang sama: mereka nyaman di setiap lapisan. mereka menggunakan ORM ketika cocok dan menulis SQL ketika tidak. mereka tidak merasa seperti mencapai untuk SQL adalah pengakuan kekalahan. mereka merasa seperti itu menggunakan alat yang tepat. dan mereka tahu alat yang tepat karena mereka mengerti keduanya. Kami tidak peduli bagaimana kueri SQL akhir dibuat. kami peduli apakah itu dapat menggunakan indeks, berapa banyak baris yang harus diperiksa, dan berapa banyak data yang harus dipindahkan.