2013-09-12, sfugcgn: css-selektoren für datenbankabfragen nutzen
DESCRIPTION
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.TRANSCRIPT
1 C. Hetzel, 12. Sept. 2013
Datenbankabfragen über CSS-Selektoren
Unter Verwendung der CssSelector-Komponente von Symfony2 Datenbankabfragen gestalten.
Von Carsten Hetzel.
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 C. Hetzel, 12. Sept. 2013
Die Anforderung
Kundenfreundliche Abfragengroßer, strukturierter
Datenmengen
Kundenfreundliche Abfragengroßer, strukturierter
Datenmengen
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 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 C. Hetzel, 12. Sept. 2013
Die Idee:CSS-Selektoren
Die Idee:CSS-Selektoren
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 C. Hetzel, 12. Sept. 2013
Die Idee: CSS-SelektorenUm 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 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
C. Hetzel, 12. Sept. 2013
Hierarchische Datenstrukturenund 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
C. Hetzel, 12. Sept. 2013
Hierarchische Datenstrukturenund 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
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
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
C. Hetzel, 12. Sept. 2013
Intermezzo!
Never use „new“(unless you‘re supposed to)
Never use „new“(unless you‘re supposed to)
15
C. Hetzel, 12. Sept. 2013
Never use „new“(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
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
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
C. Hetzel, 12. Sept. 2013
Die Komponente XPath\Translator
Offensichtlich ist die Komponente XPath\Translator 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
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
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
C. Hetzel, 12. Sept. 2013
XPath\Translator im DetailAuch 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 Extension\NodeExtension($this)) ->registerExtension(new Extension\CombinationExtension()) ->registerExtension(new Extension\FunctionExtension()) ->registerExtension(new Extension\PseudoClassExtension()) ->registerExtension(new Extension\AttributeMatchingExtension()) ; } ...}
class Translator implements TranslatorInterface{ ... public function __construct(ParserInterface $parser = null) { $this->mainParser = $parser ?: new Parser(); $this ->registerExtension(new Extension\NodeExtension($this)) ->registerExtension(new Extension\CombinationExtension()) ->registerExtension(new Extension\FunctionExtension()) ->registerExtension(new Extension\PseudoClassExtension()) ->registerExtension(new Extension\AttributeMatchingExtension()) ; } ...}
22
C. Hetzel, 12. Sept. 2013
XPath\Translator 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
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
C. Hetzel, 12. Sept. 2013
XPath\Extensions im DetailDer 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(Node\HashNode $node) { $xpath = $this->translator->nodeToXPath($node->getSelector()); return $this->translator->addAttributeMatching($xpath, '=', '@id', $node->getId()); } ...}
class NodeExtension extends AbstractExtension{ ... public function translateHash(Node\HashNode $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
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 XPath\Translator angelehnt.
Das Datenmodell besteht aus Authoren und Büchern mit einer 1:N-Beziehung.
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 DbQuery\Translator 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
C. Hetzel, 12. Sept. 2013
DbQuery\Translator im Detail
Der DbQuery\Translator orientiert sich am XPath\Translator (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 = '\\App\\DbModelSelectorBundle\\Model\\';$this->registerExtension(
new Extension\NodeExtension( $this, null, $modelNS ) ) ->registerExtension(
new Extension\CombinationExtension() )->registerExtension(
new Extension\AttributeMatchingExtension() );} 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 = '\\App\\DbModelSelectorBundle\\Model\\';$this->registerExtension(
new Extension\NodeExtension( $this, null, $modelNS ) ) ->registerExtension(
new Extension\CombinationExtension() )->registerExtension(
new Extension\AttributeMatchingExtension() );} public function cssToDbQuery( $cssExpr ) {$selectors = $this->parseSelectors( $cssExpr );
$query = null; foreach( $selectors as $selector ){$query = $this->selectorToDbQuery( $selector, $query );} return $query; }...}
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( Node\ElementNode $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( Node\ElementNode $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
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( Node\AttributeNode
$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( Node\AttributeNode
$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
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
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
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
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.
...
34
C. Hetzel, 12. Sept. 2013
Fragen? ;-)
35
C. Hetzel, 12. Sept. 2013
Vielen Dank für Ihre Aufmerksamkeit!
Vielen Dank für Ihre Aufmerksamkeit!