Článek k zamyšlení pro ty, kdo opravdu dobře znají C++. Nejprve si přečtěte toto: Native Queries for Persistent Objects
Málokoho asi překvapí, že objektová databáze včetně nativních dotazů, jak je popsána ve zmíněném článku, je implementovatelná v Javě a C# (existují také podobné projekty pro Smalltalk a další objektově-orientované jazyky). Výzvou je implementovat takovou databázi v C++. Co by měla umět?
Už bod 1 je netriviální. C++ nemá introspekci, takže si budeme muset pomoci nějakým trikem, abychom mohli napsat něco jako:
Database db;
PersistentObject obj;
obj.numField = 20;
db.save(obj);
Bod 2, pokud se nám povede splnit bod 1, je triviální. Není problémem napsat funkci get použitelnou takto:
std::vector<PersistentObject> collection = db.get("FROM PersistentObject obj WHERE obj.numField = 20");
Opravdovou výzvou je ovšem bod 3. Chceme možnost vyjadřovat dotazy do databáze přímo v C++ (důvodů je mnoho, jedním z nich je například syntaktická kontrola dotazů během kompilace, viz výše zmíněný článek), tedy něco jako:
std::vector<PersistentObject> collection = db.get(Query(PersistentObject, obj.numField == 20));
Nějaké návrhy? Těším se na diskusi (otázkou je, zda to v C++ vůbec jde, a pokud ano, nastínit řešení).
C++ sice moc nerozumím, ale co přepokládám:
1) Jestliže C++ nemá reflexi, jsou zde 2 možnosti: Buďto ji nějakým netriviálním (rozeznání struktury objektu přímo v paměti - nepředpokládám, že to jde, nebo zpracování deklarace třídy ze zdrojáku a její dostupnost v runtime) způsobem doděláte, nebo musíte vytvořit mapovač pro každou ukládanou třídu extra pro ukládání do DB.
2) Je to tak, collection je běžný objekt s adresováním jeho vlastností indexy místo pojmenováními. Na druhou stranu tato funkcionalita může porušit zapouzdření dat (danou dostupností instancí) - je to, jako kdybyste chtěl mít v aplikaci dostupnou funkci správce paměti, který vám vrátí všechny objekty vybrané vlastnosti, takže se můžete hrabat zcela kdekoli.
3) Obdobně jako 1) - bez reflexivity to asi nepůjde, ještě k tomu „obj.numField == 20“ není vlastně jak přeložit. Kdyby to bylo jako řetězec k překladu na dotaz databází, tak už se s tím dá něco dělat. Možná i jako uzávěra by to šlo zpracovat, ale ty C++ nemá. Mimoto opět porušení zapouzdření jako v 2).
@2 ad 1) Správná úvaha, je nutné "zpracování deklarace třídy ze zdrojáku". Některé OO databáze k tomu mají svůj speciální předkompilátor ("prepreprocesor"). Upřesňuji tedy otázku: jako to udělat s využitím pouze standardního preprocesoru v C++.
ad 3) Je to jak přeložit. Podle kontextu znamená "obj.numField == 20" různé věci.
P.S. "Uzávěra" mý být lambda výraz s lexikálně uzavřeným kontextem? Ty C++ má a ano, jsou k zodpovězení otázky nutné (teď jsem prozradil, že řešení existuje).
Každopádně díky za komentář.
1) Myslim, ze nejlepsi je nejaky preprocesor. Sice se musi generovat pri zmene, ale aspon se to hezky debuguje. Proste neco jako gsoap (Native->XML), ale pro tables. Asi je to radove jednodussi, kdyz to porovnam s gsoapem, ktery musim umet cele XSD.
2) Silne typovana odpoved je jednoducha a naznacil jsi ji.
3) Osobne jsme nikdy nenasel oblibu a ani vetsi pouzitelnost v techto dotazech at uz JPA nebo Hibernate. Proste refaktoring by nemel byt u dat tak casty. Mnohem jednodussi je vygenerovat si konstanty z bodu 1 a pak je pouzivat jako:
PersistentObject::Name - nazev tabulky
PersistentObject::F_numField - nazev numField
Pri prejmenovani se zmeni i tyto nazvy symbolu a hodi kompilacni chybu.
Zdravim,
pro interni potreby mame implementovano neco podobneho. Jen ne pro objektove databaze nybrz bezne SQL (MySQL, SQLite, SQLServer).
Nase implementace vychazi z Hibernate/Doctrine2, tzn Entity manager.
Pro definovani modelu pouzivame nas ORM Designer (www.orm-designer.com), ktery vyexportuje XML definice (komercne jej prodavame pro PHP ORM frameworky).
Z techto XML pomoci naseho preprocesoru vygenerujeme base tridy pro entity a jejich epository, dal pak strom konstatn pro Query Builder (slozeny z struct/namespaces), aby bylo mozne dotazy skladat pomoci intellisense, pri zmene DB hlasily errory atd. Tridy s base entitami v sobe maji vsechny Gettery/Settery pro property i asociace. Dal se musi vygenerovat jedna velka map super-class ktera obsahuje vsechny moduly/entity/fieldy/associace pro prochazeni modelu, factory pro repository/entity a mozna jeste neco.
Prace s ORM pak vypada zhruba v tomto duchu:
OrmModel::XMelvinUser objUser;
objUser->SetIp(strUserIpAdress);
objUser->SetCreatedAt(tmCreatedAt);
objUser->SetCountry(strCountry);
objUser->SetGeolocation(strGeolocation);
m_em.Persist(objUser);
m_em.Flush();
Pro ziskavani objekty z DB mame query builder, ktery umi data vratit bud jako array/scalar/objekt/pole objektu:
Atomix::Orm::CQueryBuilder qb = GetEntityManager().GetQueryBuilder();
qb.SelectEntity(_T("a"));
qb.FromEntity(_T("a"));
qb.GetConditions().AddFieldCondition(_T("a"), objParentAttribute->GetId(), Atomix::Orm::Operators::Equal);
qb.SetLimit(1);
qb.SetOffset(nPosition);
qb.AddOrderBy(_T("a"), true, Atomix::Orm::DataTypes::Integer);
OrmModel::XAttribute objResult = qb.CreateQuery().GetSingleResult();
Popsat detailne cely principu by bylo na nekolik clanku. Pokud by nekoho zajimalo vic, rad poradim. Ve zkratce jem ale chtel ukazat, ze s trochou sikovnosti, kombinace templates, maker a preprocesru lze implementovat ORM v c++ tak aby bylo efektivni na vyuziti i vykon ;-)
@8: Ono to jde urcite udelat mnohem mene zavisle na preprocesoru, ale pak je zase potreba vice psat kod rucne pripadne zkusit hacky typu mereni struktur v pameti atd, ale to vetsinou neni kompatabilni mezi ruznymi platformami, 32/64bit atd.
Kdysi jsme zkouseli mit vse v jedne tridu a do ni pomoci maker/sablon dodefinovat vse potrebne. Bohuzel se to po case zvrtlo v dost osklive monstrum, takze dalsim krokem bylo oddeleni tridy a base-tridy, kdy sme to stale psali rucne. Pak uz byl jen maly krok k napadu to generovat automaticky z nejakych XML definic. Od XML definic pak vedl napad k aplikaci na vytvoreni techto definic, cimz vznikl prvni prototyp ORM Designera. Kdyz to pak videl kolega co delal v PHP Propelu/Doctrine, zacli jsme resit zda to nemuze generovat XML/YML i pro nej. Odtud pak napad zkusit to verejne prodavat, cimz se dostavame do soucasnosti kdy je ORM Designer nase hlavni obziva ;-).
@11 Omyl, není to ani zdaleka triviální. Triviální je definovat lambda výraz, projít všechny objekty v databázi a onen výraz na nich vyhodnotit. Tomu bych ovšem neříkal databáze, instanciace objektů je drahá. Chceme logickým výrazem vyjádřit podmínku, ale místo procházení všech objektů samozřejmě musíme použít indexy databáze.
1.1. Tohle "db.save(obj);" v pripade ze bude vice typu objektu ukladanych do DB, predpoklada, ze implementuji nejaky spolecny interface a nebo dedi ze spolecneho predka. Ten iface by mohl obsahovat metodu GetSerializer () coz vrati pro dany typ objektu objekt odpovedny za prepis polozek z a do neceho co pouzivaji objekty zapisujici do DB. Asi se bude lisit pro jednotlive typy DB.
1.2. Pomoci sablon bez spolecneho predka bych zkusil neco takoveho
db.save(obj);
?
@6 No ono to asi jde udelat v C preprocesoru a templatach apod. Ale nevidim v tom moc velky rozdil. Vygenerovany kod je totiz velmi jednoduchy a jasny. Da se generovat i pro prekladace, ktere zatim 11ku nepodporuji apod. gsoap funguje bezvadne i pro stare C a prehlednost je uplne stejna. Zadna magicka makra apod. Lepsi nez vymyslet super chytre veci, ktere se pak budou kompilovat silenou dobu (napr. Spirit). Ja jsem takhle onehda transformoval nejake Java anotovane tridy. Proste staci pak mit jednu definici a pak si generovat co chces (Java, C, C++, PHP, Python, ...)
Osobně používám jen wrapper na mysql, takže přepisuju SQL příkazy do objektu Query. Výsledkem je vždy objekt Result, který se čte jako stream po řádcích. K fieldům se přistupuje přes operator[]. To úplně stačí. Cokoliv složitějšího se ukázalo jako přiliš komplikované a pomalé aby se tím vyvážil přínos.
Muj MySQL wrapper:
Transaction trn;
//... ziskani transakce
Result res = trn.SELECT("*").FROM(tabulka).WHERE("numField=%1").arg(20).exec();
while (res.hasItems()) {
Row rw = rew.getNext();
int col1 = rw[0].as();
const char *col2 = rw[1].as();
float col3 = rw["cena"].as();
//...zpracovani ...//
}
Uplne retezcum se nevyhnu.
Mozna by slo nejakym zpusobem vylepsit (pretizit) objekt Result/Row, aby se dalo predem deklarovat, jakeho typu jsou vracene vysledky. Tim bych se vyhnul tomu zapisu as();
Pak by Row fungoval jako plnohodnotny jeden radek.
@17 To neni pravda. Externi preprocesor proste vygeneruje k teto strukture:
struct Person { std::string name; };
napr. tento rozbalovac:
void unpack(Person &p, const RecordSet &s) { ..}
Pretezovani a templaty vse resi. Pouziti pak napr:
std::list records;
db.query("select ...").into(records);
Docela dobre se mi take pracuje s Poco::Data. Ne POCO jako Plain Old, ale Poco library pocoproject.org. Ten presne toto umi, ale musi se psat ty typove konvertory okolo. Nejjednodussi je stejne Poco::Tuple record;
Řešení popsané v tom PDF mi připadá jako utopie bez ohledu na jazyk (Java/C++). Jde mi o ten rozpor mezi procedurálním a deklarativním. Podmínka definovaná v metodě match() v predikátu je procedurální (mj. do ní můžu napsat libovolně složitý kód a volat kde co), což implikuje „full table scan“ – projdu všechny objekty/záznamy a na každém zavolám metodu a vyhodnotím true nebo false. Sice se tam píše, že by se bajtkód dal optimalizovat – ale detekovat v něm, že se volají ty správné metody, jsou spojeny pomocí správných logických operátorů a pak použít indexy… mi přijde hodně složité na implementaci a z hlediska uživatele příliš velké zatemnění a magie.
Přijde mi lepší používat DSL a ne psát všechno v jednom univerzálním jazyce – ono SQL/OQL (nebo podobný dotazovací jazyk) není jediný případ – totéž platí pro XPath, regulární výrazy atd. A věnovat se spíš frameworkům a IDE, které s tím umožní efektivně pracovat (kontrola/validace, napovídání, refaktoring).
[12] o tom to je a IMHO je otázka, zda je dobré používat tak mocný jazyk jako je C++ nebo Java, v jehož syntaxi jde zapsat cokoli, k popsání něčeho tak omezeného, jako je projekce, restrikce a případně nějaké to řazení a spojování. Preprocesor nebo běhové prostředí pak musí řešit nelehkou úlohu převodu „něčeho nekonečně složitého“ na „něco jednoduchého“ a nějak se vyrovnat s případy, kdy to převést nejde (resp. jde to vždycky, ale ne zrovna efektivně).
To je pohled implementátora – na druhé straně pohled uživatele takového frameworku: moc mě neláká představa, že do té „match()“ metody můžu napsat jakkoli složitý kód, teoreticky nejsem ničím omezen, ale zároveň nevím, co z toho nakonec vypadne a jestli a jak se to podaří zoptimalizovat.Z tohoto pohledu mi přijdou lepší ty různé *QL, které sice mají omezenou vyjadřovací schopnost, ale zase se dá líp odhadnout, jak dobře ten výsledek bude fungovat.
BTW: jak v C++ nebo Javě vyjádřit, zda v podmínce např. "obj.numField = nějakáFunkce()" se má nějakáFunkce() volat pro každý záznam nebo zda to lze optimalizovat tak, že se zavolá jen jednou a pak se opakovaně použije výsledek?
Také by bylo dobré mít nějakou obdobu parametrizovaných dotazů – kompilátor/proprocesor/framework by věděl, co je pevná část dotazu a co jsou proměnlivé parametry.
Chtělo by to jazykové prostředky pro vyznačení toho, co je a) neměnná část b) parametry nastavené před spuštěním dotazu c) volání funkce prováděné pro každý záznam.
Autor se zabývá vývojem kompilátorů a knihoven pro objektově-orientované programovací jazyky.
Přečteno 36 100×
Přečteno 25 284×
Přečteno 23 742×
Přečteno 20 119×
Přečteno 17 811×