We are taking a different approach that works well for us. When you look at
methods like these:
fetchLatestArticles()
fetchArticlesBasedOnCategory($category)
fetchBlueArticlesByOrderedByGermanyPaidLastMonth()
They all have two things in common: they return articles and they rely on
criteria.
With that in mind, we have a single method in our mappers that can cover all
of those cases:
Default_Model_Mapper_Articles#fetchAll($criteria, $sort)
The criteria is similar to a WHERE clause in SQL, but it's not in SQL
format. It uses objects instead:
$category = new Default_Model_Category();
$category->setId(123);
$mapper = new Default_Model_Articles_Mapper();
$articles = $mapper->fetch(array(
category => $category
));
The mapper's fetch method then inspects the criteria array and maps it to a
SQL WHERE clause. This includes adding any required joins:
public function fetchAll(array $criteria = null, $sort = null)
{
$select = $this->getSelect();
if (isset($criteria['category'])) {
$select->where('category_id = ?', $criteria['category']->getId());
}
return $this->_createCollection($select, $sort);
}
The getSelect() method returns the bare bones select object for articles,
where only the "id" is returned. The _createCollection() method takes the
select object and fetches the result into a lazy-loading iterator.
Sometimes this mapping can get very complex, especially if you want to
support lots of criteria. For these cases, I create a helper class whose
only job it is to build that select query. It would be used internally by
the mapper only, transparent from the controllers.
public function fetch(array $criteria = null, $sort = null)
{
$search = new Default_Model_Mapper_Search_Articles($this);
$search->setCriteria($criteria);
$select = $search->getSelect();
return $this->_createCollection($select, $sort);
}
The goal behind this design is to make the controllers completely unaware
that a database even exists and makes it easy to stub in mappers for unit
tests:
// in controller
public function indexAction()
{
// check for injected mappers (provided by unit tests)
// this logic could be moved to an action helper
if (!$articlesMapper = $this->getInvokeArg('articles_mapper')) {
$articlesMapper = new Default_Model_Mapper_Articles();
}
$this->view->articles = $articlesMapper->fetchAll();
}
There are a lot of steps in this design but it helps to ensure that at any
time you can swap out mappers or change your DB schema without having to
rewrite a single line in the controllers. I actually used this design for
one of our components and, half-way through, redesigned the entire schema
for better performance. The only changes I had to make were in the mappers
themselves.
Another benefit was that I was able to quickly build up some new mappers to
benchmark the performance if we switched to a NoSQL database like MongoDB. I
branched my project and rewrote some mappers to use MongoDB and I was able
to compare the app side by side with the MySQL version within just a few
hours.
--
Hector
On Thu, Apr 15, 2010 at 7:31 AM, keith Pope <[email protected]>wrote:
> On 15 April 2010 14:48, Matthew Weier O'Phinney <[email protected]> wrote:
> > -- Ralf Eggert <[email protected]> wrote
> > (on Thursday, 15 April 2010, 02:10 PM +0200):
> >> I have a question regarding a model infrastructure that uses models,
> >> data sources, data mappers and a service layer. I like this concept a
> >> lot but am still thinking about one issue.
> >>
> >> I want to get a list of all blue articles which have been ordered by
> >> users from Germany and have been paid last month. Thinking about the
> >> database this will definitely be a rather complex database join about at
> >> least three tables (articles, users, order). Lets call this method
> >> fetchBlueArticlesOrderedByGermanyPaidLastMonth().
> >>
> >> Where should this method be located?
> >>
> >> - in the data source (Zend_Db_Table class)
> >> - in the model
> >> - in the data mapper
> >> - in the service layer
> >>
> >> I could find pros and cons for each solution if I want to. But what do
> >> others think?
> >
> > I typically think of this as a job for the mapper, as it's mapping the
> > data source to objects. That gives you the flexibility of either
> > wrapping it in your service layer (I do this so I can return a paginator
> > object) or simply calling it as a method off your mapper.
>
> I have started to use the repository pattern lately this seems to work
> well, so you have:
>
> - dbTable
> - mapper
> - model
> - repository
> - service
>
> So now for highly specialized queries I would have them inside the
> repository, not sure about
> fetchBlueArticlesOrderedByGermanyPaidLastMonth() but I would have a
> pretty descriptive method name :) I have implemented a criteria object
> so the rules can be easily added at runtime, so maybe something like
> getOrderedArticlesForMonth(). Using the repo you can then do something
> like:
>
> $articleOrderRepo = new Kp_Model_Repository_ArticleOrders();
> $articleOrderRepo->addCriteria(new
> Kp_Domain_Repository_Criteria('color', '= ?', 'blue'));
> $articleOrderRepo->addCriteria(new
> Kp_Domain_Repository_Criteria('country', '= ?', 'Germany'));
> $articleOrderRepo->getOrderedArticlesForMonth(10);
>
> The only thing here is that the criteria/repository are fairly one
> dimensional so complex joins would need to be placed inside the mapper
> as its hard to accurately generate the correct joins. Also sometimes
> for large queries, for example when doing a export of all order for a
> year, I have a getRaw type method on the repository, this will then
> return data instead of objects to stop memory errors.
>
> >
> > --
> > Matthew Weier O'Phinney
> > Project Lead | [email protected]
> > Zend Framework | http://framework.zend.com/
> > PGP key: http://framework.zend.com/zf-matthew-pgp-key.asc
> >
>
>
>
> --
> ------------
> http://www.thepopeisdead.com
>