zend framework - can result array from sql query contain database names? - php

If we run a query such as the following:
SELECT `user`.`user_id`
, `user`.`username`
, `profile`.`user_id`
, `profile`.`name`
, `profile`.`location`
FROM `user`
JOIN `profile`
USING (`user_id`)
WHERE `user`.`user_id` = 1;
then we get the result set:
object(ArrayObject)
private 'storage' =>
array
'user_id' => string '1'
'username' => string 'ExampleUsername'
'name' => string 'Example name'
'location' => string 'Example location'
Notice that user_id field is only returned once, even though it exists twice in the SQL query.
Is there a way to return table names as part of the result set? For example, the following result set would be desired:
object(ArrayObject)
private 'storage' =>
array
'user' => array
'user_id' => string '1'
'username' => string 'ExampleUsername'
'profile' => array
'user_id' => string '1'
'name' => string 'Example name'
'location' => string 'Example location'
I have seen this done in other frameworks (Laravel, CodeIgniter) but am not seeing the option for Zend Framework version 2 or 3.
This is just an example SQL query. We are running much more complex queries in our project where a returned associative array with table names as keys would be ideal.

I think you mean you want the keys to include table names, not database names.
IIRC there's no built-in way to do this in Zend Framework.
You can make each key distinct, but it's up to you to do this by defining column aliases:
SELECT `user`.`user_id` AS user_user_id
, `user`.`username`
, `profile`.`user_id` AS profile_user_id
, `profile`.`name`
, `profile`.`location`
FROM `user`
JOIN `profile`
USING (`user_id`)
WHERE `user`.`user_id` = 1;
This is a common problem with any database library that returns results in an associative array, not just Zend Framework and not even just PHP.
The second example you show, of fetching columns into some kind of nested data structure broken down by tables, is not supported in any database library I've ever used. How would it return the results of the following query?
SELECT user.user_views + profile.profile_views AS total_views
FROM user JOIN profile USING (user_id)
Would total_views belong under the user key or the profile key?
There are many other similar examples of SQL queries that return results that don't strictly "belong" to either of the joined tables.

Related

Dynamically add columns to query results via CakePHP 3 ORM queries

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.

MYSQL + PHP : It is possible to union results from one table with data provided by an array?

Scenario:
We have a table where we store our data but also we got some data from an external API. It is possible, to append this external data to our query results so to be able to apply WHERE and ORDER BY conditon.
Example:
$a = array(array('RegDate' => '02-10-2018',
'JobTitle' => 'Web Designer'
),
array('RegDate' => '03-10-2018',
'JobTitle' => 'Account Manager'
),
array('RegDate' => '01-10-2018',
'JobTitle' => 'Web Designer'
),
...
);
$SQL = SELECT RegDate, JobTitle, CandidateName UNION ALL SELECT $a[]['RegDate'] ,$a[]['JobTitle'] WHERE JobTitle LIKE '%Account%' ORDER BY RegDate DESC
You would probably, deleguate your logic to php instead of MySQL here. Retrieve data from Database then append external data to it:
1 - execute query, get data from DB.
2 - Append external data to data previously returned by database
3 - Sort resulting array

Fetch all rows related to user

I am trying to create a json object with data from two tables.
Table 1 holds user information
id
name
username
email
Table 2 holds messages to the users
id
f_id
message
date
So far so good. I have managed to do this with LEFT JOIN
$sql = "SELECT * FROM user LEFT JOIN messages ON user.id = messages.f_id
$result = mysqli_query($conn, $sql) or die(mysql_error());
while ($data = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
$messages[] = array(
'users' => array(
'Id' => $data['id'],
'Name' => $data['name'],
'Username' => $data['username'],
'Email' => $data['email'],
),
'messages' => array(
'f_id' => $data['f_id'],
'message' => $data['message'],
'date' => $data['date'],
);
);
}
but the issue/challenge is that it only returns one row from messages table even if there multiple rows related to the user.
This is what I'm trying to achieve
'user' => array(
'id' => 1,
'name' => John Doe,
'username' => Johndoe,
'email' => johndoe#mail.com,
),
'messages' => array(
'id' => 1,
'f_id' => 1,
'message' => 'Just a test',
'date' => 14-08-2014,
'id' => 2,
'f_id' => 1,
'message' => 'Just a test 1',
'date' => 12-08-2014,
);
How can I achieve this?
Do I need to run a while in while?
You can do it in 2 ways:
1) Query only the users, and then iterate over them and query the messages for each user. That means that you will query the database N+1 times, where N is the number of users.
2) Query the messages joined with the users (and not the opposite), which will result with a lot of "unused" data, but a single query.
Try:
SELECT
user.id AS user_id, user.name, user.email, user.username,
messages.id AS message_id, messages.f_id, messages.message, messages.date
FROM user, messages
LEFT JOIN messages ON user.id = messages.f_id
Note that an alias was needed for user.id and messages.id as both column names are the same.
Please update your sql query
$sql = "SELECT * FROM user Right JOIN messages ON user.id = messages.f_id
or
$sql = "SELECT * FROM messages LEFT JOIN user ON user.id = messages.f_id
You can try that :
SELECT * FROM messages LEFT JOIN user ON user.id = messages.f_id
It will get all messages with users but if a user has no messages, you'll not be able to get it with this kind of query.
What is probably happening is that when you're running the query on the users table, it will use the primary key of that table to ensure that there's no "duplicate" data (as it should). You have two options here:
Query Users, Query Messages [Not Recommended]
This might be a little slow since you'll be querying the database many times. You could do something like
SELECT * FROM users;
then later iterate over the users result and run a query on messages
SELECT * FROM messages WHERE f_id = <user_id>;
What's the issue with this? The more users you have, the more wasteful this is, and also the slower your site will perform. Not recommended.
Query messages, join users [Recommended]
Run this query on messages, rather than users. It will use the primary key of the messages table (which will be id) to determine what is a duplicate or not.
SELECT * FROM messages LEFT JOIN users ON users.id = messages.f_id;
This is much more efficient because it's using one query to grab all the data you need and it should need a little tweaking maybe as your site grows.

