Yii model relation one-to-newest - orm

Assume we have 3 tables like this:
--------- --------------- -------------------
| USER | | USER_STATUS | | STATUS_DICT |
|-------| |-------------- |------------------
| id | | id | | id (int) |
| name | | user_id | | status (string) |
| email | | status_id | -------------------
--------- | timestamp |
---------------
Each user has many statuses (user_status), related with user.id <-> user_status.user_id. Each user_status has one related record in status_dict which is string label of int status (for example 0 = active).
What I want is make User model to retrieve last user's status using relation. Set relations:
/* User model */
public function relations() {
return array(
'status' => array(self::HAS_ONE, 'UserStatus', 'user_id', 'order'=>'status.id DESC'),
);
}
and
/* UserStatus model */
public function relations() {
return array(
'status' => array(self::BELONGS_TO, 'StatusDictionary', 'status_id'),
'user' => array(self::BELONGS_TO, 'User', 'user_id'),
);
}
Then when I call User::model()->findByPk(1)->status I get latest status for user #1.
BUT.
When I want to find all users with specified status it does not work as I want. Instead of getting only users with specified status (latest record in user_status) I get all users who at least once had this status.
What I have to do to make relation one-to-newest (I call it like this for this case's purpose)? I saw this article and it does the job, but I'm wondering if there is way to achieve it with Yii's conventions.
I wanted to declare named scope like this:
/* in User model */
public function withStatus($status) {
if(!is_numeric($status) && !is_null($status)) {
$status = StatusDictionary::model()->find('lower(name)=lower(:name)', array(':name' => $status))->getAttribute('id');
}
$this->getDbCriteria()->mergeWith(array(
'condition' => (is_null($status) ? 'status.status_id IS NULL' : 'status.status_id=:statusId'),
'params' => array(':statusId' => $status),
'with' => array('status')
));
return $this;
}
But it doesn't work either, User::model()->withStatus(0)->findAll() returns all users with related record in user_status with user_status.status_id = 0 even if some of users have newer, different status.
Thanks in advance for any help.

I found somewhere else similar solution:
Scope function:
public function withStatus($status) {
if(!is_numeric($status) && !is_null($status)) {
$status = StatusDictionary::model()->find('lower(name)=lower(:name)', array(':name' => $status))->getAttribute('id');
}
$this->with('status');
$this->getDbCriteria()->compare('i.statusId', $status);
return $this;
}
Relations record (this join is protecting relation from filter criteria):
'status' => array(self::HAS_ONE, 'Status', 'clientId',
'join' => 'INNER JOIN (select max(id) id from status group by clientId) j ON i.id=j.id',
'alias' => 'i'
),
It works for me.

Ok, I found solution with TheRifler's help from his post. It turned out that I had a solution right under my nose (article linked in a question).
Relation should be defined as below:
/* User model */
public function relations() {
return array(
'status' => array(self::HAS_ONE, 'UserStatus', 'user_id',
'order' => 's.id DESC',
'alias' => 's',
// http://murrayhopkins.wordpress.com/2008/10/28/mysql-left-join-on-last-or-first-record-in-the-right-table/
'join' => 'LEFT JOIN ( SELECT s1.*
FROM `user_status` as s1
LEFT JOIN `user_status` AS s2
ON s1.user_id = s2.user_id AND s1.id < s2.id
WHERE s2.user_id IS NULL ) as `status`
ON (s.user_id = `status`.user_id)'
),
);
}
and the named scope:
public function withStatus($status) {
if(!is_numeric($status) && !is_null($status)) {
$status = StatusDictionary::model()->find('lower(name)=lower(:name)', array(':name' => $status))->getAttribute('id');
}
$this->with('status');
if(!is_null($status)) {
$this->getDbCriteria()->compare('status.status_id', $status);
}
else {
$this->getDbCriteria()->addCondition('s.status_id IS NULL');
}
return $this;
}
It contains some dirty work, because withStatus() uses status join subquery's data if $status is specified or base s alias if $status is null (it's because we're looking for user without record in status table). Maybe this is not most elegant solution, but works as it should.
Now I can use:
User::model()->findByPk(1)->status to access specified user's status
User::model()->withStatus('active')->findAll() list users with specified status (status may be passed as integer [user_status.id] or string [status_dict.name])
User::model()->withStatus(null)->count() get number of users without any status

Related

Query efficiency -> merge 2 queries with a join or union

