Home » Php » php – How to scope an Eloquent relation (or perform traditional join)

php – How to scope an Eloquent relation (or perform traditional join)

Posted by: admin July 12, 2020 Leave a comment

Questions:

I’m trying to build a set of Eloquent models, which represent an existing database of hardware devices (neither of which can be changed). I know how to do this in SQL, but am struggling to structure one of the model relations which uses a third table, similar to a relationship/junction table but for a one-to-one relation with a composite key.

There are three entities (simplified):

  1. device
  2. session
  3. device_user

A user can be in many devices at once and has session logs associated with those devices. Users do have a unique ID, but from the device’s perspective they only have a “user number” which is only short (3 bytes) and so cannot represent the entire range of users, therefore it is mapped in the device_user table. (It’s actually more complex than this, but for the purposes of this question I’ve stripped it back)

device table:

d_id                PK
[data fields...]

device_user table:

du_uid              User's actual ID
du_device_id        FK to device.d_id
du_number           000-999
[metadata...]

session table:

s_device_id         device.d_id
s_user_number       000-999 (device_user.du_number)
[data fields...]

The scenario: I have a session, and I want to look up the specific device_user.d_uid. In SQL I’d do something like:

SELECT session.blah, du_uid
FROM session
INNER JOIN device_user ON du_device_id = s_device_id AND du_number = s_user_number

So I guess that makes it effectively just a relationship on a composite key.

What I’ve tried in Eloquent is like this:

class SessionLogModel {

    public function device(){
        return $this->belongsTo('MyModels\DeviceModel', 's_device_id', 'd_id');
    }

    public function user(){
        return $this->belongsTo('MyModels\DeviceUserModel', 's_user_number', 'du_number')

        // A) I tried:
        ->withDevice($this->s_device_id);

        // or B) I tried:
        ->withDevice($this->device());

    }

    // example usage
    public static function getRecentUser(DateTime $localTime, $deviceId){
        $u = null;

        // get the preceding session log
        $q = SessionLogModel::where('session_type', '=', 'S')
            ->where('session_device_id', '=', $deviceId)
            ->where('sesison_date', '<=', $localTime)
            ->orderBy('session_id', 'DESC')
            ->take(1)
            ->with('device')
            ->with('user');
        $s = $q->get()->first();

        $u = $s->user->du_uid; // use the unique user ID
        ...
    }
}

class DeviceUserModel {
    // A)
    public function scopeWithDevice($query, $device_id){
        return $query->where('du_device_id', '=', $device_id);
    }
    // OR B) I tried:
    public function scopeWithDevice($query, $device){
        return $query->where('du_device_id', '=', $device->d_id);
    }
}

I’ve tried a number of ways to limit the match to both columns with a scope or other ‘where’ constructs, but always seem to have trouble “sending” the right value via the BelongsTo. The device ID comes through as NULL, when inspecting the DB::getQueryLog. However if I hard-code a value in the belongs I can see it “working”.

I have researched this quite a lot, but am finding it difficult to find a similar construct illustrated.

I am using Eloquent from Laravel v4.2 used standalone (not in Laravel).

Is the above scope-based approach going to work?
Or should I be looking at a different method?

How to&Answers:

I have just came through this interesting question:
I tired to simulate your tables in laravel as follows as follows:

public function up(){
    Schema::create('session', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('s_device_id');
        $table->string('s_user_number',20);
        $table->timestamps();
    });
    Schema::create('device', function (Blueprint $table) {
        $table->increments('d_id');
        $table->string('blah',20);
        $table->timestamps();
    });
    Schema::create('device_user', function (Blueprint $table) {
        $table->integer('du_device_id')->unsigned();
        $table->integer('du_uid')->unsigned();
        $table->string('du_number',20);
        $table->primary(['du_device_id', 'du_uid']);//important composite key
        $table->timestamps();
    });
}
//then do the relations on Medels:
//User Model
public function deviceUser(){
    return $this->hasOne(DeviceUser::class,'du_uid');
}
//Device Model
public function deviceUser(){
    return $this->hasOne(DeviceUser::class,'du_device_id','d_id');
}
//DeviceUser Model
public function device(){
    return $this->belongsTo(Device::class,'du_device_id','d_id');
}

public function user(){
    return $this->belongsTo(User::class,'du_uid');
}
//Session Model //[Not the Session Facade of Laravel]
public function device(){
    return $this->belongsTo(Device::class,'s_device_id');
}
//Now let us do the work in SessionController after filling your tables with demo data for e.g.
//all these relations are working fine!
    $device = Device::where('d_id',1)->first();
    $user = User::where('id',4)->first();

    //dd($user,$user->deviceUser,$device,$device->deviceUser);//here instance objects and their relations can be fetched easily

    $device_user = DeviceUser::where('du_device_id',1)->where('du_uid',4)->first();

    //dd($device_user,$device_user->device);
    //$session = Session::where('id',100)->first();//can get session by ID
    $session = Session::where('s_device_id',1)->where('s_user_number','000-999')->first();//get session by unique composite key which what you are after. It is similar to the sql Query that you built. Then you can easily fetch the relations as follows:
    dd($session,$session->device,$session->device->deviceUser);

Hope this would help!