5

Let's say I have two models:

class Book
{
    public $book_id;
    public $book_author_id;
    public $title;
}

class Author
{
    public $author_id;
    public $author_name;
}

I'm used to writing something like this:

$DB->query("SELECT book_id, title, author_name 
            FROM book 
            LEFT JOIN author 
                ON book_author_id = author_id
           ");

Let's assume, that I'm not interested in having separate queries for this association. How do I proceed? Here are some things I've heard:

  • create a MySQL VIEW of the JOIN
  • create a model class of the VIEW

The application I'm working on involves dozens of tables and was highly optimized in procedural code (almost no SELECT * anywhere, for instance). I'm refactoring to make it more maintainable (I'm the original creator, too), but I would like to have the flexibility of using joins when I need to without compromising the structure of my files and DB calls.

A possibly related question I have is related to including other models:

class Author
{
    public $author_id;
    public $author_name;

    /* @var Book */ //<--don't really fully understand this but I've seen something like it somewhere
    public (array) $authors_books;
}

I'm still searching for answers, but if you could send a link my way, that would be appreciated.

tereško
  • 58,060
  • 25
  • 98
  • 150
Stephane
  • 1,613
  • 5
  • 20
  • 40
  • I agree. Not sure what this means to you, though. – Stephane Jul 16 '12 at 21:19
  • 1
    the `/* @var ..` comment is for auto-documentation. Tools like [PHPDoc](http://www.phpdoc.org/) can scan your code and generate an API documentation for you. It's also used by some IDEs to generate the code completion hints. – Spudley Jul 16 '12 at 21:27

3 Answers3

3

What you are calling "models" are actually domain objects. They should be responsible for dealing with domain business logic, and have nothing to do with storage.

The storage-related logic and interaction should be handled by separate group of objects. One of most sensible solution is using data mappers. Each mapper can deal with multiple tables and complicated SQL.

As for your query, the result of such query would contain information, appropriate for passing to a collection of domain objects.

BTW, that query is quite useless. You forget that each book can have multiple authors. Take for an example this book - it has 4 different authors. To make this query useful, you should have to do GROUP_CONCAT() based on either author_id or book_id.

When implementing such JOIN statements, the database response most likely will be a collection:

$mapper = $factory->buildMapper('BookCollection');
$collection = $factory->buildCollection('Book');

$collection->setSomeCondition('foobar');
$mapper->fetch( $collection );
foreach ( $collection as $item )
{
    $item->setSomething('marker');
}
$mapper->store( $collection );

P.S. Your code example seem to be leaking abstraction. It is a bad practice to let other structures to access object's variables directly.

P.P.S. It seems that your understanding of model part of MVC is quite different from how i see it.

Community
  • 1
  • 1
tereško
  • 58,060
  • 25
  • 98
  • 150
  • It's difficult to explain how an application works. I'm sure I have some misconceptions, but I'm also unable to fully explain how I'm using these components. My Models know nothing about MySQL. I'm working on further abstraction beyond that for file-system storage, etc, but I'm not there yet. Domain Objects are my Managers and Data Mappers are my Models. My Models are probably too tied to the tables (and their fields) I'm using, so thanks for helping me with that. The problem I'm having is if I really use the models as objects, how do I determine which properties go to which storage area? – Stephane Jul 16 '12 at 22:03
  • Are you saying that you pasted full code of `Author` and `Book` "models" ?! – tereško Jul 16 '12 at 22:08
  • I'm trying not to confuse the question by discussing my particular brand of application, but to me Models include properties and methods related to what the objects are (validation, for example). Managers include properties and methods related to storage and retrieval. So, my Models are passed in to my Managers to retrieve a PDO result set into an object, for instance. They are also used to check whether a field exists in the model. This last bit likely needs to be rethought. – Stephane Jul 16 '12 at 22:15
  • That query isn't real. I was trying to keep it simple. In your buildCollection example, you would be pulling all the data you need to have a book, including author name(s), title, etc. So this means that it's likely that your DB calls are scattered across many classes. I was trying to avoid that, but maybe that's just not possible. That was the main basis of my question, I'm realizing. I just assumed it was what other people did. – Stephane Jul 16 '12 at 22:59
  • You see, in OOP we have something called [SOLID principles](http://en.wikipedia.org/wiki/SOLID_(object-oriented_design)), first of which is SRP (single responsibility principle). It states, that each class should have only one responsibility. Or in other words: each class should have only one reason to change. If you consolidate all the SQL queries in one class, you will essentially implement the [god object](http://en.wikipedia.org/wiki/God_object) antipattern. – tereško Jul 16 '12 at 23:07
  • This might still be the same thing, but I was trying consolidate queries for one table in one place, not all queries. – Stephane Jul 16 '12 at 23:22
  • That would only be worth considering, if your code never had more then a two-way join. In my experience, that is quire rare occurrence. More often i have to deal with 3 or 4 joined tables. And in each project there is at least one 6-way join, which performs something one associates with knife, black candles, burning herbs and chicken .. and not in a good way. – tereško Jul 16 '12 at 23:34
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/13969/discussion-between-stephane-and-teresko) – Stephane Jul 16 '12 at 23:42
  • I'm starting to understand what you wrote in response here a couple months ago. So with your 6 join example: Let's say in one query, I join tables 1-5 and in another query I join tables 3-6. Are you suggesting that the query itself is irrelevant? Is the model just a logical structure for a "noun"? In this example what would the domain objects be? If I have two closely related objects, how do I decide to use the same domain object and when do I decide to create two separate ones? – Stephane Sep 28 '12 at 05:27
  • Let's say you are working on multi-lingual, multi-currency shopping site. It is natural to have `Product` domain object, but the query, that is used for getting the data, will contain tables: `Languages`, `Currencies`, `Products`, `ProductDetails`, `Discounts`, `ProductPrices` and maybe some others. The way you represent the data in DB has very little to do with how you deal with it in PHP. As to "how to decide what should be a domain object" .. well .. you try to create an API, that makes sense. – tereško Sep 28 '12 at 12:53
  • @tereško Your example interest me but what a pity that you aren't providing the class code - it's hard to understand without it. – Hexodus Nov 23 '13 at 07:50
  • @Hexodus , which is the "hard to understand" part? Also, providing the implementation instead of API would be counterproductive, because it would depend on project's precise requirements. – tereško Nov 23 '13 at 09:48
  • @tereško The implementational part. – Hexodus Nov 23 '13 at 09:52
  • @tereško The `$mapper->fetch()` method in your example instantiates new Book objects, fills them with DB data and adds them into internal array, which is inside $collection object ? Is this correct? Thanks – Andrew Aug 05 '14 at 08:23
2

Database joins are an artefact of relational databases, you don't need to model them. You need to model what your data is and how it behaves, e.g. you might have a getBooks() method in your Author instances, or a static getByAuthor() method in your Book class (generally, $author->getBooks() should be implemented as Book::getByAuthor($this), so your Author class shouldn't be concerned with Book's implementation details). It's not always a good idea to automatically instantiate all related data (e.g. instantiate Book instances for all books by a given Author instance, as you seem to be considering with your $author_books property), since this might easily degrade into a "load the entire database to memory for each request" scenario.

lanzz
  • 42,060
  • 10
  • 89
  • 98
  • I get that in principle. Let me ask two related questions: If I'm doing constructor Dependency Injection, would I need to inject for this method independently? Second question: Right now, I'm enjoying having my models (managers actually) have all the queries for a given table. Any advice on this? Should I just rely on my IDE's search capabilities instead of wanting things organized? Or should I put a comment in the model (manager) indicating where things are stored outside of the file...? – Stephane Jul 16 '12 at 21:53
  • `Book::getByAuthor($this->author_id)` is not OOP. So what would be the point in refactoring ? – tereško Jul 16 '12 at 21:55
  • @tereško: Oops, got a little carried away with oversimplifications. – lanzz Jul 16 '12 at 21:56
  • I think the idea still stands. If an Author has books and I want the Author class to have this, I would have a method getBooks() wouldn't I? And in there, I would need to find some way to pull a Book object into an array inside my Author class, right? All this with DI so I'm not making this a huge mess... – Stephane Jul 16 '12 at 22:07
  • I don't think you should inject independently for this method. An author would have all interaction with his books through the same dependency, not only for finding them. I largely agree with [tereško's article](http://stackoverflow.com/a/5864000/727208); you should not aim to have a corresponding model for each table and vice-versa. Do not model your data and class structure on your database schema, this will limit you in many ways, while not giving you any significant advantage. – lanzz Jul 16 '12 at 22:15
  • Thanks. So in this example, though, there likely would be an Author class and a Book class (which would mirror the DB), no? And each class would likely be able to retrieve data about the other: getBooks(), getAuthor(), right? And if I'm not using an object to match my DB, when would PDO->fetchObject(Class) be useful? Or do I need another set of classes? – Stephane Jul 16 '12 at 22:19
  • By all means comment your code extensively; the more abstract OOP is, the harder it is to track your way up the call stack without comments, and it quickly gets impossible to write self-commenting code, when your injected dependency might come from completely different parts of your codebase. – lanzz Jul 16 '12 at 22:19
  • I wouldn't use `PDOStatement::fetchObject()` at all, unless your data is so basic that you do not have a domain object class _at all_ and simply wish to avoid a lot of square brackets and apostrophes as needed with `PDOStatement::fetch(PDO::FETCH_ASSOC)`. You should handle proper instantiation of your data and not rely on PDO to just set a bunch of properties on an instance. – lanzz Jul 16 '12 at 22:21
  • Can you give me an example of when I should instantiate an object when I'm pulling data from the DB? Shouldn't I be able to trust my DB? – Stephane Jul 16 '12 at 22:51
1

If you're trying to model your DB in classes, this is a problem that's already been solved.

I suggest you try out the Doctrine framework, which is a full ORM framework for PHP.

Hope that helps.

Spudley
  • 166,037
  • 39
  • 233
  • 307
  • Thanks. I'll look at this, but this seems like a fairly straight-forward question. Even if I end up using something independent of my project, I still want to understand how to do it. – Stephane Jul 16 '12 at 21:18
  • it's actually a surprisingly complicated question; more so than it sounds. A simple parent-child relationship as described may seem simple, but SQL can generate some very complex relationships, many of which could be very hard to map in a class structure. – Spudley Jul 16 '12 at 21:22
  • Agreed, and I don't mind doing a straight call on some of my more complicated queries (I'm actually more proficient at MySQL than almost any other aspect of PHP apps). I'm just wondering if there's something that people commonly do when they encounter a situation like this (since every site likely has similar associations). – Stephane Jul 16 '12 at 21:36
  • People generally look into using a well thought and proven approach... like [Doctrine](http://www.doctrine-project.org/). Since the source is open, why don't you give Spudley a thumbs up and dive into this beauty? And really: it's not as straightforward as it sounds. – Bart Jul 16 '12 at 22:13