Pregunta

Say I have the following classes:

User (domain object) UserMapper (data mapper) Achievement (domain object) AchievementCollection (domain object) AchievementMapper (data mapper)

The relationship here is that a User has an AchievementCollection as a property (or, a User has many Achievements).

What I want to be able to do is query a list of users, and ALSO query all of their achievements. However, I'm not entirely sure how to do this with the data mapper pattern. If I loop through each user and use the AchievementMapper to query user's achievements, that's going to be fairly inefficient, especially if I have 50+ users to query against.

What is the best way to handle this situation? (also, this is for academic/learning purposes, which is why I'm not using Doctrine)

Should the UserMapper be responsible for getting the user's achievements? Should I use only the achievement mapper to query all achievements by userID? (and loop through each user?)

¿Fue útil?

Solución

Using an ORM (even one you write yourself, so anything that generically maps objects to relational tables or vice versa) is always a tradeoff: there are advantages, certainly, but disadvantages as well.

The main tradeoff between an ORM and write-custom-queries is ease of use and speed of development versus efficiency. In other words, the advantage is, by example, you suddenly get to write $userMapper->getUserById(5); and get the relevant User object in return. The composition and execution of the query, the fetching of the result set and mapping that on objects is all done for you. The disadvantage is that, for all but the most basic use cases, an ORM will not execute the optimal (combination of) queries to achieve the goal. In return for sacrificing that, you get more ease of use (as a programmer) and faster development (although an ORM can hold you back as well...).

In general, when using an ORM, you (try to) forget the smaller inefficiencies. If you attempt to create an ORM which is as efficient as the custom SQL queries you would write in its place, you will end up reinventing SQL.

In your example of only 50 users, I would just use the ORM and live with the inefficiency of querying the achievements table once for each user. Any decent database server will not have any problem with that.

Nevertheless, since you mention the academic side, I've come up with some options of improving on a query-per-call scenario. For your specific use case, you could try some form of caching: prefetch all Achievement objects, mark that collection as 'complete' (i.e. the mappers can assume there are no other Achievements in the database that haven't been loaded yet) and then make the $user->getAchievements() check if there is a 'complete' collection of achievements and if so, use that instead of the database. From an outside point of view, this could look like this:

$achievementMapper->preloadAll();
foreach ($userMapper->getAll() as $user) {
    echo "User {$user->getName()} has the following achievements: ";
    foreach ($user->getAchievements() as $achievement) {
        echo $achievement->getName();
    }
}

Internally, ideally this only executes two queries: one selecting all achievements and one selecting all users. The joining of users and achievements is done by the ORM in memory. The main drawbacks of this method are high memory usage and it is very inefficient when you're not interested in all objects of a certain type (for example, only users who have more than five achievements).

A more complex option is preloading achievements based on the users you selected, which could look something like this:

$top100Users = $userMapper->getTop100();

// internally cache all Achievement objects linked to any of the 100 users
$achievementMapper->preloadByUserCollection($top100Users) 

Using this approach, the possible inefficiency decreases, but (remember, always a trade off) both the usage of the ORM and the ORM itself become a bit more complex. For example, the achievement mapper would have to remember for which user it has preloaded all achievements and still go to the database if that is not the case (or if the database has changed since then).

Another (much more complex) option is a mechanism like Doctrine's DQL fetch joins, where you tell the ORM which different object (types) it should 'map' from the result set of a query. The not-having-to-write-queries part of an ORM disappears a bit with this, but the automatic mapping of the result set to objects with correct associations remains.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top