Family Fortunes: Saving and Updating Laravel Relations

Although I’ve been using Laravel heavily for over two years, I still find things there that I didn’t really know about.

Today I took some time to really understand all the different ways of saving and associating related models. I had misunderstood the docs at first, but after some searching and playing with test code I’ve come to a better understanding.

One-To-Many and Belongs-To Relationships

To illustrate I’ll use an example Parent and a Child class. The docs used Post, Comment, User and Roles which threw me off, so I hope that my naming convention is clearer.

class Parent extends Model
{
  public function children()
  {
    return $this->hasMany(App\Child::class);
  }
}

class Child extends Model
{
  public function parent()
  {
    return $this->belongsTo(App\Parent::class);
  }
}

I always use plurals for hasMany() relationships as a reminder to myself how it goes.

The first example is saving a Child object to an existing Parent:

// Find an existing Parent record
$parent = App\Parent::find(2);

// Creates a new Child instance 
$child = new App\Child(['name' => 'mei']);

// effectively sets $child->parent_id to 2 then saves the instance to the DB
$parent->children()->save($child);

If called repeatedly, the above code will continue to save Child records with the same Parent.

So far so good. We now want to do the reverse of the above.

Let’s say we want a new parent for our child (programmatic adoption?):

// a new Parent object
$parent = App\Parent::find(31);

// updates $child->parent_id to 31
$child->parent()->associate($parent);
$child->save();

Our Child had parent_id of 2. Above, associate set the parent_id property of $child to 31, then $child->save() persists the change to the database.

Of course, it’s possible to for an object to have more than one parent relation (for example, a Comment can belong to a Post and an Author). As the association is only confirmed when save() is called, we can simply call associate with another parent object.

$parent = App\Parent::find(31);
// a new parent! 
$anotherParent = new App\AnotherParent(['name' => 'steve']);

// associate with our parent, as above
$child->parent()->associate($parent);

// set the second relationship 
$child->anotherParent()->associate($anotherParent);
$child->save();

Assuming that foreign keys are set, calling $child->save() before setting the second relationship would throw an error.

Now that the relationship is set, it’s also possible to sever it.

// sever the parent-child relationship
$child->parent()->dissociate();
$child->save()

In this example $child->parent_id is set to null. The save() method must be called to persist the change to the database.

Again, if foreign keys have been set an error would be thrown here. This code would only work if the foreign key (parent_id) is nullable.

Many-To-Many Relationships

Let’s move on from Parent and Child.

To illustrate the Many-To_-Many relationship we’ll use Post and Tag. These two are connected with the belongsToMany() method.

class Post extends Model
{
  public function tags()
  {
    return $this->belongsToMany(AppTag::class);
  }
}

class Tag extends Model
{
  public function posts()
  {
    return $this->belongsToMany(AppPost::class);
  }
}

A Post can have zero or more tags, and a Tag can have zero or more posts (in theory anyway, an unused tag is mostly pointless).

Such a relationship allows us to get all tags attached to a Post, and conversely get all posts attached to a Tag.

Unlike one to many relations, both records must be persisted before the following to work. The pivot table must have both ids.

In this example, a tags are added to a post.

$post = App\Post::find(3);

$tag = App\Tag::find(47);

$post->tags()->attach($tag->id);

The first argument of attach() is an id, but an Eloquent model can also be passed:

$post->tags()->attach($tag);

What happens above is that the pivot table gets a new record, with post_id set to 3 and tag_id to 47.

If your pivot table has extra fields, these can be set by passing an array as the second argument. The parent (in this case Post) model’s timestamp can be touched by passing in true as a third argument:

$post->tags()->attach($tag, ['expires' => date('Y-m-d H:i:s')], true);

To sever the relationship, detach() is used.

A single id or an array can be passed to detach(), or called without any arguments which detaches all of the parent’s relations.

// detaches $tag from $post
$post->tags()->detach($tag->id);

// detaches tags with the following ids from $post
$post->tags()->detach([41, 52, 54]);

$post->tags()->detach();

The first and second methods deletes rows corresponding to the $post with the given tag ids. The last method deletes all rows from the pivot table with that $post’s id.

Finally, the sync() method is provided to make life just a bit easier for us.

Let’s assume we’ve edited the post, and have remove one tag and attached another.

$post = App\Post::find(4);

// $post has tags with ids: 32, 45, 47

// tag 32 is no longer required, and we need to add tag with id 86

// so instead of this:
$post->tags()->detach(32);
$post->tags()->attach(86);

// we do this
$post->tags()->sync([45, 47, 86]);

Here the new tags [45, 47, 86] will be persisted, and tag 32 will be deleted from the pivot table. This also removes any logic for finding out which tags already exist for the post which are not meant to be removed.

Conclusion

Having spent time playing with this I have a much better appreciation of how it works.

Again, Laravel and Eloquent provide an elegant interface to the database, and lets us write readable code.

I’ve picked up a few points here myself, and I’ll be taking a look back at a few of my own projects to see where I can improve them.