zend framework 2 and doctrine query builder

I am trying to write a query using the doctrine query builder based on the parameters i am getting from an array.
Here's my array,
$query = array('field' => 'number,
'from' => '1',
'to' => '100',
'Id' => '2',
'Decimation' => '10'
);
The query that i am trying to write is,
select * from table where (number between 1 AND 100) AND (Id = 2) AND number mod 10 = 0
Here's where i stand now,
if (is_array($parameters['query'])) {
$queryBuilder->select()
->where(
$queryBuilder->expr()->between($parameters['query']['field'], $parameters['query']['from'], $parameters['query']['to']),
$queryBuilder->expr()->eq('Id', '=?1'),
$queryBuilder->expr()->eq($parameters['query']['field'],'mod 10 = 0')
)
->setParameter(array(1 => $parameters['query']['Id']));
}
I just cant wrap my head around this for some reason. Help !! anyone?
Not tested or anything, just directly typed into SO answer box:
$queryBuilder->select('table')
->from('My\Table\Entity', 'table')
->where($queryBuilder->expr()->andX(
$queryBuilder->expr()->between('table.number', ':from', ':to'),
$queryBuilder->expr()->eq->('table.id', ':id'),
))
->andWhere('MOD(table.number, :decimation) = 0')
->setParameters(array(
'from' => $parameters['query']['from'],
'to' => $parameters['query']['to'],
'id' => $parameters['query']['id'],
'decimation' => $parameters['query']['decimation']
));
It does not dynamically let you set which field to apply the where condition to. However, this is most likely a bad idea without whitelisting the values you want to allow. Once this is done a simple modification to the code above (just interpolate the value in place of number in the table.number string).
I kinda got it,
The main idea here was to use the andWhere clause. Multiple where clauses was what i wanted. and the mod operator that doctrine gives. Worked out pretty well. In between the requirement changed as well.

Getting the column 'country_id' in 'where' clause is ambiguous - error in search query in cakephp 1.3

I am working with CakePHP 1.3 version for search functionality using Search Plugin.
I have three models:
Demo,
Country
State
Demo has two foreign keys, country_id and state_id. State has the foreign key country_id.
What I am doing is, I have search form which have country & state drop down which fetch all data from countries & states table. When i search any of country from dropdown & submit the form it will show me below error. If i search using only state dropdown i get the correct result.
When I execute the search query, I get the error
'Column 'country_id' in where clause is ambiguous'
My query is:
SELECT `Demo`.`id`, `Demo`.`demo2`, `Demo`.`desc`, `Demo`.`subject`, `Demo`.`gender`, `Demo`.`country_id`, `Demo`.`state_id`, `Demo`.`image_url`, `Country`.`id`, `Country`.`name`, `State`.`id`, `State`.`country_id`, `State`.`description` FROM `demos` AS `Demo` LEFT JOIN `countries` AS `Country` ON (`Demo`.`country_id` = `Country`.`id`) LEFT JOIN `states` AS `State` ON (`Demo`.`state_id` = `State`.`id`) WHERE `country_id` = 2
Model relationships in Demo table:
var $belongsTo = array(
'Country' => array(
'className' => 'Country',
'foreignKey' => 'country_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'State' => array(
'className' => 'State',
'foreignKey' => 'state_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
);
The controller query to fetch all Country in dropdown is:
$country=$this->Country->find('list'); //just display the list of country in dropdown
The query search the data from all fields except Country (country_id), because it will not know which country_id it is looking for from table Demo or table State. I need the country_id from the demo table to get the correct result.
As I understand you want to make a find over Demo for a specific country_id.
Well you should define which "country_id" you're using because more than one of those tables
has such a column.
Just use Demo.country_id in the conditions array:
array('conditions' => array('Demo.country_id' => 2));
And you should see some SQL generated by Cake like this:
SELECT `Demo`.`id`, `Demo`.`demo2`, `Demo`.`desc`, `Demo`.`subject`, `Demo`.`gender`, `Demo`.`country_id`, `Demo`.`state_id`, `Demo`.`image_url`, `Country`.`id`, `Country`.`name`, `State`.`id`, `State`.`country_id`, `State`.`description` FROM `demos` AS `Demo` LEFT JOIN `countries` AS `Country` ON (`Demo`.`country_id` = `Country`.`id`) LEFT JOIN `states` AS `State` ON (`Demo`.`state_id` = `State`.`id`) WHERE `Demo`.`country_id` = 2
Try this:
SELECT
Demo.id,
Demo.demo2,
Demo.desc,
Demo.subject,
Demo.gender,
Demo.country_id,
Demo.state_id,
Demo.image_url,
Country.id,
Country.name,
State.id,
State.country_id,
State.description
FROM demos AS Demo
LEFT JOIN countries AS Country ON (Demo.country_id = Country.id)
LEFT JOIN states AS State ON (Demo.state_id = State.id) WHERE Demo.country_id = 2

Categories