innerJoin with ActiveDataProvider on Yii2 - yii

I'm trying the following:
I have 3 models: Clinic, Pack and OfferedTreatment.
Pack and OfferedTreatment are related to Clinic by a FK clinic_id and both models are related to each other by a junction model: Pack_OfferedTreatment (pack_id, offeredTreatment_id). One Pack can have several OfferedTreatments.
From ClinicController I want a certain clinic and a certain pack related to that clinic and get all related offeredTreatments to show a view
The result I have from the following function is it returns me ALL the offeredTreatments associated to the clinic, but not to the pack....what am I doing wrong?
/**
* #param $clinicId
* #param $packId
* #return array
* #throws NotFoundHttpException
*/
public function actionPackOfferedTreatments($clinicId, $packId)
{
$clinicId = (int)$clinicId;
$packId = (int)$packId;
// Find model of the clinic
$model = $this->findModel($clinicId);
// pack offeredTreatments:
$packOfferedTreatmentDataProvider = new ActiveDataProvider([
'query' => $model->getOfferedTreatments()
->innerJoin('pack_offeredTreatment', false)
->where(['pack_offeredTreatment.pack_id' => $packId]),
'pagination' => false,
'sort' => [
'defaultOrder' => [
'order' => SORT_ASC
]
]
]);
Yii::$app->response->format = Response::FORMAT_JSON;
return $packOfferedTreatmentDataProvider->getModels();
}

I would think about it another way. You say that Offered Treatment is related to the clinic with a foreign key - but it should not be. The pack is related to the clinic, and the treatment is related to the pack, so it's already related trough pack.
If you have relations set up, Clinic hasMany Packs, and Packs hasMany treatments, you could run your query on a clinic with ->innerJoinWith('packs.treatments'). If you have the pack, you can run ->with(['clinic', 'treatments']).
From the docs:
You can eagerly load deeply nested relations, such as a.b.c.d. All parent relations will be eagerly loaded. That is, when you call with() using a.b.c.d, you will eagerly load a, a.b, a.b.c and a.b.c.d.

Related

Yii 1.1 relationship search issue

