Turiu jums kažką pasakyti, ir man prireikė labai daug laiko, kad tai pripažintum. Didžiąją dalį savo karjeros aš maniau, kad rašyti žaliavinį SQL yra nesėkmės ženklas. Tai reiškė, kad jūs negalite priversti ORM daryti tai, ko norėjote. Tai reiškė, kad jūsų abstrakcijos buvo sugadintos. vyresnieji inžinieriai naudojo ORMs. Aš klydau. Ne tik klydau. Tas įsitikinimas buvo aktyviai kenksmingas. Tai paskatino mane parašyti kodą, kuris atrodė švarus ir efektyvus, bet iš tikrųjų buvo našumo katastrofa. Ar galite atspėti, kiek SQL užklausų prireikė, kad viename iš mūsų programų būtų rodomas vienas puslapis? Ir skaičius yra 47. 47 apskrito kelionės į duomenų bazę ir atgal parodyti dvidešimt duomenų eilučių. Kodas, kuris sukūrė tai buvo visiškai švarus. Tipas saugus. Visiškai išbandytas. Su gražiais abstrakcijomis. Ir kodas tyliai sunaikino mūsų atsakymo laiką. ir niekas to nepastebėjo, nes abstrakcija paslėpė nusikaltimą nuo mūsų. Vienas dalykas, kurį niekas neįdeda į ORM rinkodaros medžiagą, yra tai, kad ji labai gerai slepia, ką ji iš tikrųjų daro su duomenų baze. Pakalbėkime apie tai, ką iš tikrųjų turite ORM atneša daug prie stalo. Ir tai yra problemos dalis. Kai kurie iš tų daiktų yra tokie geri, kad jie gali jums pakenkti, nepastebėdami to. ORM atneša schemos valdymą - versijų perkėlimus, atgalinius perdavimus ir schemos išnykimą. Ir ta dalis yra fantastiška. Be to, jis atneša gerą API paprastoms objektų operacijoms - "findById", "sukurti", "atnaujinti" ir "pašalinti".Ir ta dalis taip pat yra fantastiška. Kitas dalykas - kartografavimas tarp objektų ir lentelių. Ir tai yra pagrindinė ORM dalis. Ir taip pat, tai yra vieta, kur prasideda problemos. Šis kartografavimas yra puikus, kai jis veikia ir sukuria tylų problemą, kai jis neveikia. ORM atneša duomenų bazių perkeliamumo iliuziją. Ir taip, tam tikru idealiu pasauliu galite perjungti iš MySQL į PostgreSQL nekeičiant kodo linijos. Bet realiame pasaulyje jūs niekada to nedarysite. Jūs pasirenkate duomenų bazę ir laikysitės jos. Kiekviena abstrakcija daro kompromisus. Jei juos suprasite, išgyvensite. Jei ne, galų gale turėsite keturiasdešimt septynių (ar net daugiau) užklausų, kad parodytumėte dvidešimt eilučių. N+1 problema yra simptomas, o ne liga Beveik kiekvienas kūrėjas, kuris dirbo su ORM, yra girdėjęs apie N+1 užklausų problemą. Tačiau manau, kad sutelkiant dėmesį tik į N+1 problemą praleidžia didesnį vaizdą. „N+1“ problema yra tik matoma dalis ORM problemų ledkalnio - jie pernelyg lengvai rašo kodą, kuris atrodo efektyvus, bet yra katastrofa po gaubtu. Mano 47 užklausos buvo ne visi N + 1 modeliai. Kai kurie buvo nereikalingi paieškos. Kai kuriuose iš jų ORM įkėlė visus subjektus, kai reikėjo tik dviejų laukų. Kai kurie iš jų buvo automatiniai COUNT užklausos, siunčiamos iš puslapio bibliotekos. Ir nė vienas iš šių dalykų nėra akivaizdus skaitydamas paraiškos kodą! Tai galite pamatyti tik žiūrėdami į užklausos žurnalą. Žalioji SQL suteikia jums priešingą patirtį. Jūs tiksliai žinote, kokios užklausos siunčiamos į duomenų bazę. Patikrinkite šį RAW SQL. SELECT id, name, email FROM users WHERE account_id = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 20 OFFSET 0; Jūs žinote užklausą. Ir jūs žinote tikslius stulpelius, kurie yra paimti. Galite galvoti apie indeksus, kurių jums reikės šiai užklausai. Ir taip pat, galite įklijuoti jį tiesiai į savo duomenų bazės konsolę ir paleisti EXPLAIN komandą ant jo. Su ORM, viskas, ką žinote, yra ketinimas. Tikrasis įgyvendinimas bus vėliau paleidimo metu. Ir realios užklausos gali pasikeisti laikui bėgant, kai keičiasi jūsų modelis, ir jūs negalite jų kontroliuoti. Kur ORMs tikrai negali sekti Yra problemų klasė, kai ORM yra ne tik lėtas. jis tiesiog negali juos tvarkyti. ir konfigūracija negali to išspręsti. Langų funkcijos Langų funkcijos - `RANK()`, `ROW_NUMBER()`, `LAG()`, `LEAD()`, vykstančios sumos, besikeičiančios vidurkiai - yra viena iš galingiausių šiuolaikinės SQL savybių. 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 = ?; Ši užklausa sukurs sąskaitos vykdomąją sumą viename prašyme. ji yra greita, elegantiška ir teisinga. Jei bandysite įgyvendinti tą patį ORM užklausų kūrėjo, jūs negalėsite tai padaryti. Taigi yra dvi galimybės atlikti tą skaičiavimą.Jūs arba ištraukiate visas eilutes į atmintį ir apskaičiuojate ją taikomosios programos lygiu, arba rašote žalią SQL. Bulk operacijos Įsivaizduokite scenarijų. Turite įterpti 10 000 įrašų iš išorinio API ir juos įterpti arba atnaujinti savo duomenų bazėje. Nauji įrašai turėtų būti įterpti, o esami turėtų būti atnaujinti, jei gaunami duomenys yra naujesni. ORM požiūris: 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(); Norėdami apdoroti 10 000 įrašų, turėsime atlikti 10 000 pasirinktų užklausų ir 10 000 pasirinktų ar atnaujintų užklausų. SQL požiūris: INSERT INTO sync_records (external_id, payload, synced_at) VALUES (?, ?, NOW()), (?, ?, NOW()), ... ON DUPLICATE KEY UPDATE payload = VALUES(payload), synced_at = VALUES(synced_at); Išdėstytas 500-1000 eilučių gabalėliuose. Bendras vykdymo laikas: sekundės. „DUPLICATE KEY UPDATE“ yra MySQL funkcija. Jame nėra ORM ekvivalento. Abstrakcija tiesiog negali tai padengti. JSON operacijos Jei saugote JSON savo duomenų bazėje, o po MySQL 8 ir PostgreSQL tai padarė gana naudinga. ir daugelis programų tai daro. JSON kelio filtravimas? JSON lauko reikšmės indeksavimas? „JSON_TABLE“ naudojimas JSON masės konvertavimui į eilutes? Tai yra realios operacijos, kurias duomenų bazės tvarko gerai. Tai taip pat operacijos, kurias ORM tvarko prastai, iš dalies arba visai ne. -- 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')))); Techniškai galite paversti vidinį json lauką į virtualų lauką lentelėje, bet tai labiau panašu į triuką. Debugging: paslėptas mokestis Štai scenarijus, kurį tikriausiai patyrėte. Jūs tikriausiai prisimenate lėtą užklausą gamyboje.Ne labai lėtai, bet pakankamai lėtai, kad klientai galėtų skųstis dėl to, ar stebėti, kad pradėtų jį žymėti. Tai metodų grandinė: penkiolika skambučių giliai, filtrai taikomi sąlygiškai pagal užklausos parametrus, keletas jungčių pridėta kažkur bazinėje saugyklos klasėje, aistringas įkrovimas sukonfigūruotas modelio faile, kurio jau kurį laiką neatidarėte. Galite įgalinti užklausų įrašymą ir užfiksuoti SQL. Jame yra 60 eilučių. Jame yra subpaklausa, kurios nepamenate parašyti. stulpelyje, kuris nėra indeksuojamas, yra "UŽDARYTI". Kuris iš penkiolikos metodų grandinės skambučių jį sukūrė? ar jis kilęs iš bazinės klasės, ar vaiko? Tai yra paslėptas ORM sudėtingumo mokestis. Kai kažkas negerai, vienu metu iškraunate du sluoksnius: taikymo logiką ir užklausos generavimo logiką. // 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' Toks „SELECT *“ elgesys - kiekvieno stulpelio traukimas iš kiekvienos sujungtos lentelės į atmintį - yra tai, už ką mokate, kai naudojate pilną subjektų hidrataciją. Architektūra, kuri tikrai veikia Po dešimties metų, kai padariau ir stebėjau šias klaidas, čia yra struktūra, kurią aš pritaikiau projektams, kuriuose dirbu. ORM owns: - Visi schemos apibrėžimai ir migracijos - Entity gyvavimo ciklo valdymas (kurti, išlaikyti, išplauti, pašalinti) - Paprastos paieškos: ieškokite pagal ID, ieškokite pagal vieną filtrą, puslapių sąrašus subjektų - Santykių valdymas riboto, mažo rezultato rinkiniuose Raw SQL owns: - Užklausos, skirtos ataskaitoms, skirtukams ar eksportui - Visos didelės apimties operacijos: serijos įdėklai, serijos atnaujinimai, upserts Agregacija per kelias lenteles - Bet kokia užklausa, kuri naudoja duomenų bazės specifines funkcijas - Ir gana dažnai, bet kokia užklausa, kuri pasirodo lėtoje užklausos žurnale Simfonijoje šio atskyrimo įgyvendinimas yra švarus. Entity repositories tvarko ORM sluoksnį. Atskiras užklausų klasių rinkinys – aš juos vadinu „skaitymo repozitoriumais“ arba „užklausų repozitoriumais“ – tvarko SQL sluoksnį tiesiogiai naudojant DBAL: // 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 suteikia jums viską, ko jums reikia - paruoštus pareiškimus, ryšio valdymą, sandorių palaikymą.Jūs neatsisakysite saugumo rašyti SQL.Jūs atsisakysite objektų hidratacijos, kurios jums nereikėjo šiai užklausai. Taip pat atkreipkite dėmesį į tai, ką žaliavinė SQL versija suteikia jums, kad ORM versija negali: užklausa yra ten. Bet kuris kūrėjas komandoje gali ją perskaityti, suprasti, nukopijuoti į duomenų bazės konsolę, paleisti "EXPLAIN" ant jo ir sureguliuoti. Įgūdžiai, kurie niekada nebuvo neprivalomi Kai kurie žmonės mano, kad SQL yra paveldimas įgūdis, kažkas, ką jūs sugrįšite, kai šiuolaikiniai įrankiai jums nepavyksta, pavyzdžiui, žinoti, kaip naudoti fizinį žemėlapį, nes jūsų telefonas mirė. Manau, kad tai veda atgal. SQL yra kalba, kuria kalba jūsų duomenų bazė. Duomenų bazė atlieka daugiau darbo nei bet kuris kitas komponentas daugelyje galinių programų. Suprasti, ką paprašysite, kad ji atliktų, nėra neprivaloma patirtis. Kūrėjai, kuriuos labiausiai gerbiu per savo karjerą, turi vieną bendrą dalyką: jie yra patogūs kiekviename sluoksnyje. Jie naudoja ORM, kai jis tinka, ir rašo SQL, kai jis ne. Jie nemano, kad pasiekti SQL yra pralaimėjimo pripažinimas. Jie jaučia, kad jis naudoja tinkamą įrankį. Ir jie žino tinkamą įrankį, nes jie supranta abu. Mums nerūpi, kaip buvo sukurta galutinė SQL užklausa. Mums rūpi, ar ji gali naudoti indeksą, kiek eilučių ji turi išnagrinėti ir kiek duomenų ji turi perkelti.