Active Record, Foreign Key Mapping and Lazy Loading

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Active Record, Foreign Key Mapping and Lazy Loading

Post by Ambush Commander »

I have an object structure where an object can contain references to other objects (usually in a one to many relationship) (Foreign Key Mapping). The hierarchy is Author 1->* Story 1->* Chapter. I've made it so that when you instantiate an Author object, it doesn't load any of its Stories (lazy loading). You can either pass an optional argument via the constructor or instantiate them later. And, of course, all of the database access is done in the class (Active Record).

I couldn't figure out something though. Suppose you want your object to be unlazy and load everything in one kaboodle. However, the Author passes off the Story object array initialization to the Story object, and the Story object passes off Chapter object array initialization to the Chapter object, so if I want Author to instantiate all Stories and all Chapters, do I have to bubble the parameter all the way down the object hierarchy? Or is there a better way? That is my first question.

My second question pertains to the independence of Story objects. Suppose I want to display a certain Story on a page, but I don't want to bother instantiating an Author class (the script will handle a Story object). There are certain things that a Story class has to be aware of (like the full name of the author, the author's default CSS settings). Does the story class get extra fields that pertain to a different database (and break the tidy Active Record scheme), do I get a condensed Author object (probably Lazy Loading) just for the story, or do I just scratch the idea and instantiate the Author object anyway? That is my second question.

A few days into PoEAAs lessons... I'm sure I've got something wrong. I'm trying to refactor from a Transaction Script to Domain Model (took me forever to figure out what the domain model was about), and I think I've got the essence of the paradigm shift down. And this seems like a big of deja vu... it is. I asked a similar question here. In retrospect, though, I don't think I got the answer to question #2.
User avatar
sweatje
Forum Contributor
Posts: 277
Joined: Wed Jun 29, 2005 10:04 pm
Location: Iowa, USA

Post by sweatje »

You might have better luck with smart factories. Internal to each factory, you could implement a registry to track already loaded objects. That way you never need to fire off a query against the database twice, if the author/story/chapter has already been loaded, then the object in the cache is returned instead. With that kind of a setup, it does not matter if the author is queried first, cascading down through stories and chapters, or if a story is invoked first, and then the loads the chapters and then the author.

