I have two tables, AclGroups and AclPermissions, and I want to create a hasMany relationship between them, i.e AclGroups has many AclPermissions.
The condition to determine whether a group owns a given permission is done in a single bitwise check. This is what i'm trying to do:
SELECT
*
FROM
acl_groups
JOIN
acl_permissions ON acl_permissions.permission & acl_groups.permission != 0
In AclGroupsTable I have tried the following:
$this->hasMany('AclPermissions', [
'className' => 'AclPermissions',
'foreignKey' => 'permission',
'conditions' => [
'AclPermissions.permission & AclGroups.permission !=' => 0
]
]);
But that just gives me
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'aclgroups.permission' in 'where clause'
In my controller I do:
$this->AclGroups->find('all')->contain(['AclPermissions']);
I suppose the real question is: Is there a way I can change the conditions of the ON clause in the query that fetches associated records
As mentioned in the comments, records of hasMany associations (and belongsToMany for that matter) will always be retrieved in a separate query when using contain().
If you need to create joins with such an association, then you must explicitly use the corresponding functionality for joins, for example leftJoinWith():
$this->AclGroups->find('all')->leftJoinWith('AclPermissions');
This will create a query similar to the one you are showing. However it would also generate the default conditions using the configured foreign key, you'd have to disable the foreign key in order to avoid that, like:
$this->hasMany('AclPermissions', [
'foreignKey' => false, // << like this
// ...
]);
Given that the association conditions won't work with contain() (and disabling the foreign key will make it even more unusable for that purpose), you may want to create a separate association for your joining purposes, or use the "lower level" join methods, where you specify all the conditions manually (you can for example put this in a custom finder in order to keep your code DRY):
$query = $this->AclGroups
->find('all')
->join([
'table' => 'acl_permissions',
'alias' => 'AclPermissions',
'type' => 'LEFT',
'conditions' => [
'AclPermissions.permission & AclGroups.permission != :permission'
]
])
->bind(':permission', 0, 'integer');
Note that the value is being explicitly bound here to ensure that the correct type is being used, as it couldn't be determined from the non-standard left hand value (which isn't really ment to contain SQL snippets - you may want want to look into using expressions).
See also
Cookbook > Database Access & ORM > Query Builder > Using leftJoinWith
Cookbook > Database Access & ORM > Query Builder > Adding Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods
Related
In CakePHP 3.x I can do this:
$Substances = TableRegistry::get('Substances');
$query = $Substances->find()->where($where_conditions)->select(['id']);
debug($query);
This will show me the Query object.
If I want to get the SQL string I can use debug($query->sql());. This will give me the SQL string with placeholders for any parameters, e.g.
SELECT ... WHERE ... in (:c0,:c1,:c2))
When using debug($query) I can see the values for :c0, :c1, etc:
'params' => [
':c0' => [
'value' => (int) 47,
'type' => 'smallinteger',
'placeholder' => 'c0'
],
':c1' => [
'value' => (int) 93,
'type' => 'smallinteger',
'placeholder' => 'c1'
],
':c2' => [
'value' => (int) 845,
'type' => 'smallinteger',
'placeholder' => 'c2'
],
':c3' => [
'value' => (int) 354,
'type' => 'smallinteger',
'placeholder' => 'c3'
]
]
However, I cannot access them outside the debug statement. For example $query->params() or $query['params'] doesn't give me the parameters - it will error. I want to be able to pass this array to a custom function, so how can I access it?
It's strange because I can use debug($query->sql()) to get the SQL string as above, and params is just another thing in that object, but doesn't seem to be accessible.
I've read How to get params from query object in CakePHP 3 but think that's a different question as it was to do with not seeing the values in the debug statement due to the default depth that debug would provide.
The reason I want to do this is because I want to be able to do a CREATE TABLE AS query that will write the values of the SELECT statement into a new table (Important: see this link for an example of how that works in vanilla MySQL). I can't figure out how to do that with the ORM in Cake, so was planning on writing a custom function. But I need to be able to access both the SQL as well as the parameters bound so that the query can be executed in my own function.
If you know of a solution where I can use the ORM to do the CREATE TABLE AS query, I'm still interested to know about this. However I would like to know if params are accessible outside debug() as well.
Premise: I did not actually understand why you need the params
anyway. The information you need is stored by the query ValueBinder object
so you could simply do
$params = $query->getValueBinder()->bindings();
debug($params);
but for some reason this will get you an empty array. My guess is that the query need some kind of initialization first.
in fact if you run
debug($query);
$params = $query->getValueBinder()->bindings();
debug($params);
you'll see your params. I think someone more expert than me will come and give a full explanation
edit: I noticed that debugging $query calls $query->sql() which in turns calls conection->compileQuery();
so you can do
$query->sql(); // you need this to compile the query
// and generate the bindings
$params = $query->getValueBinder()->bindings();
CakePHP does not provide specific methods for creating such CREATE TABLE AS statements, so you'll have to build that on your own.
Compiling a query as the one shown in your question is simple enough using the query objects sql() method, and as arilia already mentioned, you'll be able to access the parameters bound to that query after is was compiled.
Having the compiled SQL and the associated value binder, you can combine this with a custom raw query to build your CREATE TABLE AS statement. All you need to do is prepare a new statement with the compiled SQL, and attach the value binder via its own attachTo() method.
One thing you might also have to do, is to define custom aliases in your select(), as otherwise you'd end up with columns selected (and created) in the form of Substances_id.
$Substances = TableRegistry::get('Substances');
$selectQuery = $Substances
->find()
->where($where_conditions)
->select(['id' => 'id']); // < create aliases as required
// compile the ORM query, this will populate the value binder
$selectSql = $selectQuery->sql();
// combine table creation SQL and compiled ORM query in a single statement
$createStatement = $Substances
->getConnection()
->prepare('CREATE TABLE dynamic_table AS ' . $selectSql);
// attach the ORM querys value binder, binding all its values to the given statement
$selectQuery->getValueBinder()->attachTo($createStatement);
$success = $createStatement->execute();
This should create SQL similar to:
CREATE TABLE dynamic_table AS
SELECT
id AS id
FROM
substances Substances
WHERE
...
See also
Cookbook > Database Access & ORM > Database Basics > Interacting with Statements
API > \Cake\ORM\Association::attachTo()
I read the cookbook, but I can not figure out how to combine in a single query a matching() and a orWhere().
Example: I have Photo that belongs from Album. Both have the active field. So I'm trying to write a findInactive() method. A "inactive" photo has the active field as false or matching an album that has the active fields as false.
Something like this:
public function findInactive(Query $query, array $options)
{
$query->matching('Albums', function ($q) {
return $q->where(['Albums.active' => false]);
})
->orWhere(['Photos.active' => false])
->enableAutoFields(true);
return $query;
}
But that does not work:
'SELECT [...] FROM photos Photos INNER JOIN photos_albums Albums ON (Albums.active = :c0 AND Albums.id = (Photos.album_id)) WHERE Photos.active = :c1'
How to do? Thanks.
EDIT
Maybe a possible solution is usecontain():
$query->contain(['Albums => ['fields' => ['active']]])
->where(['Photos.active' => false])
->orWhere(['Albums.active' => false]);
But is it not possible to use matching() or innerJoinWith()?
Add the conditions to the main query
matching() or innerJoinWith() with conditions is the wrong way to go, as the conditions are being addded to the INNER joins ON clause, which will cause the Photo row to be excluded in case the Albums.active condition doesn't match.
If you want to only receive photos that belong to an album, then you want to use matching() or innerJoinWith(), but you'll have to add the conditions to the main query instead, ie:
$query
->innerJoinWith('Albums')
->where(['Albums.active' => false])
->orWhere(['Photos.active' => false])
// ...
In case a photo doesn't have to belong to an album, or it's not important whether it does, you could use either leftJoin(), leftJoinWith(), or even contain().
The latter however may use the INNER joinStrategy and/or the select strategy (which uses a separate query), so you'd need to take care of ensuring that it uses LEFT and join instead. Using containments however is usually only advised if you actually want to contain something, and given that your finder seems to be supposed to just filter things, I'd say go with leftJoinWith() instead:
$query
->leftJoinWith('Albums')
->where(['Albums.active' => false])
->orWhere(['Photos.active' => false])
// ...
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data Via Matching And Joins
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Retrieving Associated Data
Cookbook > Database Access & ORM > Associations - Linking Tables Together > BelongsTo Associations
I have a master model, with a child model. The child model has a foreign key that links to the master model id, following cakephp naming conventions. The child model defines the name/description of the records in the master model.
I'm trying to populate a dropdown listbox in my view with these values (id and name). But the Cakephp find('list') usually gets the information from one model only.
How can I do it so that my find('list') retrieves 2 fields such as :
- child.master_id
- child.name
or
- master.id
- child.name (where child.master_id = master.id).
Thanks!
To change the fields for the keys or values of find('list', $params) you can hand an array with the key fields and the names of the fields you want to use as the second parameter over.
In your example the following code would change the key of the returned array to child.master_id and the value to child.name:
$childs = $this->Child->find('list', array(
'fields' => array('Child.master_id', 'Child.name')
));
For further information on the find type 'list' read the cookbook.
Just define the fields
For the examples given in the question you need only specify the two fields in the find call:
$result = $Child->find('list', array(
'fields' => array('master_id', 'name')
));
When find list is passed two fields, they are used as the key and value of the resultant array.
If you do need to return fields from different models, you need to tell cake to make a join. The simplest way is to specify a recursive value of 0 (Assuming the relationship Child belongsTo Master has been setup correctly):
$sameResult = $Child->find('list', array(
'fields' => array('Master.id', 'Child.name'),
'recursive' => 0
));
Note that in this scenario it is probably necessary to specify the model name in the field list to avoid ambiguous sql being generated.
For now I am using following code to get books from certain category:
$options['conditions']['Category.id'] = $category_id;
$options['joins'] = array(
array(
'table' => 'books_categories',
'alias' => 'BookCategory',
'type' => 'inner',
'conditions' => array('Book.id = BookCategory.id_book'),
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions' => array('BookCategory.kat = Category.id'),
),
);
$this->Book->find('all',$options);
This just finds all books from given category_id.
So there are 3 tables: categories,books and books_categories. books_categories has two fileds: book_id and category_id so basicly its just HABTM relation. The problem is that one book may belong to many categories and I want for example find all books from category 5 but excluding books from categories 5,6 and 7. How I can do this?
edit -------------------------------------------------------------------------------
Ok so I figured out how it should look in pure SQL - the conditions should be like this:
where
category_id = <given category>
and books.book_id not in
(
select book_id from book_categories
where category_id in (<given set of cat>)
)
order by books.inserted
this will get all books from one category but excluding books from a set of other categories.
Now I want to force Cake to generate similar SQL query.
I tried so far:
$options['conditions']['Category.id'] = $category_id;
$options['conditions']['AND'][] = 'Book.id NOT IN (SELECT id_book FROM book_categories WHERE kat IN (133,134))';
$options['order'] = array('Book.inserted' => 'desc');
$options['joins'] = array(
array(
'table' => 'book_categories',
'alias' => 'BookCategory',
'type' => 'inner',
'conditions' => array('Book.id = BookCategory.id_book'),
),
array(
'table' => 'categories',
'alias' => 'Category',
'type' => 'inner',
'conditions' => array('BookCategory.kat = Category.id'),
),
);
This generates this query (sory - table names are little bit different):
SELECT `Book`.`id`, `Book`.`OK`, `Book`.`price`, `Book`.`link`, `Book`.`title`,
`Book`.`author`, `Book`.`img`, `Book`.`inserted`, `Book`.`checked`, `Book`.`big_img`, `Book`.`lang`, `Book`.`asin`, `Book`.`description`, `Book`.`last_update`, `Book`.`review`, `Book`.`changed`
FROM `amazon`.`linki` AS `Book`
inner JOIN `amazon`.`cross_kategorie_full` AS `BookCategory` ON (`Book`.`id` = `BookCategory`.`id_book`)
inner JOIN `amazon`.`kategorie` AS `Category` ON (`BookCategory`.`kat` = `Category`.`id`)
WHERE `Category`.`id` = 4
AND `Book`.`OK` = 'OK'
AND ((`Book`.`big_img` NOT LIKE '%no-image%')
AND (`Book`.`id` NOT IN (SELECT id_book FROM cross_kategorie_full WHERE kat IN (133,134))))
ORDER BY `Book`.`inserted` desc LIMIT 20
But there is error: Maximum execution time of 30 seconds exceeded - so There is something that doesnt end (loop?) in this sql statement...
Update relative to updated question
For the sql to yield correct results (in an acceptable time) you'll need to join with Categories again giving it another alias. Since this leads to another question, I suggest you post it tagged with mysql and query-optimization.
End update
As it is, a HABTM relationship is a bit devious (since it really isn't a HABTM). If you have only one row per book-category match in books_categories you can't know to what other categories a certain book belongs to, so you can't really tell which ones you really want (i.e. don't belong in those other categories). It's CakePHP's data layer and models that solve this problem for you behind the scenes :)
The only solution I see is to use Set::extract to further query the results that you get and filter out Books that belong to Categories that you didn't want to include. (something like:
// returns all books not belonging to categories 3,4
$new_result = Set::extract('/Books/Category[id!=3][!=4]', $results);
On a side note, I find it very useful in cases like this, to query the DB and visualize the complexity of the SQL query that gets you the required results. Also, you should activate the CakePHP debug toolbar to see the SQL queries that are sent to the DB so you have a better idea of what's going on behind the scenes.
The CakePHP book, advises the following at the end of "Associations: Linking models together" Section (emphasis mine).
Using joins allows you to have a maximum flexibility in how CakePHP handles associations and fetch the data, however in most cases you can use other tools to achieve the same results such as correctly defining associations, binding models on the fly and using the Containable behavior. This feature should be used with care because it could lead, in a few cases, into bad formed SQL queries if combined with any of the former techniques described for associating models.
I think there is a typo in there. You want to get all books from category 5 but not from category 5, 6 and 7?
Nevermind. Cake convensions state out that there should be always a primary key within the HABTM table, so you may add a "id" column. With this the next steps are much easier or should I rather say: "They are getting possible".
What you do next is, you create an association model called "BooksCategory".
Use the 'with' index explained here to link your Book and Category models with each other over that new model. Don't forget to use plugin syntax (PluginName.ModelName) in case the linking model belongs to a plugin.
You are now putting two belongsTo associations in the linking model for each of the models you are linking. This makes sure to fetch those while finding.
Next thing you do is:
$this->Book->BooksCategory->find(
'all',
array(
'conditions' => array(
'BooksCategory.category_id' => 5
)
)
);
Finished ;) No you get only the books from category 5.
Greetings
func0der
I have a nodes table (Node model). I'd like it to be associated to different data types, but only if one if it's field is set to 1.
Example:
My nodes table has a data_article field (tinyint 1). I only want the Node to $hasMany Article IF that field is a 1.
I tried this:
public $hasMany = array(
'Article' => array(
'conditions' => array('Node.data_articles' => '1')
),
);
But I get an error:
Column not found: 1054 Unknown column 'Node.data_articles' in 'where
clause'
Because the association is doing the Article find in it's own query:
SELECT `Article`.`id`, `Article`.`title`, `Article`.`node_id`, ...more fields...
FROM `mydatabase`.`articles` AS `Article`
WHERE `Node`.`data_artiles` = '1'
AND `Article`.`node_id` = ('501991c2-ae30-404a-ae03-2ca44314735d')
Obviously that doesn't work, since the Node table isn't being Joined at all in this query.
TLDR:
Is it possible to have associations or not based on a field in the main model? If not, how else can I keep different data types in multiple tables, and not have to query them all every time?
I don't think that there's a standard way to do this with CakePHP (at least I can't imagine a way). What definitely is possible would be binding associated models dynamically.
So you might query your model without associations by passing the recursive parameter as -1 to the find() method and based on the result unbind the associated models dynamically. Afterwards you would have to query again, for sure. You might build a behavior out of this to make it reusable.
A quite elegant solution would be possible, if Cake could make a two-step query with a callback after the main model was queried, but before associated models are queried, but this isn't possible at the moment.
Edit: There might be a non-Cake way to archieve this more performantly with a custom Query including IF-statements, but I'm not the SQL expert here.
You should do it from the other side:
public $belongsTo = array(
'Node' => array(
'conditions' => array('Node.data_articles' => '1')
),
);