Handling Model Relationships

One of the most common things that people ask about is how to handle database relationships in CodeIgniter 4.  I feel that for people that have used CodeIgniter for awhile this is not a concern, but newer developers in the framework seem to expect some form of ORM. And that's fair. However, that's also never been something that  has been promised for CodeIgniter. While I've played around with the beginnings of one, it will never be introduced
into core. If I ever do get something complete with it, then its possible an official package might show up - but don't start holding your breath just yet. :)

I want to take a couple of articles then, and look at ways that we can work with relationships between our models. How you handle this depends entirely on your preferences and whether you're using Entities or not. For these examples, I will be using a User entity class.

Because we're using an Entity, we need a quick refresher. Entities should never know anything about the persistence layer. That's the role of the Repository. In straight CodeIgniter, the Model is the closest thing to a Repository, so we'll use that. And, yes, I know it doesn't match a perfect definition of a repository.

One thing that I'm sure will come under fire is that with an ORM you don't have to create your own methods to do  this, so it makes creating an application faster and easier. I can't argue with that. ORMs come with a lot of baggage,  though. They do a lot of stuff implicitly behind the scenes, and CodeIgniter has always been a more explicit framework than a lot of the others. That's one of the things many people like about it from what I've heard over the
years. ORMs have often been seen to slow down applications because of all of the hidden work they do to optimize things when you can easily optimize it yourself by writing just a little bit of code. When something goes wrong in a query - you know exactly where to see what's going on. No spending hours debugging something that the ORM is doing to your complex query when an edge case breaks.

Please don't take that the wrong way - I think ORMs serve a very important purpose for a lot of people and work just fine most of the time. That's just not how CodeIgniter is built currently.

Retrieving Relationships - Single Entity

When you're grabbing only a single User and need to pull some related data, it's pretty straightforward: create a method in the UserModel to load it for that class. Something like this:

public function getCommentsByUser(User $user)
{
   $commentModel = new CommentModel();
   
   $user->comments = $commentModel
       ->where('user_id', $user->id)
       ->orderBy('created_at', 'desc')
       ->findAll();
       
   return $user;
}

In your controller you might grab your user and populate it like:

public function comments(int $userId)
{
   $userModel = new UserModel();
   
   $user = $userModel->find($userId);
   
   // error checking, etc go here
   
   $user = $userModel->getCommentsByUser($user);
   
   echo view('app.comments', [
       'user' => $user
   ]);
}

For a single user at a time that works just fine. It's clear, the methods names are descriptive, and makes your controller code very readable. You might want to move the CommentModel instance to a class var, or a service, or something if it gets used a lot in your application. For our simple example, there's no performance hit since we only will instantiate it once.

Whether this method gets put in the UserModel or the CommentModel is purely a matter of preference. I used the UserModel here for brevity sake.

Saving Relationships - Single Entity

Saving related data is just about as simple. We simply create a new method to handle it in the appropriate Model:

public function updateUserComments(User $user)
{
   if (empty($user->comments)) return;
   
   foreach ($user->comments as $comment)
   {
       $foundComment = $this->commentModel->find($comment->id);
       
       if (! $foundComment instanceOf Comment:class)
       {
           $foundComment = new Comment();
       }
       
       $foundComment->fill($comment->toArray());
       $this->>commentModel->save($foundComment);
   }
}

Sure, this could be optimized a bit, but it works and, once again, is relatively simple to figure out what it does for the new developer on the team that gets thrown into the project with no ramp up time.

Retrieving 1-to-1 - Multiple Entities

This all breaks down a bit when you have multiple records that you need to populate relationships for. If you do it the way described above, you quickly hit the N+1 problem and your performance will not be ideal and, if pulling larger amounts of records, could slow it to a crawl or even crash the system, neither of which are ideal. Again,  the answer is a new method in the model of your choice:

// expects an array of User objects.
public function populateComments(array $users)
{
   if (! count($users)) return $users;
   
   // Rebuild the array so the keys are the user_id for easier assignment later
   $newUsers = [];
   foreach ($users as $user)
   {
       $newUsers[$user->id] = $user;
   }
   $users = $newUsers;
   unset($newUsers);
   
   // Grab all of the user ids so we only do one query.
   $userIds = [];
   foreach ($users as $user)
   {
       $userIds[] = $user->id;
   }
   
   $comments = $this->commentModel
       ->whereIn('user_id', $userIds)
       ->findAll();
       
   // Assign each of the comments to their appropriate user.
   foreach ($comments as $comment)
   {
       if ($user[$comment->user_id]['comments] === null)
       {
           $user[$comment->user_id]['comments'] = [];
       }
       
       $user[$comment->user_id]['comments'][] = $comment;
   }
   
   return $users;
}

The critical flaw of this method is that it will reorder your array of users during the process as an optimization step. If that's not critical, then this method works great. Otherwise, you'd need to tweak it to skip that optimization step,  which would likely end up with deeper looping. While that would impact performance, it's probably a lot less of an impact than hitting the database N+1 times, so you'd still come out ahead.

Stepping through the process, the first thing we do is to reorganize the incoming array of users so that the index of each one is the id of the user. We do this in a single loop here, instead of looping over each of the users for every comment. Now we can easily get any user by $users[$userId].

Now for the big optimization: we'll grab all of our user ids into a single array. We'll then use those as part of a  single query that fetches all of the comments for all of the users at once. Huge performance savings right there. Even though we do several loops over objects within the method, we still a huge gain from not hitting the database
but once.

Finally, we loop over all of the comments and assign them to the correct user.

---

That's the basics of how I would handle relationships for models in CI. If you need a full-on ORM, your only solution at the moment is to pull in a third party solution, like Laravel's Eloquent, or Doctrine, etc. Once you do that, though you're using a completely different set of database tools instead of CodeIgniter's.

What do you think about this approach? Is this similar to what you've done in the past? Do you have suggestions to improve it? Leave comments below and we'll discuss!

I think for next month I might take a crack at a helper to automate some of this for us since repetition isn't fun.

Tier Benefits
Recent Posts