Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Roni's avatar
Level 33

Non Standard Relationship Issue, is it a bug or is it me?

I've been trying to leverage eloquent more, as my models are becoming bloated with functionality that seems to "already" be handled by eloquent, but I can't get some relationship features to work. This issue centers around adding and updating items in a very simple pivot. I have several of these pivots around a series of objects that can all be interconnected. Servers can be under a router, users on a server, users on a router, security layers etc...

It's not that big a system and the logic is simple enough, but the many to many interconnects are killing me.

Laravel 5.1.33

Here is some working code

self::synchronizeUsers($servicePlanId,$user_id);
// This should be done via relationship, but instead I'm making M:N models
// here is the function being run.
...
    public static function synchronizeUsers($servicePlanId,$userId) {

        $servicePlansGroups = ServicePlan::groupsIDsArray($servicePlanId);
        foreach ($servicePlansGroups as $servicePlansGroup){
            SyncUserGroup::addOrUpdate($servicePlansGroup,$userId);
        }
    }

in my other class, here is the addOrUpdate function. I just made a model that fits over the existing table that I made for the M:N relationship. That was a quick easy fix, but now I'm duplicating eloquent expected behavior, and I'll have to do it everywhere I have M:N's, which seems like a poor solution.

Here is the class SyncUserGroup, that is doing the job of the relationship:

    public static function addOrUpdate($group_id,$user_id) {

        $model = SyncUserGroup::where('group_id',$group_id)->where('user_id',$user_id)->first();
        if(is_null($model)){
            SyncUserGroup::create(['group_id'=>$group_id,
                                   'user_id'=>$user_id]);

        }else{
            SyncUserGroup::where('group_id',$group_id)->where('user_id',$user_id)
                ->update(array('last_verified_on' => date('Y-m-d H:i:s')));
        }
    }

