SQL Abstraction and Object Hydration
SQL Abstraction and Object Hydration
In the last chapter, we introduced database abstraction and a new command interface for operations that might change what blog posts we store. We'll now start creating database-backed versions of the PostRepositoryInterface andPostCommandInterface, demonstrating usage of the various Zend\Db\Sql classes.
Preparing the Database
This tutorial assumes you've followed the Getting Started tutorial, and that you've already populated the data/zftutorial.db SQLite database. We will be re-using it, and adding another table to it.
Create the file data/posts.schema.sql with the following contents:
CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title varchar(100) NOT NULL, text TEXT NOT NULL);
INSERT INTO posts (title, text) VALUES ('Blog #1', 'Welcome to my first blog post');
INSERT INTO posts (title, text) VALUES ('Blog #2', 'Welcome to my second blog post');
INSERT INTO posts (title, text) VALUES ('Blog #3', 'Welcome to my third blog post');
INSERT INTO posts (title, text) VALUES ('Blog #4', 'Welcome to my fourth blog post');
INSERT INTO posts (title, text) VALUES ('Blog #5', 'Welcome to my fifth blog post');
Now we will execute this against the existing data/zftutorial.db SQLite database using the sqlite command (or sqlite3; check your operating system):
$ sqlite data/zftutorial.db < data/posts.schema.sql
If you don't have a sqlite command, you can populate it using PHP. Create the following script in data/load_posts.php:
<?php
$db = new PDO('sqlite:' . realpath(__DIR__) . 'zftutorial.db');
$fh = fopen(__DIR__ . '/posts.schema.sql', 'r');
while ($line = fread($fh, 4096)) {
$line = trim($line);
$db->exec($line);
}
fclose($fh);
and execute it using:
$ php data/load_posts.php
Quick Facts Zend\Db\Sql
To create queries against a database using Zend\Db\Sql, you need to have a database adapter available. The "Getting Started" tutorial covered this in the database chapter, and we can re-use that adapter.
With the adapter in place and the new table populated, we can run queries against the database. The construction of queries is best done through the "QueryBuilder" features of Zend\Db\Sql which are Zend\Db\Sql\Sql for select queries, Zend\Db\Sql\Insert for insert queries, Zend\Db\Sql\Update for update queries and Zend\Db\Sql\Delete for delete queries. The basic workflow of these components is:
- Build a query using the relevant class:
Sql,Insert,Update, orDelete. - Create a SQL statement from the
Sqlobject. - Execute the query.
- Do something with the result.
Let's start writing database-driven implementations of our interfaces now.
Writing the repository implementation
Create a class named ZendDbSqlRepository in the Blog\Model namespace that implements PostRepositoryInterface; leave the methods empty for now:
// In module/Blog/src/Model/ZendDbSqlRepository.php:
namespace Blog\Model;
use InvalidArgumentException;
use RuntimeException;
class ZendDbSqlRepository implements PostRepositoryInterface
{
/**
* {@inheritDoc}
*/
public function findAllPosts()
{
}
/**
* {@inheritDoc}
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function findPost($id)
{
}
}
Now recall what we have learned earlier: for Zend\Db\Sql to function, we will need a working implementation of the AdapterInterface. This is a requirement, and therefore will be injected using constructor injection. Create a __construct()method that accepts an AdapterInterface as its sole parameter, and stores it as an instance property:
// In module/Blog/src/Model/ZendDbSqlModel.php:
namespace Blog\Mapper;
use InvalidArgumentException;
use RuntimeException;
use Zend\Db\Adapter\AdapterInterface;
class ZendDbSqlRepository implements PostRepositoryInterface
{
/**
* @var AdapterInterface
*/
private $db;
/**
* @param AdapterInterface $db
*/
public function __construct(AdapterInterface $db)
{
$this->db = $db;
}
/**
* {@inheritDoc}
*/
public function findAllPosts()
{
}
/**
* {@inheritDoc}
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function findPost($id)
{
}
}
Whenever we have a required parameter, we need to write a factory for the class. Go ahead and create a factory for our new repository implementation:
// In module/Blog/src/Factory/ZendDbSqlRepositoryFactory.php
namespace Blog\Factory;
use Interop\Container\ContainerInterface;
use Blog\Model\ZendDbSqlRepository;
use Zend\Db\Adapter\AdapterInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class ZendDbSqlRepositoryFactory implements FactoryInterface
{
/**
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return ZendDbSqlRepository
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new ZendDbSqlMapper($container->get(AdapterInterface::class));
}
}
We're now able to register our repository implementation as a service. To do so, we'll make two changes:
- Register a factory entry for the new repository.
- Update the existing alias for
PostRepositoryInterfaceto point to the new repository.
Update module/Blog/config/module.config.php as follows:
return [
'service_manager' => [
'aliases' => [
// Update this line:
Model\PostRepositoryInterface::class => Model\ZendDbSqlRepository::class,
],
'factories' => [
Model\PostRepository::class => InvokableFactory::class,
// Add this line:
Model\ZendDbSqlRepository::class => Factory\ZendDbSqlRepositoryFactory::class,
],
],
'controllers' => [ /* ... */ ],
'router' => [ /* ... */ ],
'view_manager' => [ /* ... */ ],
];
With the adapter in place you're now able to refresh the blog index atlocalhost:8080/blog and you'll notice that the ServiceNotFoundException is gone and we get the following PHP Warning:
Warning: Invalid argument supplied for foreach() in {projectPath}/module/Blog/view/blog/list/index.phtml on line {lineNumber}
This is due to the fact that our mapper doesn't return anything yet. Let's modify the findAllPosts() function to return all blog posts from the database table:
// In /module/Blog/src/Model/ZendDbSqlRepository.php:
namespace Blog\Model;
use InvalidArgumentException;
use RuntimeException
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Sql\Sql;
class ZendDbSqlRepository implements PostRepositoryInterface
{
/**
* @var AdapterInterface
*/
private $db;
/**
* @param AdapterInterface $db
*/
public function __construct(AdapterInterface $db)
{
$this->db = $db;
}
/**
* {@inheritDoc}
*/
public function findAllPosts()
{
$sql = new Sql($this->dbAdapter);
$select = $sql->select('posts');
$stmt = $sql->prepareStatementForSqlObject($select);
$result = $stmt->execute();
return $result;
}
/**
* {@inheritDoc}
* @throws InvalidArgumentException
* @throw RuntimeException
*/
public function findPost($id)
{
}
}
Sadly, though, a refresh of the application reveals another error message:
PHP Fatal error: Call to a member function getId() on array in {projectPath}/module/Blog/view/blog/list/index.phtml on line {lineNumber}
Let's not return the $result variable for now and do a dump of it to see what we get here. Change the findAllPosts() method and dump the result:
public function findAllPosts()
{
$sql = new Sql($this->dbAdapter);
$select = $sql->select('posts');
$stmt = $sql->prepareStatementForSqlObject($select);
$result = $stmt->execute();
var_export($result);
die();
return $result;
}
Refreshing the application you should now see output similar to the following:
Zend\Db\Adapter\Driver\Pdo\Result::__set_state(array(
'statementMode' => 'forward',
'fetchMode' => 2,
'resource' =>
PDOStatement::__set_state(array(
'queryString' => 'SELECT "posts".* FROM "posts"',
)),
'options' => NULL,
'currentComplete' => false,
'currentData' => NULL,
'position' => -1,
'generatedValue' => '0',
'rowCount' =>
Closure::__set_state(array(
)),
))
As you can see, we do not get any data returned. Instead we are presented with a dump of some Result object that appears to have no data in it whatsoever. But this is a faulty assumption. This Result object only has information available for you when you actually try to access it. If you can determine that the query was successful, the best way to make use of the data within the Result object is to pass it to a ResultSet object.
First, add two more import statements to the class file:
use Zend\Db\Adapter\Driver\ResultInterface;
use Zend\Db\ResultSet\ResultSet;
Now update the findAllPosts() method as follows:
public function findAll()
{
$sql = new Sql($this->dbAdapter);
$select = $sql->select('posts');
$stmt = $sql->prepareStatementForSqlObject($select);
$result = $stmt->execute();
if ($result instanceof ResultInterface && $result->isQueryResult()) {
$resultSet = new ResultSet();
$resultSet->initialize($result);
var_export($resultSet);
die();
}
die('no data');
}
Refreshing the page, you should now see the dump of a ResultSet instance:
Zend\Db\ResultSet\ResultSet::__set_state(array(
'allowedReturnTypes' =>
array (
0 => 'arrayobject',
1 => 'array',
),
'arrayObjectPrototype' =>
ArrayObject::__set_state(array(
)),
'returnType' => 'arrayobject',
'buffer' => NULL,
'count' => NULL,
'dataSource' =>
Zend\Db\Adapter\Driver\Pdo\Result::__set_state(array(
'statementMode' => 'forward',
'fetchMode' => 2,
'resource' =>
PDOStatement::__set_state(array(
'queryString' => 'SELECT "album".* FROM "album"',
)),
'options' => NULL,
'currentComplete' => false,
'currentData' => NULL,
'position' => -1,
'generatedValue' => '0',
'rowCount' =>
Closure::__set_state(array(
)),
)),
'fieldCount' => 3,
'position' => 0,
))
Of particular interest is the returnType property, which has a value ofarrayobject. This tells us that all database entries will be returned as anArrayObject instances. And this is a little problem for us, as thePostRepositoryInterface requires us to return an array of Post instances. Luckily the Zend\Db\ResultSet subcomponent offers a solution for us, via theHydratingResultSet; this result set type will populate an object of a type we specify with the data returned.
Let's modify our code. First, remove the following import statement from the class file:
use Zend\Db\ResultSet\ResultSet;
Next, we'll add the following import statements to our class file:
use Zend\Hydrator\Reflection as ReflectionHydrator;
use Zend\Db\ResultSet\HydratingResultSet;
Now, update the findAllPosts() method to read as follows:
public function findAllPosts()
{
$sql = new Sql($this->db);
$select = $sql->select('posts');
$statement = $sql->prepareStatementForSqlObject($select);
$result = $statement->execute();
if (! $result instanceof ResultInterface || ! $result->isQueryResult()) {
return [];
}
$resultSet = new HydratingResultSet(
new ReflectionHydrator(),
new Post('', '')
);
$resultSet->initialize($result);
return $resultSet;
}
We have changed a couple of things here. First, instead of a normal ResultSet, we are now using the HydratingResultSet. This specialized result set requires two parameters, the second one being an object to hydrate with data, and the first one being the hydrator that will be used (a hydrator is an object that will transform an array of data into an object, and vice versa). We useZend\Hydrator\Reflection here, which is capable of injecting private properties of an instance. We provide an empty Post instance, which the hydrator will clone to create new instances with data from individual rows.
Instead of dumping the $result variable, we now directly return the initializedHydratingResultSet so we can access the data stored within. In case we get something else returned that is not an instance of a ResultInterface, we return an empty array.
Refreshing the page you will now see all your blog posts listed on the page. Great!
Refactoring hidden dependencies
There's one little thing that we have done that's not a best-practice. We use both a hydrator and an Post prototype inside our ZendDbSqlRepository. Let's inject those instead, so that we can reuse them between our repository and command implementations, or vary them based on environment. Update yourZendDbSqlRepository as follows:
// In module/Blog/src/Model/ZendDbSqlRepository.php:
namespace Blog\Model;
use InvalidArgumentException;
use RuntimeException;
// Replace the import of the Reflection hydrator with this:
use Zend\Hydrator\HydratorInterface;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Adapter\Driver\ResultInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\Sql\Sql;
class ZendDbSqlRepository implements PostRepositoryInterface
{
/**
* @var AdapterInterface
*/
private $db;
/**
* @var HydratorInterface
*/
private $hydrator;
/**
* @var Post
*/
private $postPrototype;
public function __construct(
AdapterInterface $db,
HydratorInterface $hydrator,
PostInterface $postPrototype
) {
$this->db = $db;
$this->hydrator = $hydrator;
$this->postPrototype = $postPrototype;
}
/**
* Return a set of all blog posts that we can iterate over.
*
* Each entry should be a Post instance.
*
* @return Post[]
*/
public function findAllPosts()
{
$sql = new Sql($this->db);
$select = $sql->select('posts');
$statement = $sql->prepareStatementForSqlObject($select);
$result = $statement->execute();
if (! $result instanceof ResultInterface || ! $result->isQueryResult()) {
return [];
}
$resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype);
$resultSet->initialize($result);
return $resultSet;
}
/**
* Return a single blog post.
*
* @param int $id Identifier of the post to return.
* @return Post
*/
public function findPost($id)
{
}
}
Now that our repository requires more parameters, we need to update theZendDbSqlRepositoryFactory and inject those parameters:
// In /module/Blog/src/Factory/ZendDbSqlRepositoryFactory.php
namespace Blog\Factory;
use Interop\Container\ContainerInterface;
use Blog\Model\Post;
use Blog\Model\ZendDbSqlRepository;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Hydrator\Reflection as ReflectionHydrator;
use Zend\ServiceManager\Factory\FactoryInterface;
class ZendDbSqlRepositoryFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new ZendDbSqlRepository(
$container->get(AdapterInterface::class),
new ReflectionHydrator(),
new Post('', '')
);
}
}
With this in place you can refresh the application again and you'll see your blog posts listed once again. Our repository no longer has hidden dependencies, and works with a database!
Finishing the repository
Before we jump into the next chapter, let's quickly finish the repository implementation by completing the findPost() method:
public function findPost($id)
{
$sql = new Sql($this->db);
$select = $sql->select('posts');
$select->where(['id = ?' => $id]);
$statement = $sql->prepareStatementForSqlObject($select);
$result = $statement->execute();
if (! $result instanceof ResultInterface || ! $result->isQueryResult()) {
throw new RuntimeException(sprintf(
'Failed retrieving blog post with identifier "%s"; unknown database error.',
$id
));
}
$resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype);
$resultSet->initialize($result);
$post = $resultSet->current();
if (! $post) {
throw new InvalidArgumentException(sprintf(
'Blog post with identifier "%s" not found.',
$id
));
}
return $post;
}
The findPost() function looks similar to the findAllPosts() method, with several differences.
- We need to add a condition to the query to select only the row matching the provided identifier; this is done using the
where()method of theSqlobject. - We check if the
$resultis valid, usingisQueryResult(); if not, an error occurred during the query that we report via aRuntimeException. - We pull the
current()item off the result set we create, and test to make sure we received something; if not, we had an invalid identifier, and raise anInvalidArgumentException.
Conclusion
Finishing this chapter, you now know how to query for data using theZend\Db\Sql classes. You have also learned a little about the zend-hydrator component, and the integration zend-db provides with it. Furthermore, we've continued demonstrating dependency injection in all aspects of our application.
In the next chapter we'll take a closer look at the router so we'll be able to start displaying individual blog posts.
SQL Abstraction and Object Hydration的更多相关文章
- 关于oracle PL/SQL存储过程 PLS-00905 object is invalid,statement ignored问题的解决
昨天在学习oracle存储过程的时候,写了一个存储过程的demo,语句是这样的: )) AS psssal TESTDELETE.TESTID%TYPE; BEGIN SELECT TESTID IN ...
- sql server sys.object表字段说明
列名 数据类型 说明 name sysname 对象名. object_id int 对象标识号. 在数据库中是唯一的. principal_id int 如果不是架构所有者,则为单个所有者的 ID. ...
- 分享公司DAO层动态SQL的一些封装
主题 公司在DAO层使用的框架是Spring Data JPA,这个框架很好用,基本不需要自己写SQL或者HQL就能完成大部分事情,但是偶尔有一些复杂的查询还是需要自己手写原生的Native SQL或 ...
- string.Format , object[] args 使用
string sql = "insert into TableA values('{0}','{1}','{2}',GetDate(),'{3}' "; sql = string. ...
- 简单数据访问类,生成简单SQL,自动转换成java对象
import java.util.HashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; impo ...
- Android 连接 SQL Server (jtds方式)——下
本文主要补充介绍jtds的查询方法,将以博主的一个实际开发程序进行说明 下图是项目的文件列表与界面效果: 运行效果: 1.三个EditText对应的是单个计划的序号.品种名.数量 2 ...
- SQL参数化
本文来自:caodonglin 一.SQL参数化为什么能防注入? 因为执行计划被重用了,所以可以防SQL注入. 下面有两段SQL 正常SQL: 1 select COUNT(1) from C ...
- 20141129 LinQ to SQL
ORMO-Object对象R-Relation关系M-Mapping映射 对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),是 ...
- MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间
Plugins 摘一段来自MyBatis官方文档的文字. MyBatis允许你在某一点拦截已映射语句执行的调用.默认情况下,MyBatis允许使用插件来拦截方法调用 Executor(update.q ...
随机推荐
- Sping表达式语言--SpEL
Spring表达式语言---SpEL 是一个支持运行时查询和操作对象的强大的表达式语言 语法类似于EL:SpEL使用#{...}作为定界符,所有在大括号中的字符都将被认为是SpEL SpEL为bean ...
- 【Web前沿技术】纯 CSS3 打造的10个精美加载进度条动画
之前向大家介绍8款优秀的 jQuery 加载动画和进度条插件,今天这篇文章向大家推荐10个纯 CSS3 代码实现精美加载进度条动画效果的方案.加载动画和进度条在网站和 Web 应用中的使用非常流行,特 ...
- 如何在asp.net mvc3中使用HttpStatusCode
下载了asp.net mvc 4的源码看了看,没怎么看清楚.不过个人觉得MVC4 beta中Web API这个是比较不错的,虽然说它是往传统回归. web api最好的莫过于它更加适合使用jquery ...
- Visual Studio中的一些较大的文件的作用
1.sdf 这些是工程中的中间,用于预编译等作用,最终可执行文件是不需要的,默认情况下,删除后重新编译还会生成.如果不需要,在Visual Studio里进入如下设置: 进入"Tools & ...
- [转] MATLAB快捷键
原文地址:MATLAB快捷键大全 (转载)作者:掷地有声 一.索引混排版 备注:删除了如F1(帮助)等类型的常见快捷命令 SHIFT+DELETE永久删除 DELETE删除 ALT+ENTER属性 A ...
- C# 等待另外一个窗体关闭,再进行主线程的代码
方法1 用Form类或其子类的showDialog方法. 比如你在form1里有一个按扭,然后你在Form1的点击事件里写上显示form2的代码: Form2 frm=new Form2(); frm ...
- Amoeba搞定mysql主从读写分离
前言:一直想找一个工具,能很好的实现mysql主从的读写分离架构,曾经试用过mysql-proxy发现lua用起来很不爽,尤其是不懂lua脚本,突然发现了Amoeba这个项目,试用了下,感觉还不错,写 ...
- C# 创建WebServices及调用方法
发布WebServices 1.创建 ASP.NET Web 服务应用程序 SayHelloWebServices 这里需要说明一下,由于.NET Framework4.0取消了WebService ...
- IOS 多线程 NSOperation GCD
1.NSInvocationOperation NSInvocationOperation * op; NSOperationQueue * que = [[NSOperationQueuealloc ...
- 【原】Storm调度器
Storm入门教程 1. Storm基础 Storm Storm主要特点 Storm基本概念 Storm调度器 Pluggable scheduler(可插拔调度器) Isolation schedu ...