CakePHP 3 join on a secondary key using ORM - cakephp-3.0

CakePHP 3.5.13 - working with a legacy database. I have a table called substances where the primary key is id. There is also another column in this table called app_id.
The majority of tables in the application have a foreign key which means they can be joined to substances.id. There is one table called article_95 which has a field article_95.app_id and therefore the join must be made to substances.app_id (not substances.id - there is no reference to that field inside the article_95 table at all).
The application performs a search based on up to 11 inputs. I'm using the ORM to dynamically build the query object before executing it.
I begin my query like this:
$query = $Substances->find()->select(['id' => 'Substances.id'])->distinct();
Then if I wanted to do something where the join maps to substances.id, I'm doing it like this:
// Search by CAS Number
if ($this->request->getData('cas_number')) {
$cas_number = $this->request->getData('cas_number');
$query = $query->matching('Cas', function ($q) use ($cas_number) {
return $q->where([
'Cas.value LIKE' => '%'.$cas_number.'%'
]);
});
}
So far so good. If I output the SQL string it looks like this:
'SELECT DISTINCT Substances.id AS `id` FROM substances Substances INNER JOIN cas_substances CasSubstances ON Substances.id = (CasSubstances.substance_id) INNER JOIN cas Cas ON (Cas.value like :c0 AND Cas.id = (CasSubstances.cas_id))'
My problem comes with how to manipulate the query object when it comes to my article_95 table, because it's trying to join on substances.id when I need it to join on substances.app_id.
I have the following in my Table classes. Please note the line $this->setPrimaryKey('tbl_id'); - this is because I'm using a legacy/old database and the primary key of the article_95 table is actually tbl_id not id. However this is relatively insignificant because the join should be based on app_id which exists in both tables.
// src/Model/Table/SubstancesTable.php
public function initialize(array $config)
{
$this->setTable('substances');
$this->setPrimaryKey('id');
$this->hasMany('Article95s', [
'foreignKey' => 'app_id'
]);
// ...
}
// src/Model/Table/Article95sTable.php
public function initialize(array $config)
{
$this->setTable('article_95');
$this->setPrimaryKey('tbl_id');
$this->belongsTo('Substances', [
'foreignKey' => 'app_id',
'joinType' => 'INNER'
]);
}
If I try and do a search which include an Article 95 value the SQL string becomes like this:
'SELECT DISTINCT Substances.id AS `id` FROM substances Substances INNER JOIN cas_substances CasSubstances ON Substances.id = (CasSubstances.substance_id) INNER JOIN cas Cas ON (Cas.value like :c0 AND Cas.id = (CasSubstances.cas_id)) INNER JOIN article_95 Article95s ON (Article95s.entity_name like :c1 AND Substances.id = (Article95s.app_id))'
The problem with this is the part of the SQL string which reads Substances.id = (Article95s.app_id)). I need that to be Substances.app_id = (Article95s.app_id)) but I don't know how to write this with the ORM syntax.
It's also important that the rest of the joins (e.g. CAS Number shown previously) remain joined on substances.id.
Please can someone help?

the manual explain it all
belongsTo
bindingKey: The name of the column in the other table, that will be used for matching the foreignKey. If not specified, the primary key (for example the id column of the Users table) will be used.
$this->belongsTo('Substances', [
'foreignKey' => 'app_id',
'bindingKey' => 'app_id',
'joinType' => 'INNER'
]);
hasMany
bindingKey: The name of the column in the current table, that will be used for matching the foreignKey. If not specified, the primary key (for example the id column of the Articles table) will be used.
$this->hasMany('Article95s', [
'foreignKey' => 'app_id',
'bindingKey' => 'app_id',
]);

Related

Laravel query builder not returning the full row if it does not find a join with another table