HTH
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Actually, I do have that implemented in the Active Record functions themselves (an Identity Map if I'm not mistaken).

I'm assuming that you tried to answer question one. You're right, I shouldn't make any dependencies between the objects: they should be able to be independently initialized, and perhaps you add references to the appropriate objects later. Hmm... that means more functions and a Table Module?

This is what I have currently. Come to think of it, the constructor looks a bit funky...

Code: Select all

class TWP_Author {
    
    var $user_name; //ex johndoe
    
    var $author_name; //ex John Doe
    var $author_screenname;
    var $author_website;
    var $author_websiteName;
    var $author_email;
    var $author_dateJoined;
    var $author_summary;
    var $author_font;
    
    var $author_projectManager;
    var $author_projectParticipant;
    var $author_exhibit;
    
    var $story;
    
    function TWP_Author(
      $user_name,                   $author_name,
      $author_screenname,           $author_website,
      $author_websiteName,          $author_email,
      $author_dateJoined,           $author_summary,
      $author_font,                 $author_projectManager,
      $author_projectParticipant,   $author_exhibit) {
        
        $this->user_name                 = $user_name;
        $this->author_name               = $author_name;
        $this->author_screenname         = $author_screenname;
        $this->author_website            = $author_website;
        $this->author_websiteName        = $author_websiteName;
        $this->author_email              = $author_email;
        $this->author_dateJoined         = $author_dateJoined;
        $this->author_summary            = $author_summary;
        $this->author_font               = $author_font;
        $this->author_projectManager     = $author_projectManager;
        $this->author_projectParticipant = $author_projectParticipant;
        $this->author_exhibit            = $author_exhibit;
    }
    
    function &find($user_name, $story = false, $chapter = false) {
        //Active mapper
        $TWP_GLOBAL =& TWP_Global::instance();
        $result =& $TWP_GLOBAL->mapGet('Author',$user_name);
        if ($result) {
            return $result; //Identity Map
        }
        
        $db =& $TWP_GLOBAL->mapGet('Database_Text_Cache',$user_name); //DB cache
        if ($db) {
            $authinfo = $db['authinfo'];
        } else {
            $db = array();
            //Database access.. convoluted eh?
            $readfile_parameters = $user_name;
            include (DIR_INCLUDES.'readfile.php'); //creates $sinf, $sc and $authinfo vars
            $db['authinfo'] = $authinfo;
            $db['sinf'] = $sinf;
            $db['sc'] = isset($sc) ? $sc : array();
            $TWP_GLOBAL->mapSet('Database_Text_Cache',$user_name,&$db);
        }
        unset($db);
        
        $author_font = isset($authinfo['defont']) ? $authinfo['defont'] : '';
        $author_projectManager = isset($authinfo['pm']) ? $authinfo['pm'] : array();
        $author_projectParticipant = isset($authinfo['participated']) ? $authinfo['participated'] : array();
        $author_exhibit = isset($authinfo['exhibit']) ? $authinfo['exhibit'] : array();
        
        $result =& new TWP_Author(
          $user_name, $authinfo['author'][1],
          $authinfo['screenname'], $authinfo['website'][0],
          $authinfo['website'][1], $authinfo['email'],
          $authinfo['joined'], $authinfo['summary'],
          $author_font, $author_projectManager,
          $author_projectParticipant, $author_exhibit);
        
        $TWP_GLOBAL->mapSet('Author',$user_name,&$result);
        
        if ($story) {
            $result->findAllStory($chapter);
        }
        
        return $result;
    }
    
    function findAllStory($chapter = false) {
        $this->story =& TWP_Story::findAllForAuthor($this->user_name,$chapter);
    }
}
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Nevermind, I figured it out. Lazy Loading for fields is overkill: it usually isn't worth the complexity even when the content of the field is big. So if you need some of the Author, just take it all. Hmph.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Quick check: can you have joint authorship? With a many-to-many relationship between authors and stories, Association Table Mapping could be the way to go.
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Quick check: can you have joint authorship? With a many-to-many relationship between authors and stories, Association Table Mapping could be the way to go.
Yeah. I was planning to express that relationship between the chapters (chapter to author, one to one relationship), but that's certainly novel. I'll think about that (and read up on it).

One last question... is there anything inherently "wrong" about the constructor, or is this okay?
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

I think too many parameters is definitely a code smell. More smells here.

I've got to nip out for a bit but I've been thinking about your problem and I'll try to post again later.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Re: Active Record, Foreign Key Mapping and Lazy Loading

Post by McGruff »

A Dependent Mapping could be tailor made for this situation ie if each story only has one author and in turn each chapter only has one story. However, I'm not sure about the delete/insert updates you get with Dependent Mapping. This could be OK with small numbers of dependents but if you have very many stories per author, you'd end up with a huge number of queries just to change one word in one chapter.

How about something like this to solve the lazy loading problem. Author will be responsible for loading itself and whichever stories you specify in the constructor parameter. In turn, a Story would be responsible for loading any chapters specified in the $chapter_ids parameter. It should also be possible to load an object tree with a single story id or a single chapter id - no author id required.

Code: Select all

class Author
{
    var $author_id = false;
    var $stories = array();
    // etc 

    /*
        param (object)  $db
        param (integer)   $author_id
        param (array)   $story_ids
        param (array)   $chapter_ids
    */
    function Author(&$db, $author_id = false, $story_ids = array(), $chapter_ids = array())
    {
        $this->_db =& $db;
        if( !$this->_setAuthorId($author_id, $story_ids, $chapter_ids)) {
            # will some kind of "null object" flag be needed?
            return;
        }
        $this->_loadThis();
        $this->_loadStories($story_ids, $chapter_ids);
    }
    function _setAuthorId($author_id, $story_ids, $chapter_ids)
    {
        if(false !== $author_id) {
            $this->author_id = $author_id;
            return true;
        } elseif(count($story_ids)) {
            // db query: find author id using any of the story ids 
            // (all story rows will have the same author_id value)
            $this->author_id = $row['id'];
            return true;
        } elseif(count($chapter_ids)) {
            // db query: find author id using any of the chapter ids 
            // (all chapter rows will have the same author_id value)
            $this->author_id = $row['id'];
            return true;
        } else {
            $this->author_id = false;
            return false;
        }
    }
    function _loadThis()
    {
        // db query: find author by $this->author_id
        // set author properties
    }
    function _loadStories($story_ids, $chapter_ids)
    {
        if($story_ids == 'all') {
            // db query: find all stories
            // create story objects and add to $this->stories (array)
        } elseif(count($story_ids)) {
            // build & execute db query with an "and" clause for each story id
            // create story objects and add to $this->stories (array)
        } elseif(count($chapter_ids)) {
            // get story id from chapter query (use $chapter_ids[0] - all will have the same story id)
            // create Story object and add to $this->stories (array)
        }
    }

    // etc

Code: Select all

// a new 'blank' Author:
$author =& new Author($db);

// load everything for an existing author:
$author =& new Author($db, 122, 'all', 'all');

// load with specified stories, all chapters
$author =& new Author($db, 122, array(12, 45, 32), 'all');

// load with just a story id:
$author =& new Author($db, false, array(12), 'all');

// ..or a chapter id:
$author =& new Author($db, false, array(), array(3));
You'll always get a tree of objects: author --> stories --> chapters with a variable number of stories and chapters depending on what you asked for. When you want to focus on a single story, you can instantiate Author with the single story id. Now you've got access to all the Author properties and you can pull out the Story object (a getStory method) for manipulation/display.

In saying this, I'm not sure if, when the classes are written, I might want to refactor all the data access stuff out into a Data Mapper. Also, it feels odd working "blind" without unit tests as a guide.. ;) I'm not used to that.
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

McGruff wrote:A Dependent Mapping could be tailor made for this situation ie if each story only has one author and in turn each chapter only has one story.
Alas, if only it where that simple. I've decided that the relationship between author/story is bidirectional and will require a Association Table...

Well, I suppose most of the stories have single ownership. The thing is, there is a significant minority that don't, and I'm not sure whether or not mixing it up will be a good thing.
How about something like this to solve the lazy loading problem. Author will be responsible for loading itself and whichever stories you specify in the constructor parameter. In turn, a Story would be responsible for loading any chapters specified in the $chapter_ids parameter. It should also be possible to load an object tree with a single story id or a single chapter id - no author id required.
That would work... passing arrays of ids... never thought of that...
In saying this, I'm not sure if, when the classes are written, I might want to refactor all the data access stuff out into a Data Mapper.
All in good time. Active Record was the easiest to implement initially because it jived well with the original Transaction Scripts.

I'll take all of your code into consideration. Thanks for the tips. Part of the problem is I'm not even sure what the schema is: the tables for the object's I'm trying to define don't even exist!
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Ambush Commander wrote:I'm not even sure what the schema is: the tables for the object's I'm trying to define don't even exist!
With TDD and a mock Database object, you don't need to bother too much about the db schema while you're piecing your ideas together. You can just dive in and see what turns up. The mock can be set to return whatever fields you want in different tests.

As soon as you are sure of the schema, I'd switch to testing against a real database: a mock db can't verify the SQL statements.
Post Reply