Tôi cần phải nói với bạn một điều gì đó, và điều đó khiến tôi phải mất rất nhiều thời gian để thừa nhận. Trong phần lớn sự nghiệp của tôi, tôi nghĩ rằng viết SQL thô là một dấu hiệu của thất bại. Nó có nghĩa là bạn không thể làm cho ORM làm những gì bạn muốn. Nó có nghĩa là trừu tượng của bạn đã bị phá vỡ. kỹ sư cao cấp sử dụng ORMs. kỹ sư cấp cao đã viết SQL. Tôi đã sai. Không chỉ sai. niềm tin đó là tích cực có hại. Nó dẫn tôi để viết mã mà trông sạch sẽ và hiệu quả nhưng thực sự là một thảm họa hiệu suất. Bạn có thể đoán được có bao nhiêu truy vấn SQL cần để tạo một trang duy nhất trong một trong các ứng dụng của chúng tôi? Một trang danh sách đơn giản với hai mươi hàng. Và con số là 47.47 vòng chuyến đi đến cơ sở dữ liệu và trở lại để hiển thị hai mươi hàng dữ liệu. Mã mà sản xuất mà là hoàn toàn sạch sẽ. loại an toàn. hoàn toàn kiểm tra. Với trừu tượng đẹp. Và mã đã lặng lẽ phá hủy thời gian phản ứng của chúng tôi. và không ai nhận thấy điều đó bởi vì sự trừu tượng đã che giấu tội ác khỏi chúng tôi. Một điều mà không ai đưa vào tài liệu tiếp thị của ORM là nó rất giỏi trong việc che giấu những gì nó thực sự làm với cơ sở dữ liệu. Hãy nói về những gì bạn thực sự có ORM mang lại rất nhiều cho bàn ăn.Và đó là một phần của vấn đề.Một số thứ đó là tốt đến nỗi nó có thể làm tổn thương bạn mà không cần bạn nhận ra điều đó. ORM mang lại quản lý sơ đồ - di chuyển phiên bản, quay trở lại và biến đổi sơ đồ. Và phần đó là tuyệt vời. và bạn nên tiếp tục sử dụng nó. Ngoài ra, nó mang đến một API tốt cho các hoạt động đối tượng đơn giản - 'findById', 'tạo', 'cập nhật' và 'xóa'.Và phần đó cũng tuyệt vời.Mã sạch sẽ, loại bảo mật là thực sự, và không có chi phí hiệu suất có ý nghĩa. Điều tiếp theo - bản đồ giữa các đối tượng và bảng. Và đây là một phần cốt lõi của ORM. Và cũng, đây là nơi các vấn đề bắt đầu. bản đồ này là hoàn hảo khi nó hoạt động và tạo ra một vấn đề im lặng khi nó không. ORM mang đến một ảo tưởng về tính di động của cơ sở dữ liệu. Và có, trong một số thế giới lý tưởng, bạn có thể chuyển từ MySQL sang PostgreSQL mà không thay đổi một dòng mã. Nhưng trong thế giới thực, bạn không bao giờ làm. Bạn chọn một cơ sở dữ liệu và bạn tuân thủ nó. Và theo đuổi ảo tưởng đó khiến bạn tránh các tính năng cụ thể của cơ sở dữ liệu mà thực sự sẽ làm cho ứng dụng của bạn nhanh. Nếu bạn hiểu chúng, bạn sẽ sống sót. Nếu bạn không, bạn sẽ kết thúc với bốn mươi bảy (hoặc thậm chí nhiều hơn) truy vấn để hiển thị hai mươi hàng. Vấn đề N+1 là triệu chứng, không phải bệnh tật Gần như tất cả các nhà phát triển đã làm việc với ORM đã nghe nói về vấn đề truy vấn N + 1. mối quan hệ tải lười trong vòng tròn, và điều này sẽ kích hoạt một truy vấn mỗi lần lặp lại. Nhưng tôi nghĩ rằng chỉ tập trung vào vấn đề N+1 bỏ lỡ bức tranh lớn hơn. vấn đề N+1 chỉ là một phần có thể nhìn thấy của tảng băng của các vấn đề ORM - chúng làm cho nó quá dễ dàng để viết mã có vẻ hiệu quả nhưng là một thảm họa dưới nắp. 47 truy vấn của tôi không phải là tất cả các mẫu N + 1. Một số là tìm kiếm dư thừa. Trong một số trong số đó, ORM đang tải toàn bộ thực thể khi chỉ cần hai trường. Một số trong số đó là các truy vấn tự động COUNT được gửi bởi thư viện pagination. Và không có điều này rõ ràng từ việc đọc mã ứng dụng! Bạn chỉ có thể nhìn thấy nó bằng cách nhìn vào nhật ký truy vấn. Và SQL thô cung cấp cho bạn trải nghiệm ngược lại. Bạn biết chính xác truy vấn nào đang được gửi đến cơ sở dữ liệu. Kiểm tra Raw SQL này. SELECT id, name, email FROM users WHERE account_id = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 20 OFFSET 0; Bạn biết truy vấn. Và bạn biết chính xác các cột đang được thu thập. Bạn có thể nghĩ về các chỉ mục bạn sẽ cần cho truy vấn này. Và cũng, bạn có thể dán nó trực tiếp vào console cơ sở dữ liệu của bạn và chạy lệnh EXPLAIN trên nó. Với ORM, tất cả những gì bạn biết là ý định. Việc thực hiện thực tế sẽ được thực hiện sau khi chạy. Và các truy vấn thực tế có thể thay đổi theo thời gian với các thay đổi mô hình của bạn, và bạn có thể không có quyền kiểm soát chúng. Nơi ORM thực sự không thể theo dõi bạn Có một lớp các vấn đề trong đó ORM không chỉ chậm. nó chỉ đơn giản là không thể xử lý chúng. và cấu hình không thể sửa chữa điều đó. Window chức năng Các hàm cửa sổ - `RANK()`, `ROW_NUMBER()`, `LAG()`, `LEAD()`, tổng chạy, trung bình di chuyển - là một trong những tính năng mạnh mẽ nhất của SQL hiện đại. 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 = ?; Truy vấn này sẽ tạo ra một tổng chạy cho tài khoản trong một yêu cầu duy nhất. nó là nhanh chóng, thanh lịch và chính xác. Nếu bạn cố gắng thực hiện tương tự trên trình xây dựng truy vấn của ORM, bạn sẽ không thể làm điều đó. Bạn hoặc kéo tất cả các hàng vào bộ nhớ và tính toán nó ở cấp độ ứng dụng, hoặc bạn viết SQL thô. Bulk hoạt động Hãy tưởng tượng kịch bản này. Bạn cần chèn 10.000 bản ghi từ một API bên ngoài và cần chèn hoặc cập nhật chúng vào cơ sở dữ liệu của bạn. bản ghi mới nên được chèn và bản ghi hiện có nên được cập nhật nếu dữ liệu nhập mới hơn. Cách tiếp cận 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(); Để xử lý 10k hồ sơ, chúng ta sẽ phải thực hiện 10.000 truy vấn chọn và 10.000 chọn hoặc cập nhật truy vấn. Cách tiếp cận 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); Hàng loạt trong các miếng của 500-1,000 hàng. Tổng thời gian thực hiện: giây. 'ON DUPLICATE KEY UPDATE' là một tính năng MySQL. Nó không có tương đương ORM. Abstraction chỉ đơn giản là không thể bao gồm điều này. Hoạt động JSON Nếu bạn đang lưu trữ JSON trong cơ sở dữ liệu của bạn, và sau khi MySQL 8 và PostgreSQL đã làm cho nó khá hữu ích. Lọc theo con đường JSON? Chỉ mục trên một giá trị trường JSON? Sử dụng 'JSON_TABLE' để chuyển đổi một mảng JSON thành hàng? Đây là các hoạt động thực sự mà cơ sở dữ liệu xử lý tốt. -- 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')))); Trong khoảnh khắc bạn cần một cái gì đó như thế này, bạn đang viết SQL thô dù sao. Về mặt kỹ thuật, bạn có thể chuyển đổi trường json bên trong thành một trường ảo trong bảng, nhưng nó giống như một thủ thuật. Debugging: Thuế Ẩn Đây là một kịch bản mà bạn có thể đã trải qua. Bạn có thể nhớ một truy vấn chậm trong sản xuất.Không quá chậm.Nhưng đủ chậm để khách hàng phàn nàn về nó, hoặc để giám sát để bắt đầu đánh dấu nó. Nó là một chuỗi phương pháp: mười lăm cuộc gọi sâu, bộ lọc được áp dụng có điều kiện dựa trên các thông số yêu cầu, một vài liên kết được thêm vào đâu đó trong một lớp kho cơ bản, tải mong muốn được cấu hình trong một tệp mô hình mà bạn đã không mở trong một thời gian. Bạn bật ghi nhật ký truy vấn và ghi lại SQL. Nó có 60 dòng. Nó có một subquery mà bạn không nhớ viết. Có một 'ORDER BY' trên một cột không được lập chỉ mục. Bây giờ bạn cần phải tìm ra subquery đó đến từ đâu. nào trong số mười lăm chuỗi gọi phương pháp tạo ra nó? nó đến từ lớp cơ bản hay đứa trẻ? Đây là thuế ẩn của sự phức tạp của ORM. Khi một cái gì đó đi sai, bạn đang giải quyết hai lớp cùng một lúc: logic ứng dụng và logic tạo truy vấn. // 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' Hành vi 'SELECT *' đó - kéo mỗi cột từ mỗi bảng liên kết vào bộ nhớ - là những gì bạn đang trả tiền khi bạn sử dụng hydrat hóa thực thể đầy đủ.Thường, bạn chỉ cần ba trong số hai mươi cột đó. ORM đã thu thập tất cả hai mươi vì nó muốn cung cấp cho bạn một đối tượng hoàn toàn hydrat hóa. Kiến trúc thực sự hoạt động Sau mười năm làm và quan sát những sai lầm này, đây là cấu trúc tôi áp dụng cho các dự án tôi làm việc trên. ORM owns: - Tất cả định nghĩa sơ đồ và di chuyển Quản lý vòng đời thực thể (Create, Persist, Flush, Remove) - Tìm kiếm đơn giản: tìm bằng ID, tìm bằng bộ lọc duy nhất, danh sách các thực thể được trang - Quản lý mối quan hệ trong các bộ kết quả giới hạn, nhỏ Raw SQL owns: - Các truy vấn dành cho báo cáo, bảng điều khiển hoặc xuất khẩu - Tất cả các hoạt động số lượng lớn: Batch inserts, batch updates, upserts - Tập hợp qua nhiều bảng - Bất kỳ truy vấn nào sử dụng các tính năng cụ thể của cơ sở dữ liệu - Và khá thường xuyên, bất kỳ truy vấn nào xuất hiện trong nhật ký truy vấn chậm Trong Symfony, việc thực hiện sự tách biệt này là sạch sẽ. Trạm lưu trữ thực thể xử lý lớp ORM. Một tập hợp các lớp truy vấn riêng biệt - tôi gọi chúng là "trạm lưu trữ đọc" hoặc "trạm lưu trữ truy vấn" - xử lý lớp SQL bằng cách sử dụng DBAL trực tiếp: // 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 cung cấp cho bạn mọi thứ bạn cần - các tuyên bố chuẩn bị, quản lý kết nối, hỗ trợ giao dịch. Bạn không từ bỏ bảo mật để viết SQL. Bạn đang từ bỏ hydrat hóa đối tượng, mà bạn không cần cho truy vấn này dù sao. Cũng lưu ý những gì phiên bản SQL thô cung cấp cho bạn mà phiên bản ORM không thể: truy vấn là ngay ở đó. bất kỳ nhà phát triển nào trong nhóm có thể đọc nó, hiểu nó, sao chép nó vào một bảng điều khiển cơ sở dữ liệu, chạy 'EXPLAIN' trên nó, và điều chỉnh nó. Kỹ năng không bao giờ là tùy chọn Một số người nghĩ rằng SQL là một kỹ năng thừa kế, một cái gì đó bạn rơi vào khi các công cụ hiện đại thất bại với bạn, chẳng hạn như biết cách sử dụng bản đồ vật lý vì điện thoại của bạn đã chết. Tôi nghĩ điều này đưa nó trở lại. SQL là ngôn ngữ mà cơ sở dữ liệu của bạn nói. cơ sở dữ liệu đang làm việc nhiều hơn bất kỳ thành phần nào khác trong hầu hết các ứng dụng backend. Hiểu được những gì bạn đang yêu cầu nó làm không phải là chuyên môn tùy chọn. Các nhà phát triển tôi đã tôn trọng nhất trong sự nghiệp của tôi chia sẻ một điều chung: họ thoải mái ở mọi lớp. Họ sử dụng ORM khi nó phù hợp và viết SQL khi nó không. Họ không cảm thấy như đạt được cho SQL là một sự thừa nhận của thất bại. Họ cảm thấy như nó đang sử dụng các công cụ phù hợp. Và họ biết các công cụ phù hợp bởi vì họ hiểu cả hai. Chúng tôi không quan tâm làm thế nào truy vấn SQL cuối cùng đã được tạo ra. chúng tôi quan tâm nếu nó có thể sử dụng một chỉ mục, có bao nhiêu hàng nó phải kiểm tra, và có bao nhiêu dữ liệu nó phải di chuyển.