I'm having some trouble with Cakephp Query builder and try to get results I'm looking for. I have three tables 'Users', 'Items', 'Owns', where Owns belongs to both 'Users' and 'Items'.... A user can own many items (and many of the same items).
So I'm wanting to return the "owned" items by a user along with the count... However whenever I add count into my query I lose the associated Items data.
example - this returns the user data along with associated owns data and associated items data as shown below the query
$owns = $this->Users->get($id, [
'contain' => [
'Owns' => function($q) { return $q->find('all')->group(['Owns.item_id']); },
'Owns.Items'
],
]);
SQL Generated from this is
SELECT
Owns.id AS Owns__id,
Owns.item_id AS Owns__item_id,
Owns.user_id AS Owns__user_id,
Items.id AS Items__id,
Items.name AS Items__name,
Items.description AS Items__description,
Items.created AS Items__created,
Items.modified AS Items__modified
FROM
owns Owns
INNER JOIN items Items ON Items.id = (Owns.item_id)
WHERE
Owns.user_id in (1)
GROUP BY
Owns.item_id
Data results look like this:
'owns' => [
'id' => (int) 1,
'username' => 'myusername',
'owns' => [
(int) 0 => object(App\Model\Entity\Own) id:0 {
'id' => (int) 2
'item_id' => (int) 1
'user_id' => (int) 1
'item' => object(App\Model\Entity\Item) id:1 {
'id' => (int) 1
'name' => 'Item Name'
'description' => 'Item Description'
However in the 'owns' query I want to add in the count (i.e. the number owned). I can get the count by using this query but then I lose the associated item object from my results. I've tried this many different ways but always seems that if I want to use SQL count I can't get the associated data.
$owns2 = $this->Users->get($id, [
'contain' => [
'Owns' => function($q) { return $q->select(['count' => $q->func()->count('Owns.id'),'Owns.id', 'Owns.user_id', 'Owns.item_id'])->group(['Owns.item_id']); },
'Owns.Items'
],
]);
SQL Generated from this is
SELECT
(COUNT(Owns.id)) AS count,
Owns.id AS Owns__id,
Owns.user_id AS Owns__user_id,
Owns.item_id AS Owns__item_id
FROM
owns Owns
INNER JOIN items Items ON Items.id = (Owns.item_id)
WHERE
Owns.user_id in (1)
GROUP BY
Owns.item_id
Data results look like this:
'owns2' => [
'id' => (int) 1,
'username' => 'myusername',
'owns' => [
(int) 0 => object(App\Model\Entity\Own) id:36 {
'count' => (int) 4
'id' => (int) 2
'user_id' => (int) 1
'item_id' => (int) 1
Any insight into how I can get "count" into the first $user query or the associated items into the $user2 would be appreciated.
To keep the association table's fields that are returned in the $owns = find('all') query along with the SQL count function I needed to add the associated fields into the select statement
$owns2 = $this->Users->get($id, [
'contain' => [
'Owns' => function($q) { return $q->select(['count' => $q->func()->count('Owns.id'),'Owns.id', 'Owns.item_id','Owns.user_id', 'Items.id', 'Items.collection_id','Items.name', 'Items.description'])->group(['Owns.item_id']); },
'Owns.Items'
],
]);
Related
CakePHP Version: 4.0.1
Introduction
I have 2 methods that both use the index view, index and search. On index the column can be selected from a select list and a value can be inputted via an input form control enabling a search by column and value. This data is sent via GET to the search method where empty values are checked and the query is executed and the index view is rendered.
In the later 3x versions with the below configuration the index view had the sort on the selected column which is what it is meant to do.
IE: Index view has due_date sorted on the initial load and I select task_name then submit the form to the search method. The task_name has the sort when the view is rendered.
TASKS CONTROLLER
Public pagination property:
public $paginate = [
'sortWhitelist' => [
'Tasks.due_date',
'Tasks.task_name',
'Tasks.type',
'Tasks.priority',
'Tasks.related_to_name',
'Contacts.first_name',
'Contacts.last_name',
'Accounts.account_name',
'Tasks.task_desc'
]
];
Search Method
I initialise the data received from the index method and apply the config to the pagination property and send the query object to the view.
$this->setPage('');
$this->setSort($this->request->getQuery('column'));
$this->setDirection('asc');
// Validation of the page, sort, direction and limit is done here.
// IE: The $this->getSort() must be a string and not be numeric and has a strlen check
// and the $this->getDirection() can only be a string with values 'asc' or 'desc' etc.
if (!empty($this->getPage())) {
$this->paginate['page'] = $this->getPage();
}
$this->paginate['sort'] = $this->getSort();
$this->paginate['direction'] = $this->getDirection();
$this->paginate['limit'] = $this->getLimit();
debug($this->paginate);
$tasks = $this->paginate($query);
$this->set(compact('tasks'));
The result of debug is:
[
'sortWhitelist' => [
(int) 0 => 'Tasks.due_date',
(int) 1 => 'Tasks.task_name',
(int) 2 => 'Tasks.type',
(int) 3 => 'Tasks.priority',
(int) 4 => 'Tasks.related_to_name',
(int) 5 => 'Contacts.first_name',
(int) 6 => 'Contacts.last_name',
(int) 7 => 'Accounts.account_name',
(int) 8 => 'Tasks.task_desc'
],
'sort' => 'Tasks.task_name',
'direction' => 'asc',
'limit' => (int) 25
]
Result
The sort is on the task_name.
A couple of months ago I upgraded to 4 and have just revisted this functionality to find the sort is on the column that was present on index and not the column that was selected. I tried the below to fix the problem:
I referenced this information in the cookbook. And this from SO.
$config = $this->paginate = [
'page' => $this->getPage(),
'sort' => $this->getSort(),
'direction' => $this->getDirection(),
'limit' => $this->getLimit()
];
debug($config);
$tasks = $this->Paginator->paginate($query, $config);
debug($this->Paginator);
$this->set(compact('tasks'));
The result of debug $config is:
[
'page' => '',
'sort' => 'Tasks.task_name',
'direction' => 'asc',
'limit' => (int) 25
]
The result of debug $this->Paginator is:
object(Cake\Controller\Component\PaginatorComponent) {
'components' => [],
'implementedEvents' => [],
'_config' => [
'page' => (int) 1,
'limit' => (int) 20,
'maxLimit' => (int) 100,
'whitelist' => [
(int) 0 => 'limit',
(int) 1 => 'sort',
(int) 2 => 'page',
(int) 3 => 'direction'
]
]
}
NOTE: The whitelist contains limit, sort, page and direction? And the limit is 20 and I don't even have a selection of 20?
Result
The sort is on the due_date and I need it on the task_name.
Extra Info
If I then click the sort on task_name the sort is on the task_name. All the sorts work just not on the initial load?
Question
How can I configure the pagination property so the sort is on the task_name from the initial load of the search method.
Thanks Z.
The fix is a bit costly and not ideal but it does work. I do a redirect on the initial load. Basically submit the form to search then redirect back to search. IE:
if ($this->request->getQuery('initial') === 'yes') {
$redirect = $this->request->getQuery('redirect', [
'action' => 'search',
'?' => [
'method' => 'search',
'column' => $this->getColumn(),
'input' => $this->getInput(),
'page' => $this->getPage(),
'sort' => $this->getSort(),
'direction' => $this->getDirection(),
'limit' => $this->getLimit(),
'filter' => $this->getFilter(),
]
]);
return $this->redirect($redirect);
exit(0);
}
$config = $this->paginate = [
'sortWhitelist' => [
'Tasks.due_date',
'Tasks.task_name',
'Tasks.type',
'Tasks.priority',
'Tasks.related_to_name',
'Contacts.first_name',
'Contacts.last_name',
'Accounts.account_name',
'Tasks.task_desc'
],
'page' => $this->getPage(),
'sort' => $this->getSort(),
'direction' => $this->getDirection(),
'limit' => $this->getLimit()
];
$tasks = $this->Paginator->paginate($query, $config);
$this->set(compact('tasks'));
The sort is now on the task_name.
This negates the initial load problem and simulates usage after the page initially loads where I know the sorts work.
Right now I've got two tables "Category" and "Product".
I made 'category_id' in "Product" to be equal 'id' in Category, so now in my view it shows all products of that category. But now I need one product to be in several categories.
In controller I use the following data provider:
$dataProvider = new ActiveDataProvider([
'query' => $query = Product::find()->where(['category_id' => $cats->id]),
'sort'=>array(
'defaultOrder'=>['id' => SORT_ASC],
),
'pagination' => [
'pageSize' => 9,
],
]);
Any suggestions on how to realise this feature?
if I did not understand it wrong your data structure to the pricipio
from 1 to N
Categories -> Products
and now you want it to be from N to N
a category with many products and a product with many categories.
So I would like you to create another table called Product_Categy where you keep the primary keys of the 2 tables there and you have to be able to make relations from N to N, you would have a final scheme like this
Categories (id, ...) ->
Categria_Product (id, id_categoria, id_producto, ...) <- Product (id, ..)
Try this:
$dataProvider = new ActiveDataProvider([
'query' => $query = Product::find()->where(['id' => $product_id]),
'sort'=>array(
'defaultOrder'=>['id' => SORT_ASC],
),
'pagination' => [
'pageSize' => 9,
],
]);
OR
$dataProvider = new ActiveDataProvider([
'query' => $query = Product::find()->where(['id' => $product_id])->groupBy(['cats->id']),
'sort'=>array(
'defaultOrder'=>['id' => SORT_ASC],
),
'pagination' => [
'pageSize' => 9,
],
]);
I have a Shops table which can have Products. Other Shops can have the same Products so I used a belongsToMany relation table ShopsProducts.
I can add an infinite number of Products to a Shop and remove them by saving the Shop entity including the relation.
All works fine, but if I want to unlink all Products from a Shop in my form and save, the relation is of cause empty so the Shop will always have 1 Product that I can't delete over the relation but only directly.
This is what the request looks like from the Shops form with a Product
data => [
'name' => 'some',
'is_active' => '1',
'slug' => 'some',
'product_id' => '',
'products' => [
(int) 5 => [
'id' => '5',
'_joinData' => [
'priority' => '0'
]
]
],
]
And this is the request without
data => [
'name' => 'some',
'is_active' => '1',
'slug' => 'some',
'product_id' => '',
]
What is the cake way to handle this issue?
What's your save strategy?
https://book.cakephp.org/3.0/en/orm/saving-data.html#saving-belongstomany-associations
Try replace instead of append.
If that doesn't work for you, check in the before() or afterSave() if products->get('products') is empty, if it is manually call a deleteAll() on the join table for that product an shop.
You will need to set your association to [] to blank it out.
$shops->products = [];
$shops->setDirty('products ', true);
How can I filter data-set in 2 Tables where second table have more results 1:n.
In first table I can use orWhere and I am getting right data, but my another Table contain multiple results and if I use contain or matching I am getting only data from second Table.
So, I want to filter both tables and get matched data.
Here is my query:
First query to filter first table
$query
->where([
'OR' => [
'Orders.id' => $freeText,
'Orders.postal' => $freeText,
'Orders.address LIKE' => '%' . $freeText . '%',
'Orders.city LIKE' => '%' . $freeText . '%',
'Users.first_name' => $freeText,
'Users.last_name' => $freeText,
'ProjectAddresses.cost_centre' => $freeText,
'CONCAT(first_name, last_name) LIKE' => '%' . str_replace(' ', '', $freeText) . '%',
'CONCAT(first_name, last_name) LIKE' => '%' . $freeText . '%',
'Users.first_name IN ' => $splittedKeywords,
'Users.last_name IN ' => $splittedKeywords,
]
]);
Second query - try to filter data from second Table (but still need matched data from first table)
$query->contain('Items', function ($q) use ($freeText) {
return $q->where(['vessel_id' => $freeText]);
});
So problem is if I use second query he automatically take only data from second table and my goal is to get all filtered data (from first and second table).
I have 20+ data-sets like:
(int) 0 => [
'id' => (int) 1,
'uuid' => '5f34ecda-6bc6-46ed-b5cc-b2227029aed8',
'user_id' => (int) 319,
'status' => (int) 30,
'order_price' => (float) 341.04,
'address_id' => (int) 379,
'address' => 'XYZ',
'building_number' => '171',
'postal' => '111',
'city' => 'XYZ',
'country' => 'AT',
'project_address' => [
'id' => (int) 379,
'type' => 'project',
'group_id' => (int) 3,
'default' => false,
'corresponding_invoice_address' => null,
'short_name' => 'XYT',
'comment' => '',
],
'user' => [
'id' => (int) 319,
'uuid' => '675216eb-7110-44d2-82a7-f7f020e934a6',
'title' => 'Herr',
'first_name' => 'Test',
'last_name' => 'Test',
],
'item_groups' => [],
'items' => [
(int) 0 => [
'id' => (int) 26,
'uuid' => 'f4f629be-e25e-4432-8d97-6b2adcee9065',
'item_group_id' => null,
'type' => (int) 2,
'status' => (int) 30,
'vessel_id' => (int) 40001,
'features' => [],
],
(int) 1 => [
'id' => (int) 28,
'uuid' => 'f4f629be-e25e-4432-8d97-6b2adcee9065',
'item_group_id' => null,
'type' => (int) 2,
'status' => (int) 30,
'vessel_id' => (int) 40003,
'features' => [],
],
(int) 1 => [
'id' => (int) 29,
'uuid' => 'f4f629be-e25e-4432-8d97-6b2adcee9065',
'item_group_id' => null,
'type' => (int) 2,
'status' => (int) 30,
'vessel_id' => (int) 40003,
'features' => [],
],
]
],
SQL
SELECT *
FROM orders Orders
INNER JOIN users Users ON Users.id = (Orders.user_id)
LEFT JOIN addresses ProjectAddresses ON ProjectAddresses.id = (Orders.address_id)
WHERE (Orders.id = :c0
OR Orders.postal = :c1
OR Orders.address LIKE :c2
OR Orders.city LIKE :c3
OR Users.first_name = :c4
OR Users.last_name = :c5
OR ProjectAddresses.cost_centre = :c6
OR CONCAT(first_name, last_name) LIKE :c7
OR Users.first_name IN (:c8)
OR Users.last_name IN (:c9))
Parameter is c1 = 4001 || %40001% || %40001
#ndm The goal is If somebody send in $freeText == 40003 I have to get as result this object where vessel_id = 40003 and thats works, but if somebody send in $freeText == Test then I need again same result because first_name == Test 8see first query) and when in second wuery I am using matching/contain this results are removed because he only "fetch" rows that are matching/contain Items...
Basically I want to check 10+ columns with given $freeText variable and if it is matching I want that results in my data-set (from both tables)
If I understood you correctly, then you're looking for a left join filter, ie left join Items (additionally to containing it for retrieval), group by Orders primary key (to avoid duplicates), and then simply add the Items.vessel_id condition to the main queries WHERE clause in order to make it a OR condition too.
$query
->contain('Items')
->leftJoinWith('Items')
->where([
'OR' => [
'Orders.id' => $freeText,
// ...
'Items.vessel_id' => $freeText
]
])
->groupBy('Orders.id');
See also
Cookbook > Database Access & ORM > Query Builder > Filtering by Associated Data
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Order HasOne Suborder
Suborder BelongsTo Order
I need to sort Orders by a field in Suborders, but sorting by virtual fields appears to have been removed in Cake 3.x
In OrdersTable.php, I have
$this->hasOne('Suborder', [
'className' => 'Suborders',
'foreignKey' => 'order_id',
'strategy' => 'select',
'conditions' => function ($exp, $query) {
return $exp->add(['Suborder.id' => $query
->connection()
->newQuery()
->select(['SSO.id'])
->from(['SSO' => 'suborders'])
->where([
'Suborder.order_id = SSO.order_id',
'SSO.suborder_type_id in' => [1, 2, 3]
])
->order(['SSO.id' => 'DESC'])
->limit(1)]);
}
]);
In OrdersController.php, I have
$this->paginate = [
'limit' => 20,
'order' => ['id' => 'desc'],
'sortWhitelist' => [
'id',
'title',
'client_order',
'substatus',
'Workflows.order_status_id',
'Clients.name',
'ProductTypes.type',
'Suborder.due_date',
'Suborder.created',
],
];
$orders = $this->paginate($collection);
In index.ctp, I have
$this->Paginator->sort('Suborder.created', 'Order Placed'),
$this->Paginator->sort('Suborder.due_date'),
and the error I'm getting is Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Suborder.created' in 'order clause'. How do I get Cake to include the Suborder in the initial query for sorting and pagination?
Edit:
$collection = $this->Orders->find()
->contain([
'Clients',
'CurrentAssignment.Users',
'Workflows.OrderStatuses.Category',
'Workflows.OrderStatuses.Departments' => function ($q) use ($uID) {
return $this->Departments->find()->matching('Users', function ($q) use ($uID) {
return $q->where(['Users.id' => $uID]);
});
},
'ClientProducts.ProductTypes',
'Reviews' => function ($q) {
return $q->where(['review_type_id is not' => 6]);
},
'Reviews.ReviewTypes',
'PublicNotes',
'ActiveReview',
'Suborder',
'Suborder.SuborderTypes',
'Suborders.SuborderTypes',
]);
and $collection is modified with 150 lines of wheres, orWheres, and joins based on a number of conditions.
You have configured the assocaition to use the select strategy, which will use a separate query to retrieve the data (currently wrongly documented), hence you cannot reference it in the main query used for pagination.
So you have to use the default join strategy instead if you want to sort on it.
See also
Cookbook > Database Access & ORM > Associations - Linking Tables Together > HasOne Associations