Terry Carter's solution does not provide the desired functionality / result set AND it is conveying the false notion that Joomla "doesn't handle subqueries well". While it is true that there is no method called subQuery()
, Joomla does provide a clean syntax for nesting queries inside of other queries.
The most glaring issue with Terry's query is that it disregards all persons that do not have a BMW
for sale. This is not the desired logic described in the question. The expected result set should include a row for Mary
which contains the lowest-priced non-BMW item.
Furthermore, my solution implements an INNER JOIN instead of a LEFT JOIN so that any names that exist in persons
which do not have a row in cars
will be excluded from the result set. (If the opposite logic is desired, then of course use a LEFT JOIN.) In other words, a LEFT JOIN will potentially generate rows with NULL
values in make
, price
, and color
.
Raw SQL: (DB-Fiddle.com Demo)
SELECT name, make, price, color
FROM persons p
INNER JOIN cars ON p.id = personid
WHERE price = IFNULL(
(SELECT MIN(price) FROM cars WHERE personid = p.id AND make = 'BMW'),
(SELECT MIN(price) FROM cars WHERE personid = p.id)
)
Things to notice:
- I did not add table aliases in the SELECT clause because there is no ambiguity to contend with. In other words, because none of the nominated columns exist in both table, MySQL knows exactly what is intended and so no alarm bells go off.
- The same logic is applied to the first WHERE clause -- I could have written
c.price
, but MySQL already knows which table to draw it from.
- Because
cars
is never used to identify any columns in the query, I have elected to omit the alias c
.
- If you DO decide to declare the table alias
c
and add c.
to each of its columns in the query, take special care to NOT write c.make = 'BMW'
in the first subquery because that will alter the logic and damage the results -- make
in this context needs to refer to the make
value within the subquery.
IFNULL()
is a wise choice for the query because effectively the second subquery is only executed if the first subquery returns no qualifying rows. This condition block ensures that you get the lowest BMW value unless there is no BMW for the respective person in which case, you fallback to the lowest priced "anything" for that person.
Now to use Joomla's query building methods to create the query... As with the great majority of my posts in this community, I'll package my snippet with a query dump and some diagnostic niceties to help researchers to understand and debug their own projects. Please note, that you should never display generated queries nor mysql errors to the public because this can lead to naughty people refining malicious attacks on your system.
try {
$db = JFactory::getDbo();
$bmw_subquery = $db->getQuery(true)
->select("MIN(price)")
->from("cars")
->where("personid = p.id")
->where("make = " . $db->q("BMW"));
$all_subquery = $db->getQuery(true)
->select("MIN(price)")
->from("cars")
->where("personid = p.id");
$query = $db->getQuery(true)
->select("name, make, price, color")
->from("persons p")
->innerJoin("cars ON p.id = personid")
->where("price = IFNULL(($bmw_subquery), ($all_subquery))");
$db->setQuery($query);
JFactory::getApplication()->enqueueMessage($query->dump(), 'info');
if (!$result = $db->loadAssocList()) {
echo "<p>No Rows Found</p>";
} else {
echo "<pre>";
var_export($result);
echo "</pre>";
}
} catch (Exception $e) {
JFactory::getApplication()->enqueueMessage("Query Syntax Error: " . $e->getMessage(), 'error'); // don't show $e->getMessage() to public
}
This will serve up the rendered query described above and the desired result set:
array (
0 =>
array (
'name' => 'Peter',
'make' => 'BMW',
'price' => '6000',
'color' => 'white',
),
1 =>
array (
'name' => 'Paul',
'make' => 'BMW',
'price' => '21000',
'color' => 'silver',
),
2 =>
array (
'name' => 'Mary',
'make' => 'Seat',
'price' => '4200',
'color' => 'green',
),
)
Some parting words:
I would have expected the tables in the question to have names Joomla's random prefix characters. Using these prefixes is best practice and adjusting the Joomla query builder syntax only means prepending #__
like #__cars
and #__persons
which would associate with tables that could be named abcde_cars
and abcde_persons
.
If anyone is hell-bent on writing terse code, the $bmw_subquery
subquery can be generated by cloning the $all_subquery
subquery then chaining the additional where clause condition.
$all_subquery = $db->getQuery(true)
->select("MIN(price)")
->from("cars")
->where("personid = p.id");
$bmw_subquery = (clone($all_subquery))->where("make = " . $db->q("BMW"));
// ^--------------------^-- parentheses are essential
$query = $db->getQuery(true)
->select("name, make, price, color")
->from("persons p")
->innerJoin("cars ON p.id = personid")
->where("price = IFNULL(($bmw_subquery), ($all_subquery))");
*The parentheses are required so that you "clone then chain" instead of "chaining to the original then cloning". If you remove the parentheses, you will find that both subqueries will be identical and both will include AND make = 'BMW'
.