LEFT Join onto two tables in doctrine2 query builder - mysql

I have an entity called tracking. It relates to a User and a Course.
I also have an entity called credential which officially links User and Course
User <- Tracking -> Course || User <- Credential -> Course
Tracking is a joining entity of sorts, but it's not the primary join between the two. I have a query where I already have the user and the course joined, and I want to left-join the tracking if it exists. I've tried to simplify my example.
$q->select(
'user.id',
'user.firstname',
'user.lastname',
'count(t) as courses',
'count(t.completed) as completed'
);
$q->from(Credential::class, 'c');
$q->from(Course::class, 'course');
$q->from(User::class, 'user');
$q->leftJoin(Tracking::class, 't', 'WITH', 't.user = user and t.course = course');
$q->where('c.user = user and c.object = course');
$q->groupBy('user');
What I'm trying to achieve here, is a list of users who are enrolled in courses, the number of courses, and where possible the number of completed courses.
Unfortunately, doctrine can only seem to join to either the user table or the course table, but not to both. This might even be a mysql limitation. I've debugged this over and over - and I've run into the problem several times with different examples - and I just can't seem to find a solution other than using ->from(Tracking) which would eliminate students who haven't started any courses, and stats from courses they haven't started. I've googled over and over again, but It's so hard to search for this problem and not get 'How to join two tables with Doctrine'.
I get the error Column not found: 1054 Unknown column 'c1_.id' in 'on clause' which I assume means it can join on t.user = user but not t.course = course
Here is the actual code and error
$q = $this->em->createQueryBuilder();
$q->select(
'user.id',
'user.firstname',
'user.lastname',
'count(sc.id) as courses',
'count(ct.commenced) as commenced',
'count(ct.completed) as completed',
'avg(ct.scorePercent) as avgscore',
'avg(ct.totalTime) as avgtime'
);
$q->from(Security\Credential::class, 'c');
$q->from(Security\SecurableCourse::class, 'sc');
$q->from(Security\AccreditableInheritance::class, 'ai');
$q->from(Security\AccreditableUser::class, 'au');
$q->from(User::class, 'user');
$q->join(Tracking\CourseTracking::class, 'ct', 'WITH', 'ct.objectIdentity = sc and ct.user = user');
$q->where('sc = c.securable and ai.parent = c.accreditable and au = ai.child and user = au.user');
$q->andWhere('c.action = :action and sc.course in (:courses)');
$q->setParameter('action', 'study')->setParameter('courses', $courses);
$q->groupBy('user.id');
$users = $q->getQuery()->getScalarResult();
Doctrine\DBAL\Exception\InvalidFieldNameException(code: 0): An exception occurred while executing 'SELECT u0_.id AS id_0, u0_.firstname AS firstname_1, u0_.lastname AS lastname_2, count(s1_.id) AS sclr_3, count(t2_.commenced) AS sclr_4, count(t2_.completed) AS sclr_5, avg(t2_.scorePercent) AS sclr_6, avg(t2_.totalTime) AS sclr_7 FROM Credential c3_ INNER JOIN Tracking t2_ ON (t2_.objectIdentity_id = s1_.id AND t2_.user_id = u0_.id) AND t2_.dtype IN ('coursetracking') AND ((t2_.deleted IS NULL OR t2_.deleted > '2016-04-26 08:33:31')), SecurableIdentity s1_, AccreditableInheritance a4_, AccreditableIdentity a5_, User u0_ WHERE (((s1_.id = c3_.securable_id AND a4_.parent_id = c3_.accreditable_id AND a5_.id = a4_.child_id AND u0_.id = a5_.user_id) AND (c3_.action = ? AND s1_.course_id IN (?, ?, ?))) AND ((u0_.deleted IS NULL OR u0_.deleted > '2016-04-26 08:33:31'))) AND (s1_.dtype IN ('securablecourse') AND a5_.dtype IN ('accreditableuser')) GROUP BY u0_.id' with params [\"study\", \"46\", \"45\", \"160\"]:\n\nSQLSTATE[42S22]: Column not found: 1054 Unknown column 's1_.id' in 'on clause'

This is just a hint how to achieve it. I cannot give you the correct answer as you don't give enough details. But this will help you to achieve what you need.
$q->select(u, t, co, ce);
$q->from('User', 'u');
$q->leftJoin('u.tracking', 't');
$q->leftJoin('t.course', 'co');
$q->leftJoin('u.credential', 'ce');

Related

Codeigniter's Model with QueryBuilder JOIN query

Really new to working with CI4's Model and struggling to adapt my existing MySQL JOIN queries to work with the examples in its User Guide.
I have adapted part of my code like so:
public function brand_name($brand_name_slug)
{
return $this->asArray()
->where('availability', 'in stock')
->where('sku !=', '')
->where('brand_name_slug', $brand_name_slug)
->groupBy('gtin')
->orderBy('brand_name, subbrand_name, product, size, unit')
->findAll();
}
It works fine. I have looked at examples, and figured out I can add the code ->table('shop a') and it still works, but I also need to to add the following JOIN statement:
JOIN (SELECT gtin, MIN(sale_price) AS sale_price FROM shop GROUP BY gtin) AS b ON a.gtin = b.gtin AND a.sale_price = b.sale_price
As soon as I add ->join('shop b', 'a.gtin = b.gtin and a.sale_price = b.sale_price') I get a '404 - File Not Found' error.
When I look at all examples of CI4 joins and adapt my code to fit, my foreach($shop as $row) loop generates a 'Whoops...' error because they end with a getResult() or getResultArray - instead of findAll().
Which is the way forward, and do I need to change my foreach loop.
Full MySQL statement:
SELECT * FROM shop a JOIN (SELECT gtin, MIN(sale_price) AS sale_price FROM shop GROUP BY gtin) AS b ON a.gtin = b.gtin AND a.sale_price = b.sale_price WHERE availability = 'in stock' AND sku != '' AND brand_name_slug = $brand_name_slug GROUP BY gtin ORDER BY brand_name, subbrand_name, product, size
Query builders have their limits. That's why the query method exists. If you have a complex query I'd advise you to just use $this->query();.
It will make you lose less time and effort converting something you know already works. And in the top of that, while converting complex queries you usually end up using the query builder but with big part of your SQL in it.
In your model extending CodeIgniter\Model :
$query = $this->db->query("SELECT * FROM shop a JOIN (SELECT gtin, MIN(sale_price) AS sale_price FROM shop GROUP BY gtin) AS b ON a.gtin = b.gtin AND a.sale_price = b.sale_price WHERE availability = 'in stock' AND sku != '' AND brand_name_slug = \$brand_name_slug GROUP BY gtin ORDER BY brand_name, subbrand_name, product, size");
// your array result
$result_array = $query->getResultArray();
// your object result
$result_object = $query->getResult();
BaseBuilder Class in Codeigniter expects the first join parameter to be the table name. So try passing the table name and join it on the table name itself. I haven't personally used the table aliases so I might also be wrong.
Following are the parameter that the JOIN query expects :
public function join(string $table, string $cond, string $type = '', bool $escape = null)
Here, it expects the first name be a table, so try out by switching aliases for the table's name directly.
For your second part of query, It would be better if you could show the whole error rather than just posting the first of the error.
Managed to figure it out in the end:
public function brand_name($brand_name_slug)
{
return $this
->db
->table('shop a')
->select()
->join('(SELECT sku, MIN(sale_price) AS sale_price FROM shop GROUP BY sku) AS b', 'a.sku = b.sku AND a.sale_price = b.sale_price')
->where('availability', 'in stock')
->where('a.sku !=', '')
->where('brand_name_slug', $brand_name_slug)
->groupBy('a.sku')
->orderBy('brand_name, subbrand_name, product, size, unit')
->get()
->getResult();
}
Thanks for all your pointers!

MySQL query not working, syntax error near INNER JOIN

Not done any programming for a few years so am somewhat rusty again.
I have two tables users table and users_profiles table. I want to update users_profiles table on the condition that user_uid matches on both tables (users.user_uid and users_profiles.user_uid) and where users.user_login = 'johndoe' and users.user_uid = '11'.
It will be executed in a php script, so johndoe would actually be users username (whatever is stored in session and same for users_uid. For simplicitity i added dummy data.
I run the query in phpmyadmin and get syntax error near INNER JOIN. I just cannot figure out what i'm doing wrong (probably wrote it entirely wrong) and have spent few hours trying to work it out without success.
Heres my sql query.
UPDATE
users_profiles
SET
users_profiles.user_fname = 'John',
users_profiles.user_lname = 'Doe',
users_profiles.user_gender = 'male'
INNER JOIN
users
ON
users.user_uid = users_profiles.user_uid
WHERE
users.user_login = 'johndoe'
AND
users.user_uid = '11'
error i get when running sql query via phpmyadmin.
#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 'ON users.user_uid, .user_uid FROM users_profiles WHERE
users.user_login ' at line 7
Thanks.
Try this:
UPDATE
users_profiles
INNER JOIN
users
ON
users.user_uid = users_profiles.user_uid
SET
users_profiles.user_fname = 'John',
users_profiles.user_lname = 'Doe',
users_profiles.user_gender = 'male'
WHERE
users.user_login = 'johndoe'
AND
users.user_uid = '11'
Try to exchange SET and INNER JOIN as shown, for example, in this SO answer:
UPDATE
users_profiles p
INNER JOIN
users u
ON
u.user_uid = p.user_uid
SET
p.user_fname = 'John',
p.user_lname = 'Doe',
p.user_gender = 'male'
WHERE
u.user_login = 'johndoe'
AND
u.user_uid = 11

converting this query to yii CDbCriteria

I'm pretty new to yii, and still trying to figure out some things. How do i change this mysql query to Yii's query using CDbCriteria?
SELECT DISTINCT p.prefix, p.state
FROM `store` as d
JOIN (`zip` as p)
ON (d.zip = p.zip)
WHERE d.store_code='".(int)$storeCode."'
GROUP BY p.prefix, p.state
ORDER BY p.state ASC
This is what i've done so far
$criteria = new CDbCriteria();
$criteria->select = array('prefix','state');
$criteria->join="zip";
$criteria->condition = 'store_code=:store_code';
$criteria->params = array(':store_code'=> (int)$storeCode);
$criteria->order = 'state ASC';
$query = AB::model()->findAll($criteria); //query
but it gives me this error
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 'zip WHERE store_code=64 ORDER BY state ASC' at line 1. The SQL statement executed was: SELECT prefix, state FROM `store` `t` zip WHERE store_code=:store_code ORDER BY state ASC. Bound with :store_code=64
BONUS: when i use the 1st query with the command below. it takes 2.2 seconds to return a result. any idea why its so slow? or how to speed it up? (the store table has 2518 rows and state has 20 rows)
$query = Yii::app()->db->createCommand($sql)->queryAll();
To answer your immediate question, I think the join syntax is off. From the docs:
how to join with other tables. This refers to the JOIN clause in an SQL statement. For example, 'LEFT JOIN users ON users.id=authorID'.
So in our case your command should run by doing something like
$criteria = new CDbCriteria();
$criteria->select = array('prefix','state');
$criteria->join="LEFT JOIN zip ON t.zip = zip.zip";
$criteria->condition = 'store_code=:store_code';
$criteria->params = array(':store_code'=> (int)$storeCode);
$criteria->order = 'state ASC';
$query = AB::model()->findAll($criteria); //query
t is what Yii aliases your base table with (whatever AB is representing. store?).
You can also use the Yii query builder to accomplish this task. For example
$data = Yii::app()->db->createCommand()
->selectDistinct(array("prefix","store"))
->from('store d')
->leftJoin('zip p', 'd.zip=p.zip')
->where('store_code=:store_code', array(':store_code'=>(int)$storeCode))
->queryAll();
BUT, I'd highly recommend looking more into relationships of models. Yii's active record is pretty sweet. If you build relationships, all you need to do is find the models you want with your AB::models->findAllByAttributes(array("store_code"=>$myCode));, and do $abModel->zip->column_name
You can also do something called 'eager loading'. Depending on how many times you plan on querying the relationship, (in a for loop, or just once) you may want to eager load to boost performance. E.g AB::model()->with("zip")->findAll($criteria);
You don't have to, and it's not always best practice to eager load.
If you build relationships, all you need to do is something like
$abModels = AB::models->with('zip')->findAllByAttributes(array("store_code"=>$myCode));
foreach ($abModels as $abModel) {
$state = $abModel->zip->state;
$prefix = $abModel->zip->prefix;
}
Active Record relationships
Active Record
Query Builder
Cheers
Try this:
$criteria = new CDbCriteria();
$criteria->alias = 'd';
$criteria->select = array('p.prefix','p.state');
$criteria->join='JOIN zip d ON d.zip = p.zip';
$criteria->condition = 'd.store_code=:store_code';
$criteria->params = array(':store_code'=> (int)$storeCode);
$criteria->group='p.prefix, p.state';
$criteria->order = 'p.state ASC';

Adding *1 to addAttributeToSort not working

I'm working within a product collection returning products and trying to order them. The problem is one of my product attributes (I find this out at 90% of the way through my project) is a quantity, i.e. 250, 5000 etc. However, I've just found out that despite these being numbers Magento treats them as strings, so therefore the collection returns the following quantities in this example:
50,100,250,500,1000,2000,5000
However, addAttributeToSort('quantity','ASC'); does this:
100,1000,2000,250,50,500,5000
I've done a var_dump() on the collection and ascertained that the values are being treated as strings, hence why this is probably happening. Unfortunately I've got over 6000 products with a lot of custom implementations and configurable products depending on this attribute, so am reluctant to change it. Searching on here I found that adding ORDER BY 'quantity' *1 does actually perform the sort correctly, however I can't seem to implement this clause in the standard addAttributeToSort function.
If anyone could help me implement this, I've tried addAttributeToSort('quantity','*1'); but that doesn't work, just errors.
Many thanks
UPDATE:
Here's the syntax for the query which is generated from the following code:
$collection = $this->getUsedProductCollection($product)
->addAttributeToSelect('*')
->addFieldToFilter('name', array( 'like' => '%' . $stock . '%' ));
$collection->getSelect()->order(new Zend_Db_Expr('quantity' *1));
count($collection);
'SELECT 'e'.*, 'link_table'.'parent_id', IF(at_name.value_id > 0, at_name.value, at_name_default.value) AS 'name', 'price_index'.'price', 'price_index'.'tax_class_id', 'price_index'.'final_price', IF(price_index.tier_price IS NOT NULL, LEAST(price_index.min_price, price_index.tier_price), price_index.min_price) AS 'minimal_price', 'price_index'.'min_price', 'price_index'.'max_price', 'price_index'.'tier_price' FROM 'catalog_product_entity' AS 'e' INNER JOIN 'catalog_product_super_link' AS 'link_table' ON link_table.product_id = e.entity_id INNER JOIN 'catalog_product_website' AS 'product_website' ON product_website.product_id = e.entity_id AND product_website.website_id = '1' INNER JOIN 'catalog_product_entity_varchar' AS 'at_name_default' ON ('at_name_default'.'entity_id' = 'e'.'entity_id') AND ('at_name_default'.'attribute_id' = '65') AND 'at_name_default'.'store_id' = 0 LEFT JOIN 'catalog_product_entity_varchar' AS 'at_name' ON ('at_name'.'entity_id' = 'e'.'entity_id') AND ('at_name'.'attribute_id' = '65') AND ('at_name'.'store_id' = 1) INNER JOIN 'catalog_product_index_price' AS 'price_index' ON price_index.entity_id = e.entity_id AND price_index.website_id = '1' AND price_index.customer_group_id = 0 WHERE (link_table.parent_id = 3781) AND (IF(at_name.value_id > 0, at_name.value, at_name_default.value) LIKE '%PCL Labels%')'
try
$collection->getSelect()->order(new Zend_Db_Expr('quantity' *1));
In the end I achieved this in PHP via ksort(). The database model when implementing any Zend functions was being overridden somewhere and I couldn't afford the time to figure it out.

Django ORM Creates Phantom Alias in SQL Join

I am executing the following code (names changed to protect the innocent, so the model structure might seem weird):
memberships =
models.Membership.objects.filter(
degree__gt=0.0,
group=request.user.get_profile().group
)
exclude_count =
memberships.filter(
member__officerships__profile=request.user.get_profile()
).count()
if exclude_officers_with_profile:
memberships = memberships.exclude(
member__officerships__profile=request.user.get_profile()
)
total_count = memberships.count()
which at this point results in:
OperationalError at /
(1054, "Unknown column 'U1.id' in 'on clause'")
The SQL generated is:
SELECT
COUNT(*)
FROM
`membership`
WHERE (
`membership`.`group_id` = 2 AND
`membership`.`level` > 0.0 AND
NOT (
`membership`.`member_id`
IN (
SELECT
U2.`member_id`
FROM
`membership` U0 INNER JOIN `officers` U2
ON (U1.`id` = U2.`member_id`)
WHERE U2.`profile_id` = 2
)
)
)
It appears that the SQL Join's ON statement is referencing an alias that hasn't been defined. Any ideas why!? I dropped my MySQL database and re-synced the tables from my models to try and ensure that there weren't any inconsistencies there.
The structure of the models I'm using are:
class Membership(models.Model):
member = models.ForeignKey(Member, related_name='memberships')
group = models.ForeignKey(Group, related_name='memberships')
level = models.FloatField(default=0.0)
class Member(models.Model):
name = models.CharField(max_length=32)
class Officer(models.Model):
member = models.ForeignKey(Member, related_name='officerships')
profile = models.ForeignKey(UserProfile)
class UserProfile(models.Model)
group = models.ForeignKey(Group)
class Group(models.Model)
pass
I think this may be a symptom of:
http://code.djangoproject.com/ticket/9188
which was fixed as of django revision 9589, I think. Now how to figure out which revision I'm working from...
Confirmed. When I implemented the change referenced in the ticket above:
http://code.djangoproject.com/changeset/9589
my error went away.