Here, my currently working query is
$results = DB::table('amenities as a')
->leftJoin('amenity_values as av','av.amenity_id','=','a.id')
->leftJoin('units_amenities_values as uav','uav.amenity_value_id','=','av.id')
->leftJoin('units as u','uav.unit_id','=','u.id')
->leftJoin('amenity_pricing_reviews as apr','apr.unit_id','=','u.id')
->select('a.id as amenity_id','a.amenity_name','a.category_id', 'av.amenity_value','u.id as unit_id','u.unit_number','apr.dom','apr.building_id')
->where('a.property_id', $data['property_id'])
->whereIn('apr.building_id', $data['building_ids'])
->whereNull('apr.deleted_at')
->whereNull('u.deleted_at')
->whereNull('av.deleted_at')
->whereNull('a.deleted_at')
->orderBy('a.amenity_name','asc')
->get()
->groupBy(['category_id','unit_id']);
Here, I have joined to a table amenity_pricing_reviews. In this relationship whenever it could not find the related rows in the amenity_pricing_reviews table it is discarding the full rows:
However, I want to get these rows with an empty value for those columns from that table. Something like:
amenity_id => value
amenity_name => value
category_id => value
amenity_value => value
unit_id => value
unit_number => value
dom =>
building_id =>
Also, I have done a lot of other things based on the result of this query so I want to make minimal changes to the structure of the end result.
Your whereIn condition can't succeed if the amenity pricing review doesn't exist, so it effectively changes your left joins into inner joins. Perhaps you want that to be just a condition of the join?
->leftJoin('amenity_pricing_reviews as apr', function($join) { $join->on('apr.unit_id','=','u.id')->whereIn('apr.building_id', $data['building_ids']) })
Try join instead of leftJoin. This should help

Alias for table

I have two tables. The first - Product, the second - Category. They contain fields with the same name - 'name'.
In model Product I added following code:
public function getCategory(){
return $this->hasOne(Category::className(), ['id' => 'cat_id']);
}
I need to show in GridView the column from table Category. I added following code for this in the model ProductSearch:
$query->joinWith(['category' => function($query) { $query->from(['cat' => 'category']); }]);
This code adds the alias cat for the table Category.
After that I got an error:
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'name' in where clause is ambiguous
The SQL being executed was: SELECT COUNT(*) FROM `product` LEFT JOIN `category` `cat` ON `product`.`cat_id` = `cat`.`id` WHERE `name` LIKE '%aasdadadsdgdgdg%'
Error Info: Array
(
[0] => 23000
[1] => 1052
[2] => Column 'name' in where clause is ambiguous
)
How can I add the alias for the table Product?
Open your ProductSearch model, navigate to the search($params) method. Below you should see the filtration part:
$query->andFilterWhere(['like', 'name', $this->name])
fix the ambigous part by writing table name product to that line.
$query->andFilterWhere(['like', 'product.name', $this->name])
or..
$query->andFilterWhere(['like', self::tableName() . '.name', $this->name])
This'll giving precise info that name should be queried from product.name table.
The interesting part is that you don't get the error until you join the Category table.
Imho, it's the worst to find this errors as it seems like everything is working, until search functionality is used.

How to specify value in model relation keys?

I have tables user and profile,
one user has max one profile,
and is specified by user_id and table name in profile.
I do not use foreign keys there.
The reason I do it this way, is because I have other tables like company which also uses table profile, so reference is specified by relation_id = primary key of related table and relation = table name
profile
relation_id
relation
What I want to achieve is to set model relation to be equal to string user, so not to use key there, but to use value instead.
User.php
public function getProfile()
{
return $this->hasOne(Profile::className(),
['relation_id' => 'user_id', 'relation' => User::tableName()]);
}
Error I get:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'user.user' in 'on clause'
The SQL being executed was:
SELECT COUNT(*) FROM `user_login`
LEFT JOIN `user` ON `user_login`.`user_id` = `user`.`user_id`
LEFT JOIN `profile` ON `user`.`user_id` = `profile`.`relation_id`
AND `user`.`user` = `profile`.`relation`
It is for generating GridView so sql fails on count first, but error would be the same for select *
SQL I want to achieve:
SELECT * FROM `user_login`
LEFT JOIN `user` ON `user_login`.`user_id` = `user`.`user_id`
LEFT JOIN `profile` ON `user`.`user_id` = `profile`.`relation_id`
AND `profile`.`relation` = 'user'
So the question is, How to specify value in model relation keys?
if your User has a relation hasOne with profile you should use only
public function getProfile()
{
return $this->hasOne(Profile::className(),
['relation_id' => 'user_id']);
}
and if you need a on condition use
public function getProfile()
{
return $this->hasOne(Profile::className(),
['relation_id' => 'user_id'])->andOnCondition(['relation' => User::tableName()]);
}

