Who Should Handle the Conditions in Complex Queries, the Data Mapper or the Service Layer

Who should handle the conditions in complex queries, the data mapper or the service layer?

The data mapper pattern only tells you, what it is supposed to do, not how it should be implemented.

Therefore all the answers in this topic should be treated as subjective, because they reflect each authors personal preferences.

I usually try to keep mapper's interface as simple as possible:

  • fetch(), retrieves data in the domain object or collection,
  • save(), saves (updates existing or inserts new) the domain object or collection
  • remove(), deletes the domain object or collection from storage medium

I keep the condition in the domain object itself:

$user = new User;
$user->setName( 'Jedediah' );

$mapper = new UserMapper;
$mapper->fetch( $user );

if ( $user->getFlags() > 5 )
{
$user->setStatus( User::STATUS_LOCKED );
}

$mapper->save( $user );

This way you can have multiple conditions for the retrieval, while keeping the interface clean.

The downside to this would be that you need a public method for retrieving information from the domain object to have such fetch() method, but you will need it anyway to perform save().

There is no real way to implement the "Tell Don't Ask" rule-of-thumb for mapper and domain object interaction.

As for "How to make sure that you really need to save the domain object?", which might occur to you, it has been covered here, with extensive code examples and some useful bits in the comments.

Update

I case if you expect to deal with groups of objects, you should be dealing with different structures, instead of simple Domain Objects.

$category = new Category;
$category->setTitle( 'privacy' );

$list = new ArticleCollection;

$list->setCondition( $category );
$list->setDateRange( mktime( 0, 0, 0, 12, 9, 2001) );
// it would make sense, if unset second value for range of dates
// would default to NOW() in mapper

$mapper = new ArticleCollectionMapper;
$mapper->fetch( $list );

foreach ( $list as $article )
{
$article->setFlag( Article::STATUS_REMOVED );
}

$mapper->store( $list );

In this case the collection is glorified array, with ability to accept different parameters, which then are used as conditions for the mapper. It also should let the mapper to acquired list changed domain objects from this collection, when mapper is attempting to store the collection.

The mapper in this case should be capable of building (or using preset ones) queries with all the possible conditions (as a developer you will know all of those conditions, therefore you do not need to make it work with infinite set of conditions) and update or create new entries for all the unsaved domain object, that collection contains.

Note: In some aspect you could say, that the mapper are related to builder/factory patterns. The goal is different, but the approach to solving the problems is very similar.

Data Mapper Pattern: Complexe query from Service Layer

Indeed, for two conditions, it inadvisable to query on one condition and then filter on your other while you iterate. It leaves you no clear approach for more than two conditions. And implementing pagination adapters becomes pretty messy.

Seems to me that the issue is that your mapper fetch() method supports permits only a single condition. So:

  1. Modify the signature to support an array of conditions.

  2. Alternatively, you could create a separate mapper method for each enhanced fetch method, ex: fetchByHairAndAge($hair, $age), etc.

Handling items in the collection pattern by a data mapper

I wouldn't think to actually use a factory object to add the articles. You may see yourself using one to make the instance of Article (in the second example), though. What I went ahead and did was add an addArticles () method to the ArticleCollection instance. This way you can simply call the method on your instance of ArticleCollection from the mapper. ArticleCollectionMapper may look something like:

class ArticleCollectionMapper extends DataMapperAbstract
{
public function fetch ( ArticleCollection $articles )
{
$prepare = $this->connection->prepare( "SELECT ..." );
$prepare->execute();
// filter conditions

$articles->addArticles( $prepare->fetchAll() );
}
}

You'd need to do some filtering by getting the conditions from the ArticleCollection instance, which is excluded from the snippet above. Then our domain object's addArticles() implementation would look similar following:

class ArticleCollection extends DomainObjectAbstract
{
protected $collection = array();

public function addArticles ( Array $articles )
{
foreach ( $articles as $article )
{
$articleCollectionItem = new Article;
$articleCollectionItem->setParams( $article );
// however you prefer filling your list with `Article` instances

$this->collection[] = $articleCollectionItem;
}
}
}

You may also want to add an addArticle() method depending on your needs, and then just replacing what's within the foreach above with a call to addArticle(). Note that the above examples are extremely simplified and code will need to be adapted in order to meet your standards.

Implementing Domain Model and Data Mapper pattern with external data sources

There are no "multiple models" in MVC. Model is one of two layers (along with presentation layer), which make up the MVC design pattern. What people usually call "models" are actually domain objects. You might find this relevant.

That said, you are looking at this all the wrong way. What you currently have is variation of active record pattern. Only, instead of database, in this case storage medium is REST API and/or SOAP interface instead of classical SQL storage.

The best option in this case would be to separate the domain object (for example: User) from the different elements of storage related logic. I personally prefer to implement data mappers for this. Essentially, if you have a domain object, you pass it to the mapper's method, which then either retrieves information from said object and sends it to storage or retrieves the data from storage and applies it to that domain object.

The User instance does not care whether it was save or not. It has no impact on domain logic.

PHP datamapper - why use them for non-collection objects?

Short answer

You don't need to bother creating two mappers for Author and AuthorCollection. If your program doesn't need an AuthorMapper and an AuthorCollectionMapper in order to work smoothly and have a clean source, by all means, do what you're most comfortable with.

Note: Choosing this route means you should be extra careful looking out for SRP violations.



Long(er) answer

It all depends on what you're trying to do. For the sake of this post, let's call AuthorMapper an item data mapper and AuthorCollectionMapper a collection data mapper.

Typically, item mappers won't be as sophisticated as their collection mappers. Item mappers will normally only fetch by a primary key and therefore limit the results, making the mapper clean and uncluttered by additional collection-specific things.

One main part of these "collection-specific things" I bring up is conditions1 and how they're implemented into queries. Often within collection mappers you'll probably have more advanced, longer, and tedious queries than what would normally be inside an item data mapper. Though entirely possible to combine your average item data mapper query (SELECT ... WHERE id = :id) with a complicated collection mapper query without using a smelly condition2, it gets more complicated and still bothers the database to execute a lengthy query when all it needed was a simple, generic one.

Additionally, though you pointed out that with an item mapper we really only fetch by a primary key, it usually turns out to be radically simpler using an item mapper for other things. An item mapper's save() and remove() methods can handle (with the right implementation) the job better than attempting to use a collection mapper to save/remove items. And, along with this point, it also becomes apparent that at times throughout using a collection mappers' save() and remove() method, a collection mapper may want to utilize item mapper methods.

In response to your question below, there may be numerous times you may want to set conditions in deleting a collection of rows from the database. For example, you may have a spam flag that, when set, hides the post but self-destructs in thirty days. I'm that case you'd most likely have a condition for the spam flag and one for the time range. Another might be deleting all the comments under an answer thirty days after an answer was deleted. I mention thirty days because it's wise to at least keep this data for a little while in case someone should want their comment or it turns out the row with a spam flag isn't actually spam.

1. Condition here means a property set on the collection instance which the collection mapper's query knows how to handle. If you haven't already, check out @tereško's answer here.

2. This condition is different and refers to the "evil if" people speak of. If you don't understand their nefariousness, I'd suggest watching some Clean Code Talks. This one specifically, but all are great.



Related Topics



Leave a reply



Submit