UPDATE
This is the join from the SQL after all the selects,
FROM users Users
LEFT JOIN userinfos Userinfos ON Userinfos.id = (Users.userinfo_id)
INNER JOIN offices Offices ON Offices.id = (Userinfos.office_id)
ok, I have a database (MySQL) setup with CakePHP 3 the users table has extended information held within another table.
This other table is also extended with an office / address information however this is set to NULL by default, so I want to include it in a 'contain' call but dose not return the user data when its empty?
So this is what I use currently,
$UsersTable->find('all')
->contain(['Userinfos','Userinfos.Offices'])
->toArray();
But, Offices (office_id field in table Usersinfos) is not always set, however I still want it to return all the users even if they don't have an office set.
So I have tried,
$UsersTable->find('all')
->contain([
'Userinfos',
'Userinfos.Offices'
])
->where(['Userinfos.office_id IS' => NULL])
->toArray();
Also,
$UsersTable->find('all')
->contain([
'Userinfos',
'Userinfos.Offices' =>
function($q) {
return $q->where(['Userinfos.office_id IS' => NULL]);
}
])
->toArray();
The $UsersTable var is set to
$UsersTable = TableRegistry::get('Users');
If I remove the Userinfos.Offices from the contain condition, then it returns all my current users. However, how can I call in this extra data/information so I have access to the Office information, e.g location name if that is set?
*I may not have explain myself clearly, please let me know if there is anything I can explain better, thanks.
In cakePhp3.x in default when you bake a Model the join table is set to INNER JOIN. You would need to modify the model associations. You need to set the association to LEFT JOIN.
In your case if you look at 'UserinfosTable' in src/model/UserinfosTable make sure you are 'LEFT JOIN' ing the 'User' table or you can completely remove the 'joinType' because in default cakephp sets the Contain to 'LEFT JOIN'
class UserinfosTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Userinfos', [
'foreignKey' => 'office_id',
'joinType' => 'LEFT'
]);
}
}
Try adding the IS NULL clause as a value instead of a key/value pair :
ex:
$UsersTable->find('all')->where(['Userinfos.office_id IS NULL'])
This is the correct way to do NOT NULL (null is just isNull(
->where(function ($exp, $q) {
return $exp->isNotNull('office_id');
});
Related
I use a union to join two datasets and then the following query to setup for pagination correctly
$paginationQuery = $this->find('all')
->contain(['EmailAddresses' => [
'foreignKey' => false,
'queryBuilder' => function($q) {
return $q->where(['Members__id' => 'EmailAddresses.member_id']);
}
]])
->select( $selectMainUnion )
->from([$this->getAlias() => $query])
->order(['Members__last_name' => 'ASC', 'Members__first_name' => 'ASC']);
I have also tried
$paginationQuery = $this->find('all')
->contain(['EmailAddresses'])
->select( $selectMainUnion )
->from([$this->getAlias() => $query])
->order(['Members__last_name' => 'ASC', 'Members__first_name' => 'ASC']);
and tried
$query->loadInto($query, ['EmailAddresses']); where $query is the result of the union.
Neither of these result in email addresses added to $paginationQuery.
Is there a way to do this?
Adding to clarify the code
$selectMain =['Members.id',
'Members.member_type',
'Members.first_name',
'Members.middle_name',
'Members.last_name',
'Members.suffix',
'Members.date_joined'];
foreach($selectMain as $select) {
$selectMainUnion[] = str_replace('.', '__', $select);
}
$this->hasMany('EmailAddresses', [
'foreignKey' => 'member_id',
'dependent' => true,
]);
Looking at the SQL in DebugKit SQL Log, there is no reference to the EmailAddresses table.
Generally containments do work fine irrespective of the queries FROM clause, whether that's a table or a subquery should be irrelevant. The requirement for this to work however is that the required primary and/or foreign key fields are being selected, and that they are in the correct format.
By default CakePHP's ORM queries automatically alias selected fields, ie they are being selected like Alias.field AS Alias__field. So when Alias is a subquery, then Alias.field doesn't exist, you'd have to select Alias.Alias__field instead. So with the automatic aliases, your select of Members__id would be transformed to Members.Members__id AS Members__Members__id, and Members__Members__id is not something the ORM understands, it would end up as Members__id in your entities, where the eager loader would expect id instead, ie the name of the primary key which is used to inject the results of the queried hasMany associated records (this happens in a separate query), your custom queryBuilder won't help with that, as the injecting happens afterwards on PHP level.
Long story short, to fix the problem, you can either change how the fields of the union queries are selected, ie ensure that they are not selected with aliases, that way the pagination query fields do not need to be changed at all:
$fields = $table->getSchema()->columns();
$fields = array_combine($fields, $fields);
$query->select($fields);
This will create a list of fields in the format of ['id' => 'id', ...], looks a bit whacky, but it works (as long as there's no ambiguity because of joined tables for example), the SQL would be like id AS id, so your pagination query can then simply reference the fields like Members.id.
Another way would be to select the aliases of the subquery, ie not just select Member__id, which the ORM turns into Member__Member__id when it applies automatic aliasing, but use Members.Member__id, like:
[
'Member__id' => 'Members.Member__id',
// ...
]
That way no automatic aliasing takes place, on SQL level it would select the field like Members.Member__id AS Member__id, and the field would end up as id in your entities, which the eager loader would find and could use for injecting the associated records.
I am attempting to do a GroupBy on an associated table via contains -> conditions, however I am getting the following error...
Error: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'group = 'BrandUsers.user_id' AND BrandUsers.brand_id in (1,2,3,4,5,6))' at line 1
with the following query
SELECT
BrandUsers.id AS `BrandUsers__id`,
BrandUsers.user_id AS `BrandUsers__user_id`,
BrandUsers.brand_id AS `BrandUsers__brand_id`,
Users.id AS `Users__id`,
Users.username AS `Users__username`,
Users.email AS `Users__email`,
Users.password AS `Users__password`,
Users.first_name AS `Users__first_name`,
Users.last_name AS `Users__last_name`,
Users.token AS `Users__token`,
Users.token_expires AS `Users__token_expires`,
Users.api_token AS `Users__api_token`,
Users.activation_date AS `Users__activation_date`,
Users.secret AS `Users__secret`,
Users.secret_verified AS `Users__secret_verified`,
Users.tos_date AS `Users__tos_date`,
Users.active AS `Users__active`,
Users.is_superuser AS `Users__is_superuser`,
Users.role AS `Users__role`,
Users.created AS `Users__created`,
Users.modified AS `Users__modified`
FROM
brand_users BrandUsers
INNER JOIN
users Users
ON Users.id =
(
BrandUsers.user_id
)
WHERE
(
group = :c0
AND BrandUsers.brand_id in
(
:c1,
:c2,
:c3,
:c4,
:c5,
:c6
)
)
I have taken a look at the following links, but the above error persists
Group By within contain cakephp
cakephp GROUP BY within containable
Here is my code
$this->paginate = [
'contain' => [
'BrandUsers' => [
'conditions' => [
'group' => 'BrandUsers.user_id'
]
],
'BrandUsers.Users'
]
];
$brands = $this->paginate(
$this->Brands
->find('all')
->where(['Brands.user_id' => $this->Auth->user('id')])
);
As mentioned in the answers/comments to the questions that you've linked, there is no group option for containments, that's true for CakePHP 2.x as well as 3.x, and if there was such an option you would have placed it wrong, as you've nested it inside the conditions option, hence it is being compiled into the queries WHERE clause.
If you need to modify the query used for obtaining containments on the fly, then you can for example pass a callable as known from other query builder methods:
'BrandUsers' => function (\Cake\ORM\Query $query) {
return $query->group('BrandUsers.user_id');
}
or use the finder option to point to a finder that modifies the passed query accordingly:
'BrandUsers' => [
'finder' => 'groupedByUser'
]
It should be noted that grouping only works for HasMany and BelongsToMany associations, as they are not being joined into the main/parent query.
See also
Cookbook > Database Access & ORM > Query Builder > Passing Conditions to Contain
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods
I have a table called posts that stores all the posts . It has two columns "Created" and "Modified" .
Below is my query in the model :
$options = [
'conditions' => [
'circle_id' => $my_circle_list,
'team_id' => $this->current_team_id,
'modified BETWEEN ? AND ?' => [$start, $end],
],
'order' => ['modified'=> 'desc'],
'limit' => $limit,
'fields' => ['post_id'],
];
$res = $this->find('list', $options);
Now i want the latest edited posts on top and below is what my mysql dump reads like :
SELECT `Post`.`id` FROM `db`.`posts` AS `Post` WHERE `Post`.`id` IN (125, 124) AND `Post`.`del_flg` = '0' ORDER BY `Post`.`modified` desc LIMIT 20
If i run this query in my database editor ,it gives my the correct output , but in my controller the ordering changes again , this is something i figured after i debugged the array values.
Would be helpful if anyone could tell me if there's any specific reason behind this . The call to this model method from the controller is done in a conventional manner .
When using $this->find() after Cake has queried the database with the generated SQL it calls the afterFind() method on the model where it can manipulate the data. This is the mostly likely place for the ordering to have been modified.
I an looking to use a JOIN to select data from a table and a view in CakePHP like so :
$this->Annonces->find('all')
->where($arrFiltres)
->order($arrOrder)
->join([
'table' => 'annonces_suivis',
'alias' => 'AnnoncesSuivis',
'conditions' => [...],
]);
And would like to be able to select all the fields from the first table and som of the jointed table like so :
->select(['Annonces.*', 'AnnoncesSuivis.id']);
But this creates a faulty SQL query.
.* isn't supported by the ORM Query, it will convert this to
Annonces.* AS Annonces__*
which is invalid SQL. It would work with the lower level Database Query (Connection::newQuery()), which doesn't add aliases, however it won't return entities, so that's probably not what you want.
See Cookbook > Database Access & ORM > Database Basics > \Cake\Database\Connection::newQuery()
Pass a table object
As of CakePHP 3.1 you can pass table objects to Query::select(), which will cause all the fields of the table to be selected.
$this->Annonces
->find('all')
->select(['AnnoncesSuivis.id'])
->select($this->Annonces)
->join([
'table' => 'annonces_suivis',
'alias' => 'AnnoncesSuivis',
'conditions' => [ /* ... */ ],
])
->where($arrFiltres)
->order($arrOrder);
That way the AnnoncesSuivis.id field, and all fields of Annonces will be selected.
See Cookbook > Database Access & ORM > Query Builder > Selecting All Fields From a Table
Build the fields from the schema
That's what passing a table object will cause internally too, and it's also supported in CakePHP < 3.1.
$query = $this->Annonces->find('all');
$fields = $query->aliasFields(
$this->Annonces->schema()->columns(),
$this->Annonces->alias()
);
$query
->select(array_merge(['AnnoncesSuivis.id'], $fields))
->join([
'table' => 'annonces_suivis',
'alias' => 'AnnoncesSuivis',
'conditions' => [ /* ... */ ],
])
->where($arrFiltres)
->order($arrOrder);
This would also work for the fields option that can be passed to Table::find(), though you'd have to use a separate query object in that case, like
$fields = $this->Annonces->query()->aliasFields(
$this->Annonces->schema()->columns(),
$this->Annonces->alias()
);
$this->Annonces->find('all', [
'fields' => array_merge(['AnnoncesSuivis.id'], $fields)
// ...
]);
Use Query::autoFields()
In ealier CakePHP version, you could also make use of Query::autoFields(), which, when set to true, will automatically include the fields of the main table and possible containments.
See Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Passing Conditions to Contain
Auto selecting all fields is the default behavior until you set fields via Query::select(), in that case you'll have to explicitly enable Query::autoFields().
$this->Annonces
->find('all')
->select(['AnnoncesSuivis.id'])
->autoFields(true)
->join([
'table' => 'annonces_suivis',
'alias' => 'AnnoncesSuivis',
'conditions' => [ /* ... */ ],
])
->where($arrFiltres)
->order($arrOrder);
This should give you the desired query, however as mentioned this will only work for the main table and containments, if you'd wanted to include all fields of a manually joined table, then you'd have to specify them one by one.
You also can create virtual field in Entity:
namespace App\Model\Entity;
use Cake\ORM\Entity;
class User extends Entity {
protected function _getFullName() {
return $this->_properties['first_name'] . ' ' . $this->_properties['last_name'];
}
}
echo $entity->full_name;
So I have a customers table, a contacts table, and a contacts_customers join table, which also has contact_type_id field which maps to a contact_type table. I have used the information linked to from CakePHP update extra field on HABTM join table in an attempt to come up with a solution, but it's only partly working. I changed the add() method in the ContactCustomersController to
$this->ContactsCustomer->Contact->save($this->data);
$this->ContactsCustomer->create();
if ($this->ContactsCustomer->save($this->data,
array('validate' => 'first'))) {
$this->Session->setFlash(__('The contacts customer has been saved', true));
$this->redirect(array('controller' => 'contacts_customers', 'action' => 'index'));
} else {
$this->Session->setFlash(__('The contacts customer could not be saved. Please, try again.', true));
}
Note the two-part save; this is because using the saveAll() method of the ContactsCustomer model was just out and out failing with no indication of the problem (all that showed up in the SQL log on the resulting page was a BEGIN immediately followed by a ROLLBACK). My assumption here was that this was due to the foreign key constraint between the contacts table and the contacts_customers table, so I opted for the two-part save.
In any event, there are no errors reported in the current code, but the end result is that the appropriate information is saved in the contacts table, but nothing is saved in the contacts_customers join table. The data posted appears to be appropriate according to the FireBug log data:
_method POST
data[Contact][address_1] asdsad
data[Contact][address_2]
data[Contact][city] asdasd
data[Contact][email] jb#sc.net
data[Contact][fax]
data[Contact][first_name] Joe
data[Contact][last_name] Blow
data[Contact][mobile_phon...
data[Contact][office_phon... sdfsdfdf
data[Contact][postal_code... 121212
data[Contact][state] asdas
data[Contact][title]
data[ContactsCustomer][ContactType]
data[ContactsCustomer][ContactType][] 1
data[ContactsCustomer][ContactType][] 2
data[ContactsCustomer][ContactType][] 3
data[ContactsCustomer][Customer] 1
What have I done incorrectly here? Many thanks to any assistance offered!
Relevant model data:
contact.php
var $hasMany = array(
'ContactsCustomer'
);
contact_types.php
var $hasMany = array(
'ContactsCustomer'
);
customer.php
var $hasMany = array(
'ContactsCustomer'
);
contacts_customer.php
var $belongsTo = array(
'Contact', 'ContactType', 'Customer'
);
haven't tested that yet, but I believe $this->data might have been modified by the first save. Just use an extra variable to hold that data.