Selecting distinct values from key/value pairs in a seperate table

I have 2 database tables
JOBS(JOB_ID, JOB_TIME, JOB_NAME,...), JOB_PARAMETERS(JOB_ID,NAME,VALUE)
where JOB_PARAMETERS is essentially a map containing job parameter key value pairs.
Every job may have a unique parameter key/value pairs.
I am looking to pragmatically build a query that will return distinct job id's that contain key/value combinations. Where the values are actually a list of values, comparison operators.
For example:
JOB_PARAMETERS: NAME = 'OUTPUT_FILENAME', VALUE LIKE "ALEX%", "JAX%"
NAME = 'PRIORITY' , VALUE > 7
The above example would automatically filter out all jobs that don't have the OUTPUT_FILENAME and PRIORITY key. Returning All jobs that meet both conditions.
I also need to be able to support pagination and order by.
I was planning on using Perl with DBIx::Class, But I can do it in pure Perl/SQL as well.
I am open to changing the database schema, but every job can have different key/value pairs, so I cant just make them columns in the jobs table.
Thanks in advance.
When using DBIx::Class you can generate a DBIC schema by using Schema::Loader.
After connecting to the database you get a $schema object you can use to get a ResultSet filtered to return the Result objects you want:
my $rs_job_parameters = $schema->resultset('Job_Parameters')->search({
-or => [
{
'name' => 'OUTPUT_FILENAME',
'value' => [{ like => 'ALEX%'}, { like => 'JAX%' }].
},
{
'name' => 'PRIORITY',
'value' => [{ '>' => 7}].
}
]},
{
columns => [qw( job_id )],
group_by => [qw( job_id )], # alternative you can use distinct => 1 to group_by all selected columns
having => \[ 'COUNT(*) = ?', [ 2 ] ],
}
);
my #job_ids = $rs_job_parameters->get_column('job_id')->all;
One can do it in SQL, by grouping JOB_PARAMETERS by JOB_ID and filtering the groups accordingly. For example, if there is a uniqueness constraint over (JOB_ID, NAME), one can query as follows:
SELECT JOB_ID
FROM JOB_PARAMETERS
WHERE (NAME='OUTPUT_FILENAME' AND (VALUE LIKE 'ALEX%' OR VALUE LIKE 'JAX%'))
OR (NAME='PRIORITY' AND VALUE > 7)
GROUP BY JOB_ID
HAVING COUNT(*) = 2
Absent such a uniqueness constraint, COUNT(*) would have to be replaced e.g. with COUNT(DISTINCT NAME).

CakePHP 2.0 inner join and create view

I'm new in Cakephp 2.0, but I want to make a view of two tables with Inner Join.
I have the following tables tables:
hpsas with records: id, ciname, location, status
ldaps with records: id, ciname, status
The query I would do in MySQL is:
SELECT * FROM hpsas INNER JOIN ldaps ON hpsas.ciname = ldaps.ciname;
Which syntax do I have to use in whether the models, the controller or the view.
follow the activeRecord model i think you should define a relationship in the model to avoid running complex queries everytime you need records from this models. you will need to stick to cake's conventions tho and that means you will have 3 models(most possibly)
class cinema extends AppModel{
hasMany = array(hpsas,idaps);
}
class hpsas extends AppModel{
belongsTo = array(cinema)
}
class idap extends AppModel{
belongsTo = array(cinema)
}
with recursive set at above 2 or three one simple query to any one of the models will give you all the data you want(cake magic).
$options['joins'] = array(
array('table' => 'ldaps',
'type' => 'inner',
'conditions' => array(
'hpsas.ciname = ldaps.ciname',
)
)
);
$this->Hpsa->find('all', $options);
See Cake book section on associations for more details: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#joining-tables