Situation: CausalType 1 -> N Causal
Into admin view for causaltype, i'm using cgridview, and I must show the number of causala of each causalType.
I setup the relation into CausalType
return array(
"causals" => array (self::HAS_MANY, "Causal", "causalTypeId" ),
);
I added class variable
public $activeCausalCount;
and this is the column in admin view
array (
'name' => 'activeCausalCount',
'value' => 'count($data->causals)',
),
Actually this is my criteria in search()
$criteria=new CDbCriteria;
$criteria->compare('id',$this->id,true);
$criteria->compare('name',$this->name,true);
$criteria->compare('isActive',$this->isActive);
The count of causal of each type is correct, but I've some problem
1) I need to count only ACTIVE causals (count causals where causals.isActive = 1)
2) I need to sort the column
3) I need to filter (by integer)
If you really need sorting and filtering on COUNT, then that can be little big process.
one way is...
Add a column to your CausalType table ( call it activeCausals )
define a relation in CausalType model
"totalActiveCasuals" => array(
self::STAT,
"Causal",
"causalTypeId",
'condition'=>'totalActiveCasuals.isActive=1'
),
and define afterSave method in Causal
protected function afterSave()
{
$this->causaltype->activeCausals = $this->causaltype->totalActiveCasuals;
$this->causaltype->save();
return parent::afterSave();
}
now you can filter, sort on new column activeCausals very very easily.
Add a new relation to your CasualType of type STAT like this:
return array(
"casuals" => array (self::HAS_MANY, "Causal", "causalTypeId" ),
"totalCasuals" => array (self::STAT, "Causal", "causalTypeId" ),
"totalActiveCasuals" => array (self::STAT, "Causal", "causalTypeId", 'condition' => 'active = true' ),
);
then in your view just use it as a normal attribute/relation
Related
I'm trying to write a query using CakePHP 3.7 ORM where it needs to add a column to the result set. I know in MySQL this sort of thing is possible: MySQL: Dynamically add columns to query results
So far I've implemented 2 custom finders. The first is as follows:
// src/Model/Table/SubstancesTable.php
public function findDistinctSubstancesByOrganisation(Query $query, array $options)
{
$o_id = $options['o_id'];
$query = $this
->find()
->select('id')
->distinct('id')
->contain('TblOrganisationSubstances')
->where([
'TblOrganisationSubstances.o_id' => $o_id,
'TblOrganisationSubstances.app_id IS NOT' => null
])
->orderAsc('Substances.app_id')
->enableHydration(false);
return $query;
}
The second custom finder:
// src/Model/Table/RevisionSubstancesTable.php
public function findProductNotifications(Query $query, array $options)
{
$date_start = $options['date_start'];
$date_end = $options['date_end'];
$query = $this
->find()
->where([
'RevisionSubstances.date >= ' => $date_start,
'RevisionSubstances.date <= ' => $date_end
])
->contain('Substances')
->enableHydration(false);
return $query;
}
I'm using the finders inside a Controller to test it out:
$Substances = TableRegistry::getTableLocator()->get('Substances');
$RevisionSubstances = TableRegistry::getTableLocator()->get('RevisionSubstances');
$dates = // method to get an array which has keys 'date_start' and 'date_end' used later.
$org_substances = $Substances->find('distinctSubstancesByOrganisation', ['o_id' => 123);
if (!$org_substances->isEmpty()) {
$data = $RevisionSubstances
->find('productNotifications', [
'date_start' => $dates['date_start'],
'date_end' => $dates['date_end']
])
->where([
'RevisionSubstances.substance_id IN' => $org_substances
])
->orderDesc('RevisionSubstances.date');
debug($data->toArray());
}
The logic behind this is that I'm using the first custom finder to produce a Query Object which contains unique (DISTINCT in SQL) id fields from the substances table, based on a particular company (denoted by the o_id field). These are then fed into the second custom finder by implementing where(['RevisionSubstances.substance_id IN' ....
This works and gives me all the correct data. An example of the output from the debug() statement is as follows:
(int) 0 => [
'id' => (int) 281369,
'substance_id' => (int) 1,
'date' => object(Cake\I18n\FrozenDate) {
'time' => '2019-09-02T00:00:00+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'comment' => 'foo',
'substance' => [
'id' => (int) 1,
'app_id' => 'ID000001',
'name' => 'bar',
'date' => object(Cake\I18n\FrozenDate) {
'time' => '2019-07-19T00:00:00+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
}
]
],
The problem I'm having is as follows: Each of the results returned contains a app_id field (['substance']['app_id'] in the array above). What I need to do is perform a count (COUNT() in MySQL) on another table based on this, and then add that to the result set.
I'm unsure how to do this for a couple of reasons. Firstly, my understanding is that custom finders return Query Objects, but the query is not executed at this point. Because I haven't executed the query - until calling $data->toArray() - I'm unsure how I would refer to the app_id in a way where it could be referenced per row?
The equivalent SQL that would give me the required results is this:
SELECT COUNT (myalias.app_id) FROM (
SELECT
DISTINCT (tbl_item.i_id),
tbl_item.i_name,
tbl_item.i_code,
tbl_organisation_substances.o_id,
tbl_organisation_substances.o_sub_id,
tbl_organisation_substances.app_id,
tbl_organisation_substances.os_name
FROM
tbl_organisation_substances
JOIN tbl_item_substances
ON tbl_organisation_substances.o_sub_id = tbl_item_substances.o_sub_id
JOIN tbl_item
ON tbl_item.i_id = tbl_item_substances.i_id
WHERE
tbl_item.o_id = 1
AND
tbl_item.date_valid_to IS NULL
AND
tbl_organisation_substances.app_id IS NOT NULL
ORDER BY
tbl_organisation_substances.app_id ASC
) AS myalias
WHERE myalias.app_id = 'ID000001'
This does a COUNT() where the app_id is ID000001.
So in the array I've given previously I need to add something to the array to hold this, e.g.
'substance' => [
// ...
],
'count_app_ids' => 5
(Assuming there were 5 rows returned by the query above).
I have Table classes for all of the tables referred to in the above query.
So my question is, how do you write this using the ORM, and add the result back to the result set before the query is executed?
Is this even possible? The only other solution I can think of is to write the data (from the query I have that works) to a temporary table and then perform successive queries which UPDATE with the count figure based on the app_id. But I'm really not keen on that solution because there are potentially huge performance problems of doing this. Furthermore I'd like to be able to paginate my query so ideally need everything confined to 1 SQL statement, even if it's done across multiple finders.
I've tagged this with MySQL as well as CakePHP because I'm not even sure if this is achievable from a MySQL perspective although it does look on the linked SO post like it can be done? This has the added complexity of having to write the equivalent query using Cake's ORM.
I'm trying to get only one HABTM relationship when using paginate using CakePHP 2.3.
Right now I'm getting this result:
Array
(
[0] => Array
(
[Ticket] => Array ( .....)
[User] => Array ( .....)
[Priority] => Array ( .....)
[Status] => Array(....)
[Attachment] => Array(....)
....
)
[1] => ....
[2] => ....
)
By using:
$this->Ticket->recursive = 0;
$this->paginate = array(
'limit' => 20
);
I am using $this->Ticket->recursive = 0; because I'm only interested in the belongsTo relationships with other tables, but at the same time, I would like to get only one of the HABTM relationships.
I know I could achieve it adding each of the single relationships in the contain array, like so:
$this->Ticket->recursive = 0;
$this->paginate = array(
'limit' => 20,
'contain' => array(
'Ticket`, `User`, `Priority`, `Status`, `Attachment`, `Tags`...
)
);
But I have plenty of tables and I was wondering if there's any way to do it without having to name every single table in the contain array.
I've also tried to use the recursive array inside the contains one like so:
//getting the filtered tickets
$this->paginate = array(
'limit' => 20,
'contain' => array(
'Ticket' => array('recursive' => 1),
'Tag' //this is the hasMany relationship
)
);
But it didn't work. Is there any way to achieve this without having to name all the tables to make it more simple to maintain?
Ok, as it seems there's no solution for it, I managed to do it by getting the associated models of the type belongsTo and then adding the model name of the HABTM relationship:
$associations = $this->Ticket->getAssociated('belongsTo');
$associations[] = 'Tag';
//getting the filtered tickets
$this->paginate = array(
'limit' => 20,
'contain' => $associations
);
I have a table called items and a table called item_pics.
item_pics has an item_id, file_name and a rank field (among others).
What I'm looking for is for each item my index page's $items array to contain the file_name from the item_pics matching the item's item_id with the lowest rank. So I can access like (or something like) this in my Items/index.ctp:
foreach ($items as $item):
$img = $item['Item']['ItemPic']['file_name'];
...
I'm pretty new to CakePHP, this is my first project. I thought that this within the Item model would cause item_pics data to be pulled (although I figured all related item_pics for each item would get pulled rather than just the one with the lowest rank):
public $hasMany = array(
'ItemPic' => array(
'className' => 'ItemPic',
'foreignKey' => 'item_id',
'dependent' => false
)
}
but I can see that no item_pics data is loaded (at the bottom of items/index):
SELECT `Item`.`id`, `Item`.`title`, `Item`.`description`, `Item`.`created`, `Item`.`modified`, `Item`.`type`, `Project`.`id`, `Project`.`item_id`, `Project`.`title`, `Project`.`description`, `Project`.`rank`, `Project`.`created`, `Project`.`modified`
FROM `laurensabc`.`items` AS `Item`
LEFT JOIN `laurensabc`.`projects`
AS `Project`
ON (`Project`.`item_id` = `Item`.`id`)
WHERE `Item`.`type` IN (1, 2)
LIMIT 20
also, while I would like projects to be joined in the view pages, I don't really need them in the index page.
I've done some searching and haven't been able to find exactly what I'm looking for. I suppose I could do a query within the index view item loop, but I'm trying to make sure I do things the right way... the CakePHP way. I assume I need to change something about my model relationships but I haven't had any luck.
CakePHP - Associations - HasMany, this makes it seem like I could order by rank and limit 1. But this didn't work... and even if it did, I wouldn't want that to affect the view pages but rather just the index page.
My Controller looks like this:
public function index($type = null) {
$this->Item->recursive = 0;
$conditions = array();
if ($type == "sale") {
$conditions = array(
"Item.type" => array(self::FOR_SALE, self::FOR_SALE_OR_RENT)
);
} else if ($type == "rent" ) {
$conditions = array(
"Item.type" => array(self::FOR_RENT, self::FOR_SALE_OR_RENT)
);
} else {
$conditions = array("Item.type !=" => self::HIDDEN);
}
$paginated = $this->Paginator->paginate($conditions);
debug($paginated);
$this->set('items', $paginated);
$this->set('title', ($type == null ? "Items for Sale or Rent" : "Items for " . ucwords($type)));
}
I have also tried this on my controller, but it doesn't seem to do anything either:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'alias' => 'ItemPic',
'table' => 'item_pics',
'type' => 'left',
'conditions' => array('ItemPic.item_id' => 'Item.id'),
'order' => array('ItemPic.rank' => 'asc'),
'limit' => 1
)
)
);
$paginated = $this->paginate($this->Item);
First, set containable behavior in AppModel (or if you don't want it on each model, put it on Item model):
public $actsAs = array('Containable');
Then, on your find query:
$items = $this->Item->find('all', array(
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
));
Then the result array you can access it like:
foreach ($items as $item):
$img = $item['ItemPic']['file_name'];
Edit: Then you should put it on the paginate query:
$this->paginate = array(
'conditions' => $conditions,
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
);
In this case, I would probably order by rank and limit 1 as you said, and make that a dynamic association just for the index page (See http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly). So use $this->Item->bindModel(array('hasMany' => array('ItemPic' => $options))); (which I believe should replace your current settings for HasMany ItemPic, but you may have to unbindmodel first)
Associations created through bindModel will go through for the next query only, then it'll revert to your normal settings, unless you specifically set an option to keep using the new association.
As for why it's not getting ItemPics with Items, or why trying to order by rank and limit 1 didn't work for you, I can't really say without seeing more of your code.
i would like to paginate a list of games, where the sport is a chosen sport.
the relatition is as followed:
Game BelongsTo Competition BelongsTo Team BelongsTo sport
What i would like to do is show all games where the teams sport_id = 1.
The following doesn't work:
$this->paginate = array('limit' => 30, 'page' => 1,
'conditions' => array('Competition.Team.sport_id' => '1'),
'contain' => array('Competition', 'Competition.Team',
'Gamefield', 'Changingroom', 'ChangingroomAway', 'Gametype'),
'order'=>array('game_date'=>'asc'),
);
Can anyone help me with this one?
I've found my solution:
var $paginate=array(
'Game'=>
array(
'joins'=>array(
array('table'=>'competitions',
'alias'=>'Competition2',
'type'=>'left',
'conditions'=>array('Game.competition_id=Competition2.competition_id')
),
array('table'=>'teams',
'alias'=>'Team2',
'type'=>'left',
'conditions'=>array('Competition2.team_id=Team2.team_id')
),
),
'order'=>array('game_date'=>'asc'),
'contains'=>array('Competition2'=>array('Team2'))
));
function index() {
$datum = date('Y-m-d H:m');
$this->Game->recursive = 0;
$scope=array('OR' => array(array('Team2.sport_id' => 2), array('Team2.sport_id' =>3)), 'Game.game_date >' => $datum);
// Configure::write('debug',2);
$this->set('games', $this->paginate(null,$scope));
}
Thanks to TehThreag for helping me
Containable doesn't create any joins unless the relation would have been joined anyways, despite using Containable.
This means that for habtm, if you want a join you have to do as Michael did and specify them.
Also, doing a couple of joins should be faster with logical indexes than doing a habtm with Containable anyways, as the results from a contain for the same data as above would require an in ( id1,id2,...id# ) condition and a number of queries from there to fetch the individual related records.
The joins solution gets the data back in one db query.
I've got a series of Post models that hasAndBelongsToMany Media models. In certain function calls inside of the Post model, I don't need to retrieve the entire list of Media models. However, when I use the following code:
$this->unbindModel( array('hasAndBelongsToMany' => array('Media')) );
// Rebind to get only the fields we need:
$this->bindModel(
array('hasAndBelongsToMany' => array(
'Media' => array(
'className' => 'Media',
'joinTable' => 'media_posts',
'foreignKey' => 'post_id',
'associationForeignKey' => 'media_id',
'limit' => 1,
'fields' => array('Media.type', 'Media.path', 'Media.title')
)
)
)
);
$this->find('all', $params);
This limit only works on one of the first retrieved Post model and all following Post models have no associated Media:
Array
(
[0] => Array
(
[Profile] => Array
(
)
[Media] => Array
(
[0] => Array
(
[type] => photo
[path] => ''
[title] => ''
)
)
)
[1] => Array
(
[Profile] => Array
(
)
[Media] => Array
(
)
)
)
Any suggestions would be great. Thanks!
why not use the containable behaviour
// you would probably want the next line in the app_model ot be able to use it with all models
$this->Post->actsAs = array('Containable')
$params['conditions'] = array(
);
$params['contain'] = array(
'Media' => array(
'fields' => array(
'type', 'path', 'title'
),
'limit' => 1
)
);
$this->Post->find('all', $params);
EDIT:
Just tried that and got this sql (Module <-> Tag):
SELECT `Module`.`id` FROM `modules` AS `Module` WHERE 1 = 1
and
SELECT `Tag`.`id`, `ModulesTag`.`module_id`, `ModulesTag`.`tag_id`
FROM `tags` AS `Tag`
JOIN `modules_tags` AS `ModulesTag`
ON (`ModulesTag`.`module_id` IN (1, 2, 3, 4) AND `ModulesTag`.`tag_id` = `Tag`.`id`)
WHERE `Tag`.`belongs_to` = 'Module'
ORDER BY `Tag`.`name` ASC
LIMIT 1
obviously that cannot return the wanted result, as you would have to do a query for each Module result (which then again would result in way too many queries).
As a conclusion I would return all Tags (in my example) as the overhead in too many result rows is better than the overhead of too many queries..
Cake fetches all the Habtm-related records in one batch query and then assembles them into the results array afterwards. Any additional conditions you specify in the association will be used as is in the query, so it'll look something like this:
SELECT … FROM Media WHERE Media.id in (1, 2, 3, …) LIMIT 1
So it'll only retrieve a single HABTM model.
There's no apparently easy solution for this. Maybe you could think about the original premise again and why the "first" (LIMIT 1) record is supposedly the right one, maybe you can find a different condition to query on.
Failing that, you could rebind your models so Media has a hasMany relationship to medias_posts, the pivot table. For hasMany and belongsTo queries, Cake automatically does JOIN queries. You could use a GROUP BY clause then, which would give you the desired result:
SELECT … FROM Media JOIN medias_posts … GROUP BY medias_posts.post_id
You might also want to experiment with passing the 'join' parameter with the query, to achieve that effect without extensive rebinding.
$this->Media->find('all', array('join' => array(…), …));
Try this:
$this->yourModel->hasAndBelongsToMany['Media'] = false; // or null
And then set your HABTM association manually
$this->yourModel->hasAndBelongsToMany['Media'] = array(........);
Or simply modify the association without nulling it:
$this->yourModel->HABTM['Media']['fields'] = array(....)
CakePHP has a very powerful tool for this containable behaviour