Now, even though this all works fine, It just bothers me that I can't get this relationship to work, so here's my second attempt. With all the relationships: So I'm I'm getting a userobject, relating a service plan to the user, and then calling all the groups (apps, which are hosted on servers and adding the user to the group.

    foreach ($this->plan->groups as $group)
            {
                // I used attach detach because I couldn't find a good test to
                // run against searching for the key,I found one in the next
                // iteration, but it exposed another weird issue.

                $group->users()->detach($user_id);
                $group->users()->attach($user_id);

            }

// here are the associated relationships on the vpnuser object
    public function groups() {
        return $this->belongsToMany('\App\Group', 'sync_user_groups','user_id','group_id');
    }

    public function plan() {
        return $this->belongsTo('\App\ServicePlan', 'service_plan_id');
    }

// Here is one other side of the plan relation, the service plan to group

    public function groups() {
        return $this->hasMany('\App\Group','service_plan_id','id');
    }

    public function vpnuser() {
        return $this->hasMany('App\VpnUser','service_plan_id','id');
    }

// And finally the group to vpnuser

    public function servicePlan() {
        return $this->belongsTo('\App\ServicePlan','service_plan_id');
    }

    public function users() {
        return $this->belongsToMany('\App\VpnUser','sync_user_groups','group_id','user_id')
            ->withPivot('last_verified_on')
        ;

I haven't calculated the server cost, but I thought an update might be simpler then detach and attach , however the contains() method is a super expensive server method downloading the entire pivot and using a collection to filter it, so, I thought I'd write a "hasPivot" function and add it to the mix. I found this on stack exchange, but the error that it's throwing shows a really weird SQL error that in my mind is showing me something else is happening or I'm getting a bit lost.

So I've changed detach(), attach() to the follwing

        foreach ($this->plan->groups as $group)
            {
                if($group->hasPivot('users','user_id',$user_id))
                    $group->users()->updateExistingPivot(array('last_verified_on'=>time()));
                else
                    $group->users()->attach($user_id);
            }

The hasPivot code function found in the controller looks like this:

function hasPivot($relation_name, $searchKey,$keyValue) {
    return (bool) $this->$relation_name()->where($searchKey,'=',$keyValue)->count();
}

This blows up to an error that has me writing this post I could probably figure out how to add the table name to the search key, so I'm not worried about the ambiguous call. The big issue is on the join

QueryException in Connection.php line 655:
SQLSTATE[23000]: Integrity constraint violation: 
1052 Column 'user_id' in where clause is ambiguous 
(SQL: select count(*) as aggregate from `vpnusers` 
inner join `sync_user_groups` on `vpnusers`.`id` = `sync_user_groups`.`user_id` 
where `sync_user_groups`.`group_id` = 15 and `user_id` = 21)

vpnusers.id = sync_user_groups.user_id where Specifically sync_user_groups.id???? Where is that comming from? The relationships all state sync_user_groups.user_id specifically.

If you are running into something similar, Read this thread to the end!!! Thanks.

0 likes
8 replies
ShaneTurner's avatar

"It's not you, it's me."

Sorry, not at all helpful. But I couldn't help myself.

1 like
Roni's avatar
Level 33

I know, relationships can be hard.... I'm just trying to work it out :)

jekinney's avatar
Level 47

Your way over complicating it. Your relationships look good I think.

Lets get a user real quick

// Lets set up a user to a group, either a single id or array of ids. 
// WARNING:Keep in mind though, you may end up with duplicates.
  $groupId = 1;
  $user = User::find(1);
  $user->groups()->attach($groupId);

// Lets pass in an array of groups. Like from a form that has checkboxes to 
// assign a user to groups
$groups = [1, 2, 3, 4, 5];
$groupId = 1;
 $user = User::find(1);
$user->groups()->sync($groups); 
// Sync method with automatically detach all the user's groups and accepts an array of ids.
// Basically runs detach() and then attach() for you in one go

// Lets pass in group objects/collections
$groups = Group::where('is_cool', 1)->get();
$user = User::find(1);
$user->groups()->saveMany($groups);

// lets pass in a single group/collection
$group = Group::where('is_cool', 1)->first();
$user = User::find(1);
$user->groups()->save($group);

//Obviously detach is the same as attach but removes the connection instead of adding. BUT
$user = User::find(1);
$user->groups()->detach();
// This will detach ALL groups

// If your pivot table has timestamps be sure to add at the end of the relationship
->withTimeStamps();

// If your pivot table has other columns 
// (generally they shouldn't but I have on
//  roles if the role is attached to a team or three way pivot table)
// Add to your relationships
->withPivot('team_id');

// In action
$user = $user->with('groups')->find(1);

foreach($user->groups as $group)
{
    $group->pivot->created_at;
    $group->pivot->team_id;
}

// Update a pivot table that has other columns
$user->groups()->updateExistingPivot($groupId, ['team_id' => 4]);

// for me on doing checks you can use:
$user->groups->contains(); //Pas in the group id.
//OR
foreach($user->groups as $group)
{
    if($group->id == 1)
    {
        return true;
    }
    return false;
}
Roni's avatar
Level 33

Hi @jekinney,

There are some things that I can't use, like contains for example, if there are 100,000 records on the pivot or more, getting searching the collection is too costly, especially if this is run many times per day, per user. I saw the sync command, but the way it read to me, was that it purges the whole pivot, not just the pivot tuples related to the current group. So for example if I sync 1 users records, I expect all the other users records are gone. I'll double check it on my test DB in a few min and post a followup.

Also, this app is connecting to another existing application, so I have to use the keys that they have in the tables that I'm pulling from that DB. So there are many tables that don't have a primary key of id, or where In the case of my pivot where I would normally use a combined key pair, I can't due to laravel.

-Roni

Roni's avatar
Level 33

@jekinney, thanks for the advice, but I haven't been able to integrate it into my code. Simple examples are often just that, simple. But once it gets ugly, not knowing how something internally works is well, really ugly! Granted I'm trying to tear this all apart and possibly over complicating it, but here is a perfect example why:

While I was fiddling with this getting ready to test out the sync function to see if it syncs the whole pivot or just the related rows, I found something so weird that was happening, but I can't see how it's happening in my code.

This side app is just a form generator app that runs a series of server maintenance commands. It's all forms and views. With interconnected objects. But... It generates a huge number of different forms. Now, I wouldn't want to write classes for all these, commands, it would take forever and be insane. Hundreds of controllers etc...

So I am running essentially hundreds of forms on just 7-8 models representing the things that are being controlled. The forms are all DB generated and that is all working great.

Now this form, has a user, a server and a plan to support my first option of static class calls for the M:N relations.

With the internal relations, I have no need of anything, the plan server etc... is all derived from the user. So I took the ServicePlan Field and the server fields out of the form.

And got a huge shock...

This code, which has worked fine from the outset died:

            foreach ($this->plan->groups as $group)  //<- DEAD RIGHT HERE
            {
                // I used attach detach because I couldn't find a good test to
                // run against searching for the key,I found one in the next
                // iteration, but it exposed another weird issue.

                $group->users()->detach($user_id);
                $group->users()->attach($user_id);

                
            }

This was never the issue, so I added this code in front of it

    dd($this->plan);  //THIS NOW RETURNS NULL!!!!! WTF!!! HOW CAN MY FIELDS AFFECT THIS RELATIONSHIP??

So I put the SELECT field back in the mix, which is populated by a static call to ServicePlans, and everything works again.

I threw a toSql on there to check it: I removed and added the field to check and see what it affects

dd($this->plan()->toSql()); 

// With un-needed the field on the form, I have the expected
"select * from `servicePlans` where `servicePlans`.`deleted_at` is null and `servicePlans`.`id` = ?"

// Without the field
"select * from `servicePlans` where `servicePlans`.`deleted_at` is null and `servicePlans`.`id` is null"
 

How can that be? It should be 100% unrelated! The relationship itself is changing based on some field values! Or in memory calls.

Ideas welcome

-Roni

Roni's avatar
Level 33

Found it, why programming at 3:30 AM is not a great idea:

The model is instantiated here, in a controller before saving off request object fields,

$model_name = $data['model']['name'];
$full_model_name = $this->extractFullModelName($model_name);
$model = new $full_model_name($request->all()); 
// IF NO DATA IN REQUEST ALL PASSES MASS ASSIGNMENT, IT HAS NO STARTING POINT
// AND ESSENTIALLY MAKES A NULL OBJECT. AND ALL THE RELATIONS ARE NULL.
// THAT EXTRA FIELD GAVE IT A STARTING POINT THAT PASSED MASS ASSIGNMENT.

$model->save($data);

Also, sync didn't blow away the other data, which was nice, I've switched it over.

1 like
Roni's avatar
Level 33

OK, this appeared solved but it wasn't... however it is now, and here is the solution... which digs into the SQL construction of the belongsToMany

Basically, I had some DB tables I couldn't manipulate, that needed to be related, now, I was trying to override the relationship in a way that joined to a non-standard naming convention. Meaning specifically, I wanted it to relate my parent table's field "user_id" to the pivot tables foreign key user_id.

Now here is the secret sauce, it wasn't possible to do. The way belongs to many works, is that it finds the primary key or "id" and relates that field. so the initial join in the belongs to many relationship is always done to the "id" field primary key. Normally in this situation you are using a combined key which laravel can't handle.

Anyhow, to solve this, simply add a belongsTo and add your own new model and db table which you can manipulate, remember the other one was locked by another app. Now on the new model "foo", you can have your pkey id = to your user_id, or the field you couldn't join on before, and now it just follows convention.

So to use the first code block on this post as an example, I took this our of class group and moved it into foo

    public function vpnusers() {
        return $this->belongsToMany('\App\VpnUser','sync_user_groups','group_id','user_id')
            ->withPivot('last_verified_on');

//and changed it too

    public function foos() {
        return $this->belongsToMany('\App\foo','sync_user_groups','group_id','user_id')
            ->withPivot('last_verified_on');

after that I opened class foo and added

public function vpnuser(){
        return $this->belongsTo('App\VpnUser');
    }

// and in vpnuser
public function foo() {
        return $this->hasOne('App\foo');
    }

so you before you couldn't do

$group->users;

becasue the join was wrong, now you can simply do a

$group->foo->users;

and the magic is back!

jlrdw's avatar

You're not referring to one of those relationships when someone says hi meet my wife and my sister and there's only one woman standing there? Eloquent will do you no good in this case.

1 like

Please or to participate in this conversation.