In diesem Vortrag vom Symfony User Group Cologne Treffen in Köln zeige ich, wie man mit Hilfe von CSS-Selektoren und der CssSelector-Komponente des Symfony2 Frameworks Datenbankabfragen generieren kann. Diese Technik ermöglicht auch Laien einfache Abfragen von komplexen Datenbeständen effizient durchzuführen.
2013-09-12, sfugcgn: CSS-Selektoren für Datenbankabfragen nutzen
1. 1
C. Hetzel, 12. Sept. 2013
Datenbankabfragen über
CSS-Selektoren
Unter Verwendung der CssSelector-Komponente
von Symfony2 Datenbankabfragen gestalten.
Von Carsten Hetzel.
2. 2 C. Hetzel, 12. Sept. 2013
Zur Person
Carsten Hetzel
Seit 2000 in der IT als
Softwareengineer tätig, seit 10
Jahren als Freelancer
Likes: Softwaredesign, SF2
(ach was), BDD mit Behat,
Oracle-Datenbanken
Hobbies: Joggen, Reiten,
Klavierspielen
http://www.coding-inquiries.de
3. 3 C. Hetzel, 12. Sept. 2013
Die Anforderung
Kundenfreundliche Abfragen
großer, strukturierter Datenmengen
4. 4 C. Hetzel, 12. Sept. 2013
Die Anforderung
Für einen Kunden war in einer Browser basierten
Software eine Suchfunktion bereitzustellen, die
beliebigeObjekte einer Objekthierarchie abfragen
und darstellen können sollte.
Probleme:
- Ca. 1 Mio. Objekte.
- Ca. 60 Objekttypen.
- Ca. 16 Mio. Parameter.- Die Suchanfragen mussten für
Laien möglich sein.
5. 5 C. Hetzel, 12. Sept. 2013
Die Anforderung
Beispiel:
Wir wollen alle Objekte X angezeigt bekommen,
die irgendwo unterhalb eines Objekts A hängen
bei dem der Parameter "a" den Wert 10 hat.
6. 6 C. Hetzel, 12. Sept. 2013
Die Idee:
CSS-Selektoren
CSS-Selektoren
7. 7 C. Hetzel, 12. Sept. 2013
Aufbau von CSS-Selektoren
* -> Alle Elemente.
E -> Alle Elemente des Typs "E"
E[foo] -> Alle Elemente des Typs "E", die ein Attribut "foo" besitzen.
E[foo="bar"] -> Alle Elemente des Typs "E", deren Attribut "foo" den Wert "bar" hat.
E#10 -> Alle Elemente des Typs "E", deren Attribut "ID" den Wert 10 hat (entspricht
E[id=10]).
E.myClass -> Alle Elemente des Typs "E", deren Attribut "class" mindestens "myClass"
enthält (entspricht E[class~="myClass"]).
E F -> Alle Elemente des Typs "F", die Nachkommen von "E"-Elementen sind, sich also
irgendwo unterhalb eines E befinden.
E > F -> Alle Elemente des Typs "F", die ein direktes Kind von einem "E"-Element sind.
E, F -> Alle Elemente des Typs "E" und "F".
Siehe: http://www.w3schools.com/cssref/css_selectors.asp oder
http://www.w3.org/TR/2009/CR-CSS2-20090908/selector.html
8. 8 C. Hetzel, 12. Sept. 2013
Die Idee: CSS-Selektoren
Um bestimmte Elemente in einem HTML-Dokument auszuwählen und zu bearbeiten
werden sogenannte CSS-Selektoren verwendet.
Ein solcher Selektor beschreibt den Pfad und die Eigenschaften der Elemente, welche
ausgewählt und durch Zuweisung von Eigenschaftswerten verändert werden sollen.
CSS-Selektoren sind eine einfache Notation für die Auswahl
von Elementen in einem HTML-Dokument.
Ein HTML-Dokument ist eine konkrete hierarchische
Datenstruktur.
Schlussfolgerung: CSS-Selektoren können für beliebige
hierarchische Datenstrukturen verwendet werden.
9. 9 C. Hetzel, 12. Sept. 2013
Die Idee: CSS-Selektoren
Was könnte problematisch sein?
Datenbankmodelle sind nicht notwendigerweise hierarchisch.
Es können nicht (einfach) alle Abfragen, die über SQL
möglich sind, über CSS-Selektoren abgebildet werden.
Beispiel: Selektiere alle Äpfel, die schwerer sind, als die
schwerste Birne.
Oder doch(?): Apfel[gewicht>Birne:max(gewicht)]
10. 10 C. Hetzel, 12. Sept. 2013
Hierarchische Datenstrukturen
und Datenmodelle
und Datenmodelle
Ziel ist, Datenbankabfragen über die Formulierung von Selektoren zu generieren.
Beispiele:
Author
select * from author
Author#1
select * from author where id = 1
Author[lastname=Martin]
select * from author where lastname = 'Martin'
Author[lastname=Martin] Books
select b.* from author a join books b on b.author_id = a.id where a.lastname = 'Martin'
Books[title^=Clean] Author
select a.* from books b join author a on a.id = b.author_id where b.title like 'Clean%'
11. 11 C. Hetzel, 12. Sept. 2013
Hierarchische Datenstrukturen
und Datenmodelle
und Datenmodelle
Wie den einzelnen Beispielen zu entnehmen ist, können Referenzen
in Datenbankmodellen in beiden Richtungen genutzt werden, selbst
wenn die Entitäten, wie in diesem vereinfachten Beispiel, nicht über
eine 1:N-, sondern über eine N:M-Relation verbunden sind. Die
"logische" Hierarchie ergibt sich durch die Wahl der
Selektoren.ACHTUNG: Die "natürliche" Verknüpfung von zwei
Tabellen findet über ihre gegenseitigen Referenzen statt. Trotz dieser
Einschränkung kann es zu großen Ergebnismengen und mehreren
gleichen Ergebnisdatensätzen kommen!
12. 12 C. Hetzel, 12. Sept. 2013
Die Komponente CssSelector
CssSelector stellt eine
statische Hilfsfunktion zur
Verfügung. Diese baut
lediglich einen Translator
zusammen, der die
eigentliche Arbeit der
Konvertierung des CSS-
Ausdrucks in einen
XPath-Ausdruck
durchführt.
class CssSelector{ public static function toXPath($cssExpr, $prefix =
'descendant-or-self::') { $translator = new Translator(); if (self::
$html) { $translator->registerExtension(new
HtmlExtension($translator)); } $translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser()) ; return $translator-
>cssToXPath($cssExpr, $prefix); } ...}
class CssSelector{ public static function toXPath($cssExpr, $prefix =
'descendant-or-self::') { $translator = new Translator(); if (self::
$html) { $translator->registerExtension(new
HtmlExtension($translator)); } $translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser()) ; return $translator-
>cssToXPath($cssExpr, $prefix); } ...}
13. 13 C. Hetzel, 12. Sept. 2013
Die Komponente CssSelector
Da die Klasse CssSelector primär zur Konvertierung von CSS-
Selektoren zu XPath-Ausdrücken gedacht ist, stelltsie eine
entsprechende Objekthierarchie zusammen. Sie agiert damit als
Fassade zum dahinterliegenden, komplexen Objektstruktur.Der
Translator ist dementsprechend ein XPath-Translator und
verwendet für seine Arbeit die dazugehörigenExtensions und den
eigentlichen Parser.
14. 14 C. Hetzel, 12. Sept. 2013
Intermezzo!
Never use „new“
(unless you‘re supposed to)
15. 15 C. Hetzel, 12. Sept. 2013
Never use „new“
(unless you‘re supposed to)
(unless you‘re supposed to)
Hilft auch ohne Kenntnisse von OO-Prinzipien
und Entwurfsmustern bessere Lösungen
hervor zu bringen
Erzwingt Dependency Injection
Hilft in Komponenten zu denken
Fördert flexibilität und Testbarkeit von Code
Statische Methoden sind KEIN Ersatz!
16. 16 C. Hetzel, 12. Sept. 2013
Die Komponente CssSelector
Da CssSelector als
Fassade dient und die
Komplexität der
interagierenden Objekte
versteckt, ist die
Verwendung von „new“
hier akzeptabel.
Insgesamt ist die
Implementierung nicht
gut, weil sie keine
Einflussnahme auf den
Parsingprozess und die
Konfiguration der Objekte
erlaubt.
class CssSelector{ public static function toXPath($cssExpr, $prefix =
'descendant-or-self::') { $translator = new Translator(); if (self::
$html) { $translator->registerExtension(new
HtmlExtension($translator)); } $translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser()) ; return $translator-
>cssToXPath($cssExpr, $prefix); } ...}
class CssSelector{ public static function toXPath($cssExpr, $prefix =
'descendant-or-self::') { $translator = new Translator(); if (self::
$html) { $translator->registerExtension(new
HtmlExtension($translator)); } $translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser()) ; return $translator-
>cssToXPath($cssExpr, $prefix); } ...}
17. 17 C. Hetzel, 12. Sept. 2013
Die Komponente CssSelector
Probleme, die sich aus der gegebenen Implementierung der
CssSelector-Komponente ergeben:
CssSelector::toXPath() erzeugt eine direkte Abhängigkeit im
Client-Code.
Jeder Aufruf von CssSelector::toXPath() erzeugt alle
verwendeten Objekte neu.
CssSelector ist weder wiederverwendbar, noch erweiterbar.
Aber: toXPath() macht was es soll. ;-)
18. 18 C. Hetzel, 12. Sept. 2013
Die Komponente XPathTranslator
Offensichtlich ist die Komponente XPathTranslator diejenige, die
die eigentliche Arbeit macht.
Verwendet einen Parser, um den CSS-Selektor in eine
Node-Struktur umzuwandeln.
Benutzt Extensions um die verschiedenen NodeTypen
(ElementNode, AttributeNode etc.) in einen XPath-Ausdruck
umzuwandeln.
19. 19 C. Hetzel, 12. Sept. 2013
Der Parser
Der Parser zerteilt über
einen Tokenizer
zunächst den CSS-
Selektor in einzelne
Tokens.
Die Tokens werden
anschließend in
entsprechende Node-
Instanzen umgewandelt
und zurück geliefert.
20. 20 C. Hetzel, 12. Sept. 2013
Die Node-Klassen
Jeder Bestandteil eines
CSS-Selektors wird
durch eine
gleichlautende Klasse
repräsentiert.
Der Parser wandelt also
den Selektor in eine
Objekthierarchie um und
erzeugt dazu die
jeweiligen Instanzen.
21. 21 C. Hetzel, 12. Sept. 2013
XPathTranslator im Detail
Auch hier wird ähnlich wie
bei der XssSelector-
Klasse eine Reihe von
zusätzlichen Hilfsklassen
instanziiert, um die
Verarbeitung in separate
Aufgabenbereiche
aufzutrennen.
Zusätzlilch zur
Konfiguration durch
CssSelector nimmt also
auch noch Translator
selber seine Konfiguration
in die Hand.
class Translator implements TranslatorInterface{ ... public function
__construct(ParserInterface $parser = null) { $this->mainParser =
$parser ?: new Parser(); $this ->registerExtension(new
ExtensionNodeExtension($this)) ->registerExtension(new
ExtensionCombinationExtension()) ->registerExtension(new
ExtensionFunctionExtension()) ->registerExtension(new
ExtensionPseudoClassExtension()) ->registerExtension(new
ExtensionAttributeMatchingExtension()) ; } ...}
class Translator implements TranslatorInterface{ ... public function
__construct(ParserInterface $parser = null) { $this->mainParser =
$parser ?: new Parser(); $this ->registerExtension(new
ExtensionNodeExtension($this)) ->registerExtension(new
ExtensionCombinationExtension()) ->registerExtension(new
ExtensionFunctionExtension()) ->registerExtension(new
ExtensionPseudoClassExtension()) ->registerExtension(new
ExtensionAttributeMatchingExtension()) ; } ...}
22. 22 C. Hetzel, 12. Sept. 2013
XPathTranslator im Detail
In cssToXPath() wird der
Selektor zunächst in
Node-Instanzen
(SelectorNode) und
anschließend über
selectorToXPath() in
einen XPath-Ausdruck
umgewandelt.
class Translator implements TranslatorInterface{ ... public function
cssToXPath($cssExpr, $prefix = 'descendant-or-self::') { $selectors =
$this->parseSelectors($cssExpr); /** @var SelectorNode $selector */
foreach ($selectors as $selector) { if (null !== $selector-
>getPseudoElement()) { throw new
ExpressionErrorException('Pseudo-elements are not supported.'); }
} $translator = $this; return implode(' | ', array_map(function
(SelectorNode $selector) use ($translator, $prefix) { return
$translator->selectorToXPath($selector, $prefix); },
$selectors)); } ...}
class Translator implements TranslatorInterface{ ... public function
cssToXPath($cssExpr, $prefix = 'descendant-or-self::') { $selectors =
$this->parseSelectors($cssExpr); /** @var SelectorNode $selector */
foreach ($selectors as $selector) { if (null !== $selector-
>getPseudoElement()) { throw new
ExpressionErrorException('Pseudo-elements are not supported.'); }
} $translator = $this; return implode(' | ', array_map(function
(SelectorNode $selector) use ($translator, $prefix) { return
$translator->selectorToXPath($selector, $prefix); },
$selectors)); } ...}
23. 23 C. Hetzel, 12. Sept. 2013
Die Extension-Klassen
Jede Extension-Klasse
ist dafür zuständig, die
für sie relevanten Node-
Instanzen in einen
XPath-Ausdruck
umzuwnadeln.
Dabei werden vom
Translator die dazu
registrierten Methoden
aufgerufen.
24. 24 C. Hetzel, 12. Sept. 2013
XPathExtensions im Detail
Der Vorgang lässt sich
anschaulich an der
Umsetzung der
„HashNode“ zeigen: Ein
Ausdruck wie „Author#1“
soll das Element mit der
ID „1“.
Die HashNode hat eine
SelectorNode und die
gewünschte ID. Beide
Bestandteile werden vom
Translator in den finalen
XPath-Ausdruck
konvertiert.
class NodeExtension extends AbstractExtension{ ... public function
translateHash(NodeHashNode $node) { $xpath = $this->translator-
>nodeToXPath($node->getSelector()); return $this->translator-
>addAttributeMatching($xpath, '=', '@id', $node->getId()); } ...}
class NodeExtension extends AbstractExtension{ ... public function
translateHash(NodeHashNode $node) { $xpath = $this->translator-
>nodeToXPath($node->getSelector()); return $this->translator-
>addAttributeMatching($xpath, '=', '@id', $node->getId()); } ...}
class AttributeMatchingExtension extends AbstractExtension{ ... public
function translateEquals(XPathExpr $xpath, $attribute, $value)
{ return $xpath->addCondition(sprintf('%s = %s', $attribute,
Translator::getXpathLiteral($value))); } ...}
class AttributeMatchingExtension extends AbstractExtension{ ... public
function translateEquals(XPathExpr $xpath, $attribute, $value)
{ return $xpath->addCondition(sprintf('%s = %s', $attribute,
Translator::getXpathLiteral($value))); } ...}
25. 25 C. Hetzel, 12. Sept. 2013
CSS-Selektoren für
Datenbankstatements
Als Datenbankschicht wurde das ORM Propel
verwendet - es lassen sich aber auch andere
ORMs oder DBALs wie z.B. Doctrine
verwenden.
Die Realisierung des Translators wurde an
XPathTranslator angelehnt.
Das Datenmodell besteht aus Authoren und
Büchern mit einer 1:N-Beziehung.
26. 26 C. Hetzel, 12. Sept. 2013
Die Implementierung auf
einen Blick
Es wurde eine einfache Symfony2
Anwendung generiert mit Controllern
zu Book und Author.
Der DbQueryTranslator wandelt den
CSS-Selektor in ein ModelCriteria-
Objekt um - entweder vom Typ
AuthorQuery oder BookQuery.
Die Extensions sind dafür zuständig
die API der Propel-Query-Klassen
anzusprechen.
27. 27 C. Hetzel, 12. Sept. 2013
DbQueryTranslator im Detail
Der DbQueryTranslator
orientiert sich am
XPathTranslator (und macht die
gleichen Fehler).
Damit die NodeExtension die
richtigen Model-Klassen finden
kann, benötigt sie deren
Namespace.
Der Rest funktioniert analog:
Parsen des Selektors und
Umwandeln der Nodes in ein
ModelCriteria (bzw.
AuthorQuery oder BookQuery).
class Translator{ public function __construct( ParserInterface $parser =
null ) { $this->mainParser = $parser ? : new Parser();
$modelNS = 'AppDbModelSelectorBundleModel';
$this->registerExtension(
new ExtensionNodeExtension( $this, null,
$modelNS ) ) ->registerExtension(
new ExtensionCombinationExtension() )
->registerExtension(
new ExtensionAttributeMatchingExtension() );
} public function cssToDbQuery( $cssExpr ) {
$selectors = $this->parseSelectors( $cssExpr );
$query = null; foreach( $selectors as $selector ){
$query = $this->selectorToDbQuery( $selector, $query );
} return $query; }
...}
class Translator{ public function __construct( ParserInterface $parser =
null ) { $this->mainParser = $parser ? : new Parser();
$modelNS = 'AppDbModelSelectorBundleModel';
$this->registerExtension(
new ExtensionNodeExtension( $this, null,
$modelNS ) ) ->registerExtension(
new ExtensionCombinationExtension() )
->registerExtension(
new ExtensionAttributeMatchingExtension() );
} public function cssToDbQuery( $cssExpr ) {
$selectors = $this->parseSelectors( $cssExpr );
$query = null; foreach( $selectors as $selector ){
$query = $this->selectorToDbQuery( $selector, $query );
} return $query; }
...}
28. 28 C. Hetzel, 12. Sept. 2013
Extensions im Detail
Basis für alle weiteren
Operationen ist ein Query-
Objekt des gewünschten
Models.
Dazu wird der Name der
ElementNode ermittelt und im
Namespace der Models geprüft,
ob es eine passende Klasse
gibt, also AuthorQuery oder
BookQuery gesucht.
Query-Klassen werden bei
Propel über die Statische
„create()“-Methode erzeugt.
class NodeExtension extends AbstractExtension{ public function
translateElement( NodeElementNode $node ) {
$element = $node->getElement(); $queryClassName = $this-
>modelNamespace . $element . 'Query'; if( !
class_exists( $queryClassName ) ) throw new
RuntimeException( sprintf( 'Model %s not supported!', $queryClassName )
); return $queryClassName::create(); }
...}
class NodeExtension extends AbstractExtension{ public function
translateElement( NodeElementNode $node ) {
$element = $node->getElement(); $queryClassName = $this-
>modelNamespace . $element . 'Query'; if( !
class_exists( $queryClassName ) ) throw new
RuntimeException( sprintf( 'Model %s not supported!', $queryClassName )
); return $queryClassName::create(); }
...}
29. 29 C. Hetzel, 12. Sept. 2013
Extensions im Detail
Hier als Beispiel noch die
Implementierung für
AttributeNodes, mit denen
where-Bedingungen für die
Datenbank erzeugt werden.
Die SelectorNode kapselt dabei
wieder den Zugriff auf die
Datenbanktabelle, die
AttributeNode die Bedingungen
für einzelne Spalten der Tabelle.
class NodeExtension extends AbstractExtension{
... public function translateAttribute( NodeAttributeNode
$node ) { $attribute = $node->getAttribute();
$operator = $node->getOperator(); $value = $node->getValue();
$query = $this->translator-
>nodeToDbQuery( $node->getSelector() ); return $this-
>translator->addAttributeMatching(
$query,
$operator,
$attribute,
$value ); } ...}
class NodeExtension extends AbstractExtension{
... public function translateAttribute( NodeAttributeNode
$node ) { $attribute = $node->getAttribute();
$operator = $node->getOperator(); $value = $node->getValue();
$query = $this->translator-
>nodeToDbQuery( $node->getSelector() ); return $this-
>translator->addAttributeMatching(
$query,
$operator,
$attribute,
$value ); } ...}
30. 30 C. Hetzel, 12. Sept. 2013
Die Anwendung in
Bildern
Über den CssSelector
„Author“ werden alle
Authoren angezeigt.
Er kann alternativ
auch weggelassen
werden.
In den Beispieldaten
hat Robert Martin zwei
Bücher und Martin
Fowler eines.
31. 31 C. Hetzel, 12. Sept. 2013
Die Anwendung in
Bildern
Durch den Join mit Books
wird somit Robert Martin
zweimal angezeigt.
Der Translator wäre
nützlicherweise so
anzupassen, dass er
einen Pseudoknoten
„distinct“ erlaubt, um
diesen Effekt zu
vermeidenn.
32. 32 C. Hetzel, 12. Sept. 2013
Die Anwendung in
Bildern
Wie man dem Profiler entnehmen kann, werden die korrekten
Datenbankabfragen generiert.
Der Selector „Book[title*=Coder] Author[Lastname=Martin]“ wurde
umgesetzt.
33. 33 C. Hetzel, 12. Sept. 2013
Ausblick
Zusätzliche Funktionalitäten wie „distinct“ oder
Aggregatfunktionen sind denkbar.
Virtuelle Attribute wie „Book.count()“ können
realisiert werden.
Bei bestimmten Zugriffen können Hints für den
Ausführungsplan ergänzt werden.
...
35. 35 C. Hetzel, 12. Sept. 2013
Vielen Dank für Ihre
Aufmerksamkeit!
Hinweis der Redaktion
Hinweis: Dieses Problem ist einfach, weil es von einer festen Hierarchie ausgeht! Hinweis: Die Auswahl der darzustellenden Objekte kann nicht PHP-seitig stattfinden, denn dafür ist die Datenmenge zu groß.
Vereinfachung: Author und Buch haben eine 1:N-Relation Was fällt am letzten Statement auf? Es kann der selbe Author mehrfach in der Ergebnismenge auftreten! Hier wäre ein „distinct“ nötig, etwa: Books[title^=Clean] Author.distinct()
Hinweise: - Die XPath\Translator-Komponente bietet z.B. über den Konstruktor einen anderen Parser zu verwenden. Dem Anwender von CssSelector wird aber keine Möglichkeit geboten einen anderen Parser zu injizieren! - Auf diese Weise kann weder in den Parsing-Prozess eingegriffen, noch dezidiert auf Ereignisse reagiert werden!
Hinweise: - Die XPath\Translator-Komponente bietet z.B. über den Konstruktor einen anderen Parser zu verwenden. Dem Anwender von CssSelector wird aber keine Möglichkeit geboten einen anderen Parser zu injizieren! - Auf diese Weise kann weder in den Parsing-Prozess eingegriffen, noch dezidiert auf Ereignisse reagiert werden!