1. Table meeting
Model name CoreMeeting
Fields id, title, start time, created_by
Relationship:
'MeetingParticipants' => array(self::HAS_MANY, 'MeetingParticipants', 'meeting_id'),
2. Table core_meeting_participant
Model name is meeting_participant
Fields are id, meeting_id, participant_id, group_id
Relationship:
'meeting' => array(self::BELONGS_TO, 'CoreMeeting', 'meeting_id'),
'group' => array(self::BELONGS_TO, 'MeetingGroup', 'group_id'),
3. Table core_meeting_group
Model name is MeetingGroup
Fields are id, group_name
My search filter in the meeting model is:
public function search()
{
$group=filter_var($_REQUEST['group'], FILTER_SANITIZE_STRING);//contain group name
$criteria=new CDbCriteria;
$user_id = Yii::app()->user->id;
$criteria->compare('id',$this->id);
$criteria->compare('title',$this->title,true);
$criteria->with=array('MeetingParticipants'=>array("select"=>"*"),'MeetingParticipants.group'=>array('select'=>'id,group_name'));
if(isset($this->start_time) && !empty($this->start_time))
$criteria->compare('start_time',date("Y-m-d", strtotime($this->start_time)), true);
$criteria->compare('created_by',$user_id);
if(isset($group)&&!empty($group))
$criteria->compare('group.group_name',$group);
$criteria->together = true;
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
I have created 4 meetings, each meeting have at least 5 participants each participant belongs to a meeting group. I want to list all meetings with the following filed meeting title, groups and meeting time.
My problem is if I enable $criteria->together = true; if the meeting has more than 10 participants that will show only 1 meeting in grid view. If I disable this that will show all but I can't search with the meeting name.
SQL fiddle link is http://sqlfiddle.com/#!9/fdaacf
full SQL dump https://pastebin.com/NtpMuCpE
Documentation for CDbCriteria->together property:
https://www.yiiframework.com/doc/api/1.1/CDbCriteria#together-detail
Whether the foreign tables should be joined with the primary table in a single SQL. This property is only used in relational AR queries for HAS_MANY and MANY_MANY relations.
As I can see your relation in CoreMeeting model:
'group' => array(self::BELONGS_TO, 'MeetingGroup', 'group_id')
Obviously you are trying to fetch one-to-one (BELONGS_TO) relation CoreMeeting -> MeetingGroup eagerly by globally setting to your CDbCriteria->together = true; which is not correct according to the documentation.
What I would suggest you, first of all get rid of this global together setting to CDbCriteria and try changing your with clause like this:
$criteria->with = [
// this is your has-many relation (one meeting has several participants), thus we set here `together` to true
'MeetingParticipants' => [
'together' => true,
'with' => [
// this is your one-to-one relation (one participant has one group)
'group'
]
]
];

Yii2 - hasMany relation with multiple columns

I have a table message_thread:
id
sender_id
recipient_id
I want to declare a relation in my User model that will fetch all message threads as follows:
SELECT *
FROM message_thread
WHERE sender_id = {user.id}
OR recipent_id = {user.id}
I have tried the following:
public function getMessageThreads()
{
return $this->hasMany(MessageThread::className(), ['sender_id' => 'id'])
->orWhere(['recipient_id' => 'id']);
}
But it generates an AND query. Does anyone know how to do this?
You cannot create regular relation in this way - Yii will not be able to map related records for eager loading, so it not supporting this. You can find some explanation int this answer and related issue on GitHub.
Depending on use case you may try two approach to get something similar:
1. Two regular relations and getter to simplify access
public function getSenderThreads() {
return $this->hasMany(MessageThread::className(), ['sender_id' => 'id']);
}
public function getRecipientThreads() {
return $this->hasMany(MessageThread::className(), ['recipient_id' => 'id']);
}
public function getMessageThreads() {
return array_merge($this->senderThreads, $this->recipientThreads);
}
In this way you have two separate relations for sender and recipient threads, so you can use them directly with joins or eager loading. But you also have getter which will return result ofboth relations, so you can access all threads by $model->messageThreads.
2. Fake relation
public function getMessageThreads()
{
$query = MessageThread::find()
->andWhere([
'or',
['sender_id' => $this->id],
['recipient_id' => $this->id],
]);
$query->multiple = true;
return $query;
}
This is not real relation. You will not be able to use it with eager loading or for joins, but it will fetch all user threads in one query and you still will be able to use it as regular active record relation - $model->getMessageThreads() will return ActiveQuery and $model->messageThreads array of models.
Why orOnCondition() will not work
orOnCondition() and andOnCondition() are for additional ON conditions which will always be appended to base relation condition using AND. So if you have relation defined like this:
$this->hasMany(MessageThread::className(), ['sender_id' => 'id'])
->orOnCondition(['recipient_id' => new Expression('id')])
->orOnCondition(['shared' => 1]);
It will generate condition like this:
sender_id = id AND (recipent_id = id OR shared = 1)
As you can see conditions defined by orOnCondition() are separated from condition from relation defined in hasMany() and they're always joined using AND.
For this query
SELECT *
FROM message_thread
WHERE sender_id = {user.id}
OR recipent_id = {user.id}
You Can use these
$query = (new \yii\db\Query)->from("message_thread")
$query->orFilterWhere(['sender_id'=>$user_id])->orFilterWhere(['recipent_id '=>$user_id]);

Returning related fields with Lamba LINQ entity

I have seen many questions related to how you can return a limited set of fields from a single EF entity with anonymous types. My issue is about the opposite of that. I want to return values from related tables along with all of the fields in my entity table:
IQueryable<EntityModels.TBLEFFORT> query = db.TBLEFFORT.AsQueryable();
query = query.Where(a => a.EFFDELETE == "0");
if (!string.IsNullOrEmpty(sGuid))
{
query = query.Where(a => a.TBLEFFORTLINK.Any(b => b.TBLSHS.TBLTACT.Any(c => c.CGUID == sGuid)));
}
query.Select(x => new EffortSearchResult()
{
Efguid = x.EFGUID,
Efstatus = x.EFSTATUS
});
Here my base entity is TBLEFFORT and i query directly against it on the EFFDELETE field and then i query against it with a related table TBLSHS.
However, in my anon return object i only have fields from TBLEFFORT (EFGUID and EFSTATUS). How can i include fields from the related TBLSHS entity in my anonymous return object?
The related tables are lookups so they will be 1:1. I'm not looking to return complex sets based on a FK field in TBLEFFORT
I figured it out. you just extend out in your anon type and dig for what you need from the related tables:
Shguid = x.TBLEFFORTLINK.Select(b => b.TBLSHS.SOMEFIELD).FirstOrDefault(),

Yii with join using CDbCriteria and CActiveDataProvider with custom Search

Hi I am using Yii to create an Application
I have modified the search() in model and I have come up with a problem.
Users can log in as admins, managers, clients
When a user is logged as a manager, he can view clients only from the store they both belong. So far so good, I've managed to accomplish that.
Now the problem is when I try to prevent managers from viewing other managers from the same store (and therefore edit each other's accounts) in CGridView.
The relations
/**
* #return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'authitems' => array(self::MANY_MANY, 'Authassignment', 'authassignment(userid, itemname)'),
'additionalContacts' => array(self::HAS_MANY, 'AdditionalContact', 'Client_Id'),
'store' => array(self::BELONGS_TO, 'Store', 'Store_Id'),
'user' => array(self::BELONGS_TO, 'User', 'User_Id'),
'genericPoints' => array(self::HAS_MANY, 'GenericPoint', 'Client_Id'),
);
}
The Custom model search
public function searchCustom()
{
// Warning: Please modify the following code to remove attributes that
// should not be searched.
$criteria=new CDbCriteria;
$criteria->addCondition('t.id !='.$this->id);
$criteria->compare('t.Created',$this->Created,true);
$criteria->compare('t.Updated',$this->Updated,true);
$criteria->compare('t.Discount',$this->Discount,true);
$criteria->compare('t.Discount_Type',$this->Discount_Type,true);
$criteria->addCondition('t.Store_Id ='.$this->Store_Id);
//$criteria->addCondition('t.id !='.$this->id);
//GET FIELDS FROM USER IN SEARCH
$criteria->with=array('user');
$criteria->compare('user.id',$this->User_Id,true);
$criteria->compare('user.First_Name',$this->First_Name,true);
$criteria->compare('user.Last_Name',$this->Last_Name,true);
$criteria->compare('user.Username',$this->Username,true);
$criteria->compare('user.Email',$this->Email,true);
$criteria->addCondition('user.Status = 1');
$criteria->with=array('authitems');
$criteria->compare('authitems.userid',$this->id,false);
$criteria->compare('authitems.itemname','client',false);
$criteria->together = true;
$criteria->order = 't.Created DESC';
return new CActiveDataProvider($this, array(
'criteria'=>$criteria,
));
}
And this the error I am getting
CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'user.Status' in 'where clause'. The SQL statement executed was: SELECT COUNT(DISTINCT `t`.`id`) FROM `client` `t` LEFT OUTER JOIN `authassignment` `authitems_authitems` ON (`t`.`id`=`authitems_authitems`.`userid`) LEFT OUTER JOIN `authassignment` `authitems` ON (`authitems`.`itemname`=`authitems_authitems`.`itemname`) WHERE (((((t.id !=2) AND (t.Store_Id =1)) AND (user.Status = 1)) AND (authitems.userid=:ycp0)) AND (authitems.itemname=:ycp1))
I know it has something to do with the relations the gii set up for me but I can't pinpoint it.
Perhaps authitems' MANY_MANY relation stops the user relation from loading?
Instead of making another assignement for $criteria->with (which will override the previous one), you should simply try :
$criteria->with=array('user', 'authitems');

Double Join Select

Three tables Project, Users, Issues.
Project table columns: p_id,name,...
Users table columns: u_id username...
Issues table columns: i_id i_name...
Relations:
Project has many Users - 1..*
Project has many Users - 1..*
Project has many Issues - 1..*
Users has many Issues - 1..*
What I want to do:
In Yii framework logic: Select Project with all it's users, these users has to have only Issues of the selected Project.
In tables logic: Select Issues of certain project AND user.
What sql code I want to mimic:
SELECT Issue.i_name FROM Issue Join Project on Issue.i_id =
Project.p_id Join User on Issue.i_id User.u_id
What I want to do in Yii:
//get Project
$model = Project::model()->findByPk( $p_id );
//get Project's users
$users = $model->users;
//get each of users issues of selected project
foreach( $users as $user )
$issues = $user->issues;
To solve this you have to use through in your ralations method.
Project model relations method should look like this:
public function relations()
{
return array(
'users' => array(self::MANY_MANY, 'User', 'tbl_project_user_assignment(project_id, user_id)'),
//'issues' => array(self::HAS_MANY, 'Issue', 'project_id'),
'issues' => array(self::HAS_MANY,'Issue',array('id'=>'owner_id'),'through'=>'users'),
'columns' => array(self::MANY_MANY, 'Column', 'tbl_project_rel_column(p_id,c_id)'),
);
}
Now in action select project, it's users and users's posts(or in my case issues) of selected project:
$project = Project::model()->with('users','issues')->findByPk(1);
$users = $project->users;
foreach($users as $user) {
echo $user->username."<br/>";
}
$issues = $project->issues;
foreach($issues as $issue) {
echo $issue->name."<br/>";
}