I need some serious help/direction. I have two tables:
students
-id
-site_id
-name
-enter_date
-exit_date
student_meals
-id
-site_id
-student_id
-meal_type_id (1, 2, or 3)
-date_served
I need two arrays:
All students enrolled on the requested 'serviceDate' ('serviceDate is between their enter_date and exit_date) that DO NOT have a meal_type_id of the requested mealType on the rquested serviceDate.
All students enrolled on the requested 'serviceDate' ('serviceDate is between their enter_date and exit_date) that DO have a meal_type_id of the requested mealType on the requested serviceDate.
I got it to work with the following:
'unservedStudents' => Auth::user()->site
->students()
->where('enter_date', '<=',Request::only( 'serviceDate') )
->where('exit_date', '>=',Request::only( 'serviceDate') )
->OrderByName()
->filter(Request::only('search', 'serviceDate', 'mealType'))
->get()
->map(fn ($students) => [
'id' => $students->id,
'name' => $students->name,
]),
'servedStudents' => Auth::user()->site
->student_meals()
->with('student')
->where('meal_type_id', Request::only( 'mealType'))
->where('date_served', Request::only( 'serviceDate'))
->orderBy('created_at', 'DESC')
->get()
->map(fn ($served_students) => [
'id' => $served_students->id,
'student' => $served_students->student ? $served_students->student->only('id','name') : null,
]),
//Filter for students
public function scopeFilter($query, array $filters)
{
$mealType = $filters['mealType'] ?? null;
$serviceDate = $filters['serviceDate'] ?? null;
$search = $filters['search'] ?? null;
$query
->when($search, function ($query) use ($search) {
$query->where( fn ($query) =>
$query->where('first_name', 'like', '%'.$search.'%')
})
->when( $mealType, function ($query) use ($mealType, $serviceDate) {
$query->whereDoesntHave('student_meals', fn ($query) =>
$query->where('meal_type_id', $mealType )
->where('date_served', $serviceDate));
});
When I seeded my database ites that have more than 400 students or so gets really slow. I'm pretty sure I need to condense the two queries above, but I can't figure out the logic.
Below is an attempt, but it gives me an error 'Method Illuminate\Database\Eloquent\Collection::getBindings does not exist'.
$students = Auth::user()->site
->students()
->join('student_meals as m', 'm.student_id', '=', 'students.id')//this is my attempt to get the same columns as the table to union....
->where('enter_date', '<=',Request::only( 'serviceDate') )
->where('exit_date', '>=',Request::only( 'serviceDate') )
->where('date_served', '=',Request::only( 'serviceDate') )
->filter(Request::only('search', 'serviceDate', 'grade', 'hr'))
->select('students.id as studentId', 'first_name', 'students.site_id as siteId', 'm.id as mealId', 'm.meal_type_id', )
->get()
->map(fn ($students) => [
'id' => $students->studentId,
'name' => $students->first_name,
'siteId' => $students->site_id,
'mealId' => $students->mealId,
'mealType' => $students->meal_type_id,
]),
'student_meals' => Auth::user()->site
->student_meals()
->join('students as s', 's.id', '=', 'student_meals.student_id')
->where('date_served', '>=',Request::only( 'serviceDate') )
->where('meal_type_id', '>=',Request::only( 'mealType') )
->select('s.id as studentId', 'first_name',
's.site_id as siteId', 'student_meals.id as mealId', 'meal_type_id')
->union($students)
->map(fn ($students) => [
'id' => $students->studentId,
'name' => $students->first_name,
'siteId' => $students->site_id,
'mealId' => $students->mealId,
'mealType' => $students->meal_type_id,
]),
If you're up for it, I'd really appreciate any insight/help/pointers/tips.
I think that your problem is very simple if you use the collections
//Relation name should be meals instead of student_meals because is redundant that a student has many student meals
$students = Student::with([
'meals' => function ($query) use ($request) {
$query->where('date_served', $request['serviceDate']);
}
])
->where('site_id', $request->user()->site_id)
->where('enter_date', '<=', $request['serviceDate'])
->where('exit_date', '>=', $request['serviceDate'])
->get();
At this point you have all students that has the requested serviceDate between enter_date and exit_date and belongs to the same site_id of the current user (lazy loading all the meals of the student that belongs to the requested serviceDate), so, all you have to do is spread them in two different collections.
//Students with requested meal type
$swrmt = collect();
//Students without requested meal type
$swtrmt = collect();
foreach ($students as $student) {
//If student contains at least one meal with the requested mealType
if ($student->contains('meals.meal_type_id', $request['mealType'])) {
$swrmt->push($student);
} ese {
$swtrmt->push($student);
}
}
So you only have one query, and only need to be worried if the result is greater than 2000 students, if that happens would be necesary to change the with for a load using chunk of 2000 for preventing limit param query error. (Sorry if there is any type mistake, i write all of this on my cellphone), and don't forget to add your name filter at the main query with the same when that you alredy use.

Laravel Row duplication inserted, with updateOrCreate method with Race-Condition

i have function in my controller that create a forecast :
public function updateOrCreate(Request $request, $subdomain, $uuid)
{
$fixture = Fixture::where('uuid',$uuid)->firstOrFail();
request()->validate([
'local_team_score' => 'integer|min:0',
'visitor_team_score' => 'integer|min:0',
'winner_team_id' => 'integer|nullable'
]);
if ($fixture->status !== "PENDING"){
return response()->json([
'message' => "You can not add or modify a forecast if the fixture is not pending"
], 403);
}
$winner_team = null;
// local team win
if ($request->local_team_score > $request->visitor_team_score) {
$winner_team = $fixture->localTeam;
}elseif ($request->local_team_score < $request->visitor_team_score){ //visitor win
$winner_team = $fixture->visitorTeam;
}else{ // draw
$winner_team = FixtureTeam::where('team_id',$request->winner_team_id)->first();
}
$user = auth('api')->user();
$platform = Platform::first();
$forecast = Forecast::updateOrCreate([
'user_id' => $user->id,
'fixture_id' => $fixture->id,
'platform_id' => $platform->id
],[
'local_team_score' => $request->local_team_score,
'visitor_team_score' => $request->visitor_team_score,
'winner_team_id' => is_null($winner_team) ? null : $winner_team->team_id
]);
$forecast->load('winnerTeam');
return new ForecastResource($forecast);
}
As you can see i use updateOrCreate methods to add or update a forecast.
The problem is when 2 requests from the same user run at the same time (and no forecast is already created) 2 row are inserted.
Do you have a solution ?
I See that the problem is not new but i could not find a solution https://github.com/laravel/framework/issues/19372
updateOrCreate does 2 steps:
tries to fetch the record
depending on the outcome does an update or a create.
This operation is not atomic, meaning that between step 1 and 2 another process could create the record and you would end up with duplicates (your situation).
To solve your problem you need following:
determine what columns would give the uniqueness of the record and add an unique index (probably compound between user_id, fixture_id, platform_id)
you need to let database handle the upsert (ON DUPLICATE KEY UPDATE in MySQL, ON CONFLICT (...) DO UPDATE SET in Postgres, etc). This can be achieved in Laravel by using the upsert(array $values, $uniqueBy, $update = null) instead of updateOrCreate.

Retrieve data value in CGridView by relation HAS_ONE

The Relation:
public function relations() {
return array(
'submissionStatus' => array(self::HAS_ONE, 'SubmissionStatus', 'submission_status_id'),
);
}
Column value in GridView:
'value' => '$data->submissionStatus->submission_status_name',
SubmissionStatus Model Attributes:
public function attributeLabels() {
return array(
'submission_status_id' => 'Status ID',
'submission_status_name' => 'Status Name',
);
}
SubmissionStatus Relations:
public function relations() {
return array(
'submissionStatus' => array( self::BELONGS_TO, 'Submission', 'submission_status_id' ),
);
}
Yet I get an error:
Trying to get property of non-object
Why can I not retrieve submission_status_name by this relation?
EDIT:
This on the other hand works perfectly:
'submissionStatus' => array(self::BELONGS_TO, 'SubmissionStatus', 'submission_status')
DATABASE:
| tbl_submission |
-----------------------------------
| submission_id (int) PK |
| submission_name (varchar) |
| submission_status_id (int) FK |
| tbl_submission_status |
------------------------------------
| submission_status_id (int) PK |
| submission_status_name (varchar) |
There to 2 questions here.
if your relations really works
hot to get the data in gridview
I think you have an error in the realtion. The difference between BELONGS_TO and HAS_ONE is, that it tries to create the realtion in a different way. Could you post your Database tables, what fields they have, we need to see what is the key and foreign jey fields.
i guess it is id in one table and submission_status_id in the other one.
try to change you relation in one of these two (try if one of them works):
1.
'submissionStatus' => array(self::HAS_ONE, 'SubmissionStatus', array('submission_status_id'=>'id'),
'submissionStatus' => array(self::HAS_ONE, 'SubmissionStatus', array('id'=>''),
2.
in your grid the code you use is correct but as stated in the comment from #Manquer, if the relation returns null it will not work.
But you can try the more Yii style in the GridViiew like this:
do not give any value only tne name
'name'=>'>submissionStatus.submission_status_name'
If the value of relation field is null then you relation will not return a object of the Foreign model; therefore calling the attribute in it will give the error
try changing
'value' => '$data->submissionStatus->submission_status_name',
to
'value' => '(isset($data->submissionStatus))?
$data->submissionStatus->submission_status_name:""',

Yii: A HAS_MANY B, B HAS_MANY C, find count of C belonging to A

Basically, I have 3 tables.
house_table
===========
house_id
floor_table
===========
floor_id
house_id
room_table
===========
room_id
floor_id
is_occupied
A house has many floor, a floor has many rooms. A room belongs to one floor, a floor belongs to one house. The corresponding models created automatically by gii are HouseTable, FloorTable, RoomTable.
What I need is to findAll() houses that have rooms that are not occupied.
How do I do that? Something like this?
class HouseRecord extends CActiveRecord {
public function relations() {
return array(
'FREE_ROOM_COUNT' => array(self::STAT ...???...),
);
}
}
Sure, I could do it with SQL, but it needs to be done this way, because the result of findAll() is used as a data provider in a grid.
UPDATE
Following tinybyte's advice, here's what finally worked.
public function relations() {
return array(
'FREE_ROOM_COUNT' => array(
self::STAT ,
'FloorTable',
'house_id',
'select' => 'COUNT(rt.floor_id)',
'join' => 'INNER JOIN room_table rt ON t.floor_id = rt.floor_id',
'condition' => 'rt.is_occupied = 0',
),
);
}
And to be used as so:
$criteria = new CDbCriteria;
$criteria->with = array('FREE_ROOM_COUNT');
$criteria->together = true;
$provider = new CActiveDataProvider(HouseTable::model(), array(
'criteria'=>$criteria,
'pagination' => array(
'pageSize' => 1,
),
));
$this->widget('zii.widgets.CListView', array(
'dataProvider'=>$provider,
'itemView'=>'house',
));
Unfortunately it turns out one can not use these STAT relations as conditions! (Confirmed here: Using STAT relation in CActiveDataProvider criteria).
I think this should do the trick
'FREE_ROOM_COUNT' => array(
self::STAT ,
'Floor' ,
'house_id'
'select' => 'count(rt.floor_id)' , // or count(rt.room_id)
'join' => 'Inner join room_table rt ON Floor.floor_id = rt.floor_id' ,
),

Kohana ORM: Get results based on value of a foreign table

taxonomies
-id
-name
taxonomy_type
-taxonomy_id
-type_id
I've configured two models:
class Model_Taxonomy{
protected $_has_many = array('types'=>array());
}
class Model_Taxonomy_Type{
protected $_belongs_to = array('taxonomy' => array());
}
*Please note that taxonomy_type is not a pivot table.*
A taxonomy can have multiple types associated.
Then, what I'm trying to do is get all taxonomies that belong to a given type id.
This is would be the SQL query I would execute:
SELECT * FROM taxonomies, taxonomy_type WHERE taxonomy_type.type_id='X' AND taxonomies.id=taxonomy_type.taxonomy_id
I've tried this:
$taxonomies = ORM::factory('taxonomy')
->where('type_id','=',$type_id)
->find_all();
Obviously this doesn't work, but I can't find info about how execute this kind of queries so I have no clue.
class Model_Taxonomy{
protected $_belongs_to = array(
'types' => array(
'model' => 'Taxonomy_Type',
'foreign_key' => 'taxonomy_id'
)
);
}
class Model_Taxonomy_Type{
protected $_has_many = array(
'taxonomies' => array(
'model' => 'Taxonomy',
'foreign_key' => 'taxonomy_id'
)
);
}
And use some like that:
$type = ORM::factory('taxonomy_type')
->where('type_id', '=', $type_id)
->find();
if( ! $type->taxonomies->loaded())
{
types->taxonomies->find_all();
}
type_id column is a PK of taxonomy_type table, am I right?
So, you have one (unique) taxonomy_type record, and only one related taxonomy object (because of belongs_to relationship). Instead of your:
get all taxonomies that belong to a
given type id
it will be a
get taxonomy for a given type id