Persistence / ORM
n2n Persistence bildet die Schnittstelle zur Datenbank und bietet eine umfangreiche ORM (object-relational mapping) API, welche die Spezifikation 2.1 der JPA (Java Persistence API) nahezu vollständig übernimmt. Ein ORM bietet dir eine objektorientierte Abstraktionsebene der Datenbank. Die Basis aller Datenbank-Verbindungen in n2n bildet PDO (PHP Data Objects).
Persistence Unit
In der app.ini
under der Gruppe [database]
kannst du Datenbank-Verbindungen (Persistence Units) für deine Applikation registrieren. Das Schema für die Eigenschaften einer Verbindung ist {persistence_unit_name}.{property}
.
[database : development] default.dsn_uri = "mysql:server=localhost;dbname=testdb" default.user = "root" default.password = "" default.transaction_isolation_level = "SERIALIZABLE" default.dialect = "n2n\persistence\meta\dialect\mysql\MysqlDialect" [database : live] default.dsn_uri = "mysql:server=localhost;dbname=livedb" default.user = "liveuser" default.password = "livepw" default.transaction_isolation_level = "SERIALIZABLE" default.dialect = "n2n\persistence\meta\dialect\mysql\MysqlDialect"
In diesem Beispiel registrieren wir eine Persistence Unit mit Namen "default" für den Development- und Live-Modus. Die Eigenschaft {persistence_unit_name}.dsn_uri
entspricht dem Parameter $dsn
von PDO::__construct()
. Über {persistence_unit_name}.transaction_isolation_level
wählst du das Isolations-Level, das für Transaktionen dieser Persistence Unit gelten soll. Als Werte sind "READ UNCOMMITTED"
, "READ COMMITTED"
, "REPEATABLE READ"
und "SERIALIZABLE"
möglich. Der Standard-Wert ist "SERIALIZABLE"
.
Mit {persistence_unit_name}.dialect
definierst du den, zur Datenbank passenden, Dialect
. Der Dialect
bildet eine Abstraktions-Ebene, über welche Meta-Informationen zur Datenbank einheitlich abgefragt werden können. Diese werden sowohl von der ORM API wie auch der Meta API benötigt.
n2n bietet standardmässig folgende Implementationen von Dialect
.
Datenbank | Dialect Klasse |
---|---|
MySQL | n2n\persistence\meta\dialect\mysql\MysqlDialect |
PostgreSQL | n2n\persistence\meta\dialect\pgsql\PgsqlDialect |
SQLite | n2n\persistence\meta\dialect\sqlite\SqliteDialect |
Microsoft SQL | n2n\persistence\meta\dialect\mssql\MssqlDialect |
Oracle | n2n\persistence\meta\dialect\oracle\OracleDialect |
PDO-Objekt anfordern
Wie bereits erwähnt werden Datenbank-Verbindungen in n2n über PDO realisiert. In magischen Methoden kannst du dir ein n2n\persistence\Pdo
-Objekt der Persistence Unit "default" übergeben lassen.
private function _init(Pdo $pdo) { $this->pdo = $pdo; }
n2n\persistence\Pdo
-Objekte anderer Persistence Units musst du über den n2n\persistence\PdoPool
anfordern.
private function _init(DbhPool $dbhPool) { $this->pdo = $dbhPool->getPdo('db2'); }
In diesem Beispiel fordern wir ein n2n\persistence\Pdo
-Objekt der Persistence Unit mit dem Namen "db2" an.
n2n\persistence\Pdo
erweitert PDO
und bietet zusätzliche Methoden.Entity
Eine Entity ist eine PHP-Klasse, die auf eine einzelne Tabelle in einer relationalen Datenbank abgebildet wird. Instanzen dieser Klasse entsprechen hierbei den Zeilen der Tabelle und die Eigenschaften den Spalten. Entities sind der Kern des ORM's und bilden normalerweise die Business-Logik deiner Applikation.
namespace atusch\bo; use n2n\reflection\ObjectAdapter; class Article extends ObjectAdapter { const TYPE_NEWS = 'news'; const TYPE_COMMENT = 'comment'; private $id; private $title; private $text; private $type = self::TYPE_NEWS; private $online = false; public function __construct($title) { $this->title = $title; } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getTitle() { return $this->title; } public function setTitle($title) { $this->title = $title; } public function getText() { return $this->text; } public function setText($text) { $this->text = $text; } public function getType() { return $this->type; } public function setType($type) { $this->type = $type; } public function isOnline() { return $this->online; } public function setOnline($online) { $this->online = $online; } }
Es werden alle Eigenschaften unabhängig von ihrer Sichtbarkeit erkannt. Du kannst aber einzelne Eigenschaften ignorieren lassen, indem du sie mit n2n\persistence\orm\annotation\AnnoTransient
annotierst. Wir empfehlen dir Entities zu erstellen, die den n2n\reflection\ObjectAdapter
erweitern. Der ObjectAdapter
implementiert die statische Methode ObjectAdapter::getClass()
, die dich beim Arbeiten mit dem EntityManager unterstützt.
Für die Entity Article
im oben stehenden Beispiel wäre folgende Tabelle denkbar:
CREATE TABLE `article` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(50) DEFAULT NULL, `text` TEXT, `type` ENUM('news','comment') DEFAULT 'news', `online` TINYINT(4) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Alle Entity-Klassen müssen in der app.ini
unter der Gruppe [orm]
registriert werden.
[orm] entities[] = "atusch\bo\Article" entities[] = "atusch\bo\User"
EntityManager
Über den EntityManager
kannst du Entities in die Datenbank speichern (persistieren) und auch wieder abfragen oder löschen. Der EntityManager
für die Persistence Unit "default" lässt sich in allen magischen Methoden übergeben. Du kannst zum Beispiel ein Lookupable ArticleDao
implementieren, das den EntityManager
über _init()
holt und verschiedene Methoden zum Lesen und Verwalten von Aricle
-Entities zur Verfügung stellt.
class ArticleDao implements RequestScoped { private $em; private function _init(EntityManager $em) { $this->em = $em; } /** * @param int $id * @return atusch\bo\Article */ public function getArticleById($id) { return $this->em->find(Article::getClass(), $id); } public function saveArticle(Article $article) { $this->em->persist($article); } }
Dieses ArticleDao
steht nun in jeder magischen Methode zur Verfügung und kann zum Beispiel in einem ArticleController
verwendet werden.
public function doArticle(ArticleDao $articleDao, $id) { $article = $articleDao->getArticleById($id); if ($article === null) { throw new PageNotFoundException(); } $this->forward('view\article.html', array('article' => $article)); }
EntityManager
für andere Persistence Units kannst du, wie bei den Pdo
-Objekten, über den PdoPool
anfordern.
private function _init(PdoPool $pdoPool) { $this->em = $pdoPool->getEntityManagerFactory("db2")->getExtended(); }
In diesem Beispiel holen wir zuerst die EntityManagerFactory
der Persistence Unit "db2" und fordern dann über EntityManagerFactory::getExtended()
einen EntityManager
für diese Persistence Unit an.
EntityManagerFactory::getExtended()
forderst du einen "extended" EntityManager
an, der mit der ganzen Applikation geteilt wird. Du kannst über EntityManagerFactory::create()
auch einen neuen erstellen oder über EntityManagerFactory::getTransactional()
einen EntityManager
anfordern, der nur für die aktuelle Transaktion gültig ist. In den Artikeln Lifecycle und Transaktionen kannst du mehr über dieses Konzept erfahren..Abfragen
Wie im oben stehenden Beispiel ersichtlich, kannst du über EntityManager::find()
eine einzelne Entity über ihren Primary Key abfragen.
/** * @param int $id * @return atusch\bo\Article */ public function getArticleById($id) { return $this->em->find(Article::getClass(), $id); }
find()
erwartet als ersten Parameter ein ReflectionClass
-Objekt der abzufragenden Entity. Erweitert die gewünschte Entity n2n\persistence\orm\ObjectAdapter
, kannst du auf das jeweilige ReflectionClass-Objekt über ObjectAdapter::getClass()
zugreifen.
Möchtest du komplexere Abfragen ausführen, kannst du wahlweise auf die Criteria API oder NQL (n2n Query Language) zurückgreifen.
Criteria API
Ein Criteria
-Objekt beschreibt eine Abfrage. Es beschreibt, was, unter welchen Bedingungen und in welcher Reihenfolge selektiert werden soll. Die Beschreibungen beziehen sich dabei nicht auf Datenbanktabellen und Spalten, sondern auf Entities und ihre Eigenschaften. Mit Criteria::toQuery()
lässt sich ein Query
-Objekt bilden, über welches die Abfrage ausgeführt und die jeweiligen Daten ausgelesen werden können.
Um ein einfaches Criteria
zusammenzustellen, das die Abfrage eines einzelnen Entity-Typs unter einfachen Vergleichsbedingungen beschreibt, eignet sich EntityManager::createSimpleCriteria()
.
/** * @param string $type * @param int $limit * @param int $num * @return Article[] */ public function getArticlesByType($type, $limit, $num) { $criteria = $this->em->createSimpleCriteria(Article::getClass(), array('online' => true, 'type' => $type), array('id' => 'DESC'), $limit, $num); return $criteria->toQuery()->fetchArray(); }
createSimpleCriteria()
erwartet als ersten Parameter die ReflectionClass
der Entity, die du abfragen möchtest (beeinflusst den SELECT
- und FROM
-Abschnitt im SQL-Statement).
Über den zweiten Parameter kannst du einfache Vergleichsbedingungen in der Form Eigenschaft = Wert bestimmen, wobei als Array-Schlüssel der Name der Eigenschaft und als Array-Wert der Vergleichswert erwartet (beeinflusst den WHERE
-Abschnitt im SQL-Statement).
Über den dritten Parameter definierst du die Sortierreihenfolge (beeinflusst den ORDER BY
-Abschnitt im SQL-Statement). Über die letzten beiden Parameter kannst du definieren, wieviele Article-Objekte maximal ausgeliefert werden sollen (beeinflusst LIMIT
-Abschnitt im SQL-Statement).
Sobald du das Criteria in ein Query
umgewandelt hast, kannst du mit Query::fetchArray()
die Abfrage ausführen und das Resulat als array
zurückgeben lassen. Im oben stehenden Beispiel gibt Query::fetchArray()
ein array
von Article
-Objekten zurück. ArticleDao::getArticlesByType()
könntest du also mit $articleDao->getArticlesByType('news', 0, 30)
aufrufen, um die ersten 30 Artikel umgekehrt sortiert nach Article::$id
abzufragen, bei denen Article::$online
gleich true
und Article::$type
gleich "news" ist.
Erwartest du als Resultat nur eine einzelne Zeile, kannst du die Abfrage auch mit Query::fetchSingle()
ausführen. Query::fetchSingle()
gibt dir nur eine einzelne Zeile zurück, während Query::fetchArray()
ein array
mit einer Zeile zurückgeben würde.
/** * @param string $userName * @return User */ public function getUserByUserName($userName) { return $this->em->createSimpleCriteria(User::getClass(), array('userName' => $userName)) ->toQuery()->fetchSingle(); }
In diesem Beispiel gibt fetchSingle()
den User mit dem übergebenen Benutzername zurück. Ist das Resulat leer, wird null
zurückgegeben. Umfasst das Resultat mehrere Zeilen, wird eine QueryConflictException
geworfen.
Mit EntityManager::createCriteria()
kannst du auch ein "leeres" Criteria erstellen und die Abfrage selbst beschreiben. Dies ermöglicht dir komplexere Abfragen zu formulieren.
/** * @param array $types * @return Article[] */ public function getArticlesByTypes(array $types) { $criteria = $this->em->createCriteria(); $criteria->select('a') ->from(Article::getClass(), 'a') ->where()->match('a.online', '=', true)->andMatch('a.type', 'IN', $types); return $criteria->toQuery()->fetchArray(); }
ArticleDao::getArticleDao()
des oben stehenden Beispiels könntest du mit $articleDao->getArticlesByTypes(array('news', 'comment'))
aufrufen, um alle Article-Objekte abzufragen bei denen Article::$online
gleich true
und Article::$type
gleich "news" oder "comment" ist.
NQL (n2n Query Language)
Die n2n Query Language ähnelt syntaktisch SQL-Statements, beziehen sich aber, wie die Criteria API, auf Entities und dessen Eigenschaften statt auf Datenbanktabellen und Spalten. Mit EntityManager::createNqlCriteria()
kannst du ein Criteria
-Objekt auf Basis von NQL erstellen.
/** * @param string $type * @param int $limit * @param int $num * @return Article[] */ public function getArticlesByType($type, $limit, $num) { $criteria = $this->em->createNqlCriteria( 'SELECT a FROM Article a WHERE a.online = :online AND a.type = :type', array('online' => true, 'type' => $type)); return $criteria->limit(0, 30)->toQuery()->fetchAll(); }
In diesem Beispiel haben wir die Methode ArticleDao::getArticlesByType()
, aus dem Beispiel weiter oben, auf Basis von NQL implementiert. EntityManager::createNqlCriteria()
erwartet als ersten Parameter den NQL-String und als zweiten Parameter die Werte der Placeholder, die du in deinem NQL-Statement verwendet hast.
Query::setParameter()
setzen. Der Unterschied dieser zwei Varianten wird im Abschnitt Placeholders des Artikels Criteria API / NQL erläutert.Schreiben
Über den EntityManager::persist()
kannst du Entities in die Datenbank speichern und mit EntityManager::remove()
löschen. Operationen, die zu Datenbank-Mutationen führen, können nur innerhalb einer Transaktion ausgeführt werden. Am einfachsten ist es Transaktionen im Controller zu verwalten. Über ControllerAdapter::beginTransaction()
kannst du eine Transaktion starten und mit ControllerAdapter::commit()
abschliessen.
public function doCreateArticle(ArticleDao $articleDao) { $this->beginTransaction(); $article = new Article('Lorem ipsum'); $article->setText('Lorem ipsum dolor...'); $article->setType(Article::TYPE_COMMENT); $articleDao->saveArticle($article); $this->commit(); }
ArticleDao::saveArticle()
kann folgendermassen aussehen:
public function saveArticle(Article $article) { $this->em->persist($article); }
Die INSERT
- und DELETE
-Statements werden ausgeführt, sobald die Transaktion abgeschlossen wird (COMMIT). Beim Abschliessen einer Transaktion werden auch alle Änderungen, die du an Entities vorgenommen hast, in der Datenbank aktualisiert (UPDATE
-Statement).
public function doExample(ArticleDao $articleDao) { $this->beginTransaction(); $article = $articleDao->getArticleById(1); $article->setTitle('New title'); $this->commit(); }
In diesem Beispiel wird der Titel in der Datenbank automatisch aktualisiert, sobald ControllerAdapter::commit()
aufgerufen wird (ein UPDATE
-Statement wird abgesetzt).
Logger
Interessiert es dich welche SQL-Statements der EntityManager generiert und absendet, kannst du sie über den n2n\persistence\PdoLogger
ausgeben lassen. Der PdoLogger
logt alle Datenbankzugriffe eines Pdo
-Objekts. Auf den jeweiligen PdoLogger
kannst du über Pdo::getLogger()
zugreifen.
public function saveArticle(Article $article) { $this->em->persist($article); var_dump($this->em->getPdo()->getLogger()->getEntries()); }
In diesem Beispiel holen wir über EntityManager::getPdo()
das vom EntityManager
verwendete Pdo
und greifen über PdoLogger::getEntries()
auf die gewünschten Log-Informationen zu.