I'm trying to select data from two tables and show that data in a paginated grid view. The problem is that Yii is joining the data with a SQL join (because I told it to do it) and because of that it's not showing the correct number of items per page.
To make in clear, I'm selecting from topics and messages_in_topics, and I'm joining then with
$criteria->together = true;
This makes MySQL to return a row for each message (and the related topics). Example:
id_topic topic id_messages message
1 topic1 1 message1_in_topic1
1 topic1 2 message2_in_topic1
1 topic1 3 message3_in_topic1
2 topic2 4 message1_in_topic2
2 topic2 5 message2_in_topic2
There are only 2 topics, but Yii's paginator thinks there are 5.
The fastest way to "fix" this is grouping by the id_topic field, anyways, I can't do that because there's a where condition which searches with the like statement.
Thank you
EDIT:
Here's my action code:
$criteria = new CDbCriteria;
$get_s = Yii::app()->request->getQuery('s', '');
if( $get_s ){
$criteria->compare("topic_title", $get_s, true);
$criteria->compare("message_text", $get_s, true, 'OR');
}
$criteria->with = array('messages');
$criteria->addCondition(array( ...... )); <--- some rules like if the topic is validated...
$dataProvider = new CActiveDataProvider('Topics', array(
'criteria'=>$criteria,
'pagination=>array('pageSize'=>15)
));
Actually, what happens there is that the information being displayed is in regard to your messages. The repeated topic values are because these topics are the corresponding related data to the message.
You could try to tell your query to use GROUP BY in the results...
SELECT t.id_topic, t.topic, COUNT(m.id_messages)
FROM topics t LEFT JOIN messages m ON t.id_topic = m.id_topic
GROUP BY t.id_topic
This way, more or less, you can display the count of messages-per-topic.
I could help you more if you'd show us your SQL.
EDIT: After seeing your code, here is my guess:
$criteria = new CDbCriteria;
$get_s = Yii::app()->request->getQuery('s', '');
if( $get_s ){
$criteria->compare("topic_title", $get_s, true);
$criteria->compare("message_text", $get_s, true, 'OR');
}
$criteria->with = array('messages');
$criteria->addCondition(array( ...... )); <--- some rules like if the topic is validated...
$dataProvider = new CActiveDataProvider('Topics', array(
'criteria'=>$criteria,
'pagination=>array('pageSize'=>15)
));
You can make sure the query isn't complicated by yii's formatting by pouring it using only three basic properties of CDbCriteria:
$criteria->select is exactly the list of columns you want to select.
$criteria->condition is exactly the WHERE condition, I honestly prefer using a string to an array, since using the string allows me to use the exact condition I put in here.
$criteria->join is attached immediately after the $model->tableName() you specify in the CActiveDataProvider constructor.
$criteria->group is added at the end of the query, you just need to specify the grouping column.
Also, You can also make sure to set these properties directly, so you actually descompose your query into the CDbCriteria object. For instance:
SELECT t.id_topic, t.topic, COUNT(m.id_messages)
FROM topics t LEFT JOIN messages m ON t.id_topic = m.id_topic
GROUP BY t.id_topic
would be like this
/*1*/ $criteria->select = 't.id_topic, t.topic, COUNT(m.id_messages)';
/*2*/ $criteria->condition = 't.topic_title LIKE %'.$get_s.'% OR ...';/* add your conditions here*/
/*3*/ $criteria->join = 'LEFT JOIN messages m ON t.id_topic = m.id_topic'; //Full join statement here, including the nature of the join.
/*4*/ $criteria->group = 't.id_topic';
IMPORTANT: take into account that since you pass your Topics classname to the CActiveDataProvider constructor, the table under Topics will be known as t. Any other tables must be specified as well (Pretty much like messages m or messages AS m)in the join condition otherwise you might get a column xxxx is ambiguous warning.
Don't pass out the chance of giving an eye to CDbCriteria and CActiveDataProvider for any questions you might have.
The problem isn't the paginator, it is you query. If you run the query:
SELECT *
FROM topics as t
INNER JOIN messages_in_topics as mt
ON mt.topics_id = t.id
INNER JOIN messages as m
ON m.id = mt.messages_id
You will get 5 results, as show in your example above.
So more importantly, what are you trying to do?
Also, your table shows a MANY_MANY relationship (message1 has 2 topics, topic1 has 3 messages), is your database set up the right way, and are your model relations configured accordingly?
Are you trying to show ALL messages in each topic on a single line?
Are you trying to show ALL topics and list each message with that topic?
Assuming you have relations set up correctly, you can just use: $messages=$topics->messages; and get an array with all the messages listed.
Conversely, you can do $topics=$messages->topics; to get the topics.
Related
I am starting to get headaches over this so I thought I just post it here.
I have two tables that are related through a pivot table (as it is a many-to-many relationship). I use Laravel and Eloquent (but general help on how to achieve this with normal SQL queries is also highly appreciated).
I want to order the first table based a column of the second one but the column needs to be "aggregated" for this.
Example with Cars that are shared by many drivers and can have different colors:
Car-Table: [id, color]
Driver-Table: [id, name]
Car.Driver-Table: [car_id, driver_id]
I need a query that gets all drivers that only drive red cars and then all that don't drive red cars.
I have to use a query because I'll maybe do other things (like filtering) on this query afterwards and want to paginate in the end.
I already use queries that get either one of the two groups. They look like this:
In the Driver model:
public function redCars() {
return $this->cars()->where('color', 'red');
}
public function otherColoredCars() {
return $this->cars()->where('color', '<>', 'red');
}
And then in somewhere in a controller:
$driversWithOnlyRedCars = Driver::whereDoesntHave('otherColoredCars')->get();
$driversWithoutRedCars = Driver::whereDoesntHave('redCars')->get();
Is there a way to combine these two?
Maybe I am just thinking completely wrong here.
Update for clarification:
Basically I would need something like this (ot any other way that would lead to the same outcome)
$driversWithOnlyRedCars->addTemporaryColumn('order_column', 0); // Create temporary column with value 0
$driversWithoutRedCars->addTemporaryColumn('order_column', 1);
$combinedQuery = $driversWithOnlyRedCars->combineWith($driversWithoutRedCars); // Somehow combine both queries
$orderedQuery = $combinedQuery->orderBy('order_colum');
$results = $combinedQuery->get();
Update 2
I think, I found out how to get near my goal with raw queries.
Would be something like this:
$a = DB::table(DB::raw("(
SELECT id, 0 as ordering
FROM drivers
WHERE EXISTS (
SELECT * FROM cars
LEFT JOIN driver_car ON car.id = driver_car.car_id
WHERE driver.id = driver_car.driver_id
AND cars.color = 'red'
)
) as only_red_cars"));
$b = DB::table(DB::raw("(
SELECT id, 1 as ordering
FROM drivers
WHERE EXISTS (
SELECT * FROM cars
LEFT JOIN driver_car ON car.id = driver_car.car_id
WHERE driver.id = driver_car.driver_id
AND cars.color <> 'red'
)
) as no_red_cars"));
$orderedQuery = $a->union($b)->orderBy('ordering');
Now the problem is that I need the models ordered like this and paginated in the end so this is not really an answer to my question. I tried to convert this back to models but I didn't succeed yet. What I tried:
$queriedIds = array_column($orderedQuery->get()->toArray(), 'id');
$orderedModels = Driver::orderByRaw('(FIND_IN_SET(drivers.id, "' . implode(',', $queriedIds) . '"))');
But looks like FIND_IN_SET only allows for a column of the table as second parameter. Is there another way to get the Models in the right order out of the ordered union query?
You can use a UNION query:
$driversWithOnlyRedCars = Driver::select('*', DB::raw('0 as ordering'))
->whereDoesntHave('otherColoredCars');
$driversWithoutRedCars = Driver::select('*', DB::raw('1 as ordering'))
->whereDoesntHave('redCars');
$drivers = $driversWithOnlyRedCars->union($driversWithoutRedCars)
->orderBy('ordering')
->orderBy('') // TODO
->paginate();
How do you want drivers with the same ordering to be ordered? You should add a second ORDER BY clause to get a consistent order every time you execute the query.
This is the best I got:
$driversWithOnlyRedCars = Driver::whereHas('cars',function($q){
$q->where('color', 'red');
})->withCount('cars')->get()->where('cars_count',1);
I have a table called Listings and a table called wines and one more called wineinfo
I originally was using the following to get the info from the listings table only, how ever since I restructured my DB, it requires the use of two other tables.
$listing = $this->db->get_where( "listings", [ "listingID" => $id ]
)->row();
if( !$listing )
throw new Exception("Error: Listing you're trying to bid on does not exist.", 1);
// check not to bid on own listings
if( $listing->list_uID == $userid )
throw new Exception("Error: Dont't bid on your own listings.", 1);
I then tried changing the code so the JOIN statements could work
$this->db->select("FROM listings.*, Vintage, Vineyard, Wine_Name, Region, Advice, Grape,Producer,Type id,wine_id,Wine_Name,");
$this->db->from("wineinfo");
$this->db->where(["listingsID" => $id]);
$this->db->where(["wineinfo.wine_id" => "listings.wine_id"]);
$this->db->where(["wineinfo.Vintage" => "listings.wine_id"]);
$this->db->join("wines", "wineinfo.wine_id = wines.wine_id");
$listing = $this->db->get()->row();
I am being given this error.
Unknown table 'listings'
But there is 100% a table called listings.
I know I've missed something, or definitely messed up the code, I am only just learning about this and the code above worked for something else, but now I've amended it to this, it hasn't.
I then tried changing the code so the JOIN statements could work
you are trying to combine 3 tables with 2 FROM and one JOIN clauses, which is incorrect the way you do it.
you need to keep SELECT clean, just select the columns you need, like:
$this->db->select("listings.*, wineinfo.*, wine.*");
then the FROM clause:
$this->db->from("wineinfo");
then make the joins:
$this->db->join("listings", "wineinfo.wine_id = listings.listingsID");
$this->db->join("wines", "wineinfo.wine_id = wines.wine_id");
followed by your where clauses.
please note, I don't know your table structure, so I can only guess your JOIN relationships. Also this is a simplified example, where I suppose that the 3 tables don't have matching column names.
response to "ambiguous" comment: you can limit your select clause to just necessary columns, e.g.
$this->db->select("listings.*, wineinfo.vintage, wine.*");
or use an alias:
$this->db->select("listings.*, wineinfo.wine_id as my_wineID, wine.*");
The FROM in $this->db->select("FROM listings.*, Vintage, Vineyard, Wine_Name, Region, Advice, Grape,Producer,Type id,wine_id,Wine_Name,"); is not supposed to be there.
The generated out query now looks like:
SELECT FROM *... FROM sometable
Setup:
Codeigniter 3 running on a variant of PHP 5 server. Using the Query Builder to deal with the db.
Background:
Creating a software that has a client profile. Users can add multiple notes to the profile, and link a contact to that note. The profile then displays all the notes and the contact on the corresponding profile, using the join() method from Codeigniter 3's Query Builder.
Issue:
A note can be added without a client contact, in the case of a generalised note. This sets the default value of NULL in the DB which in turn prevents the client_model from returning the notes, because it cant join the tables.
Current code:
function get_client_notes($client_id)
{
$this->db->join('nh_note_types', 'nh_note_types.type_id = nh_client_notes.client_notes_type');
$this->db->join('nh_user_profiles', 'nh_user_profiles.user_profile_user_id = nh_client_notes.client_notes_added_by');
$this->db->join('nh_client_contacts', 'nh_client_contacts.client_contact_id = nh_client_notes.client_notes_client_contact_id');
$this->db->order_by("client_notes_added_date", "desc");
$query = $this->db->get_where('nh_client_notes', array('client_notes_client_id' => $client_id));
return $query;
}
Currently if the value for client_notes_client_contact_id is NULL it will not return any data for that row.
What I am trying to find out is if there is a way to do the following:
IF the client_notes_client_contact_id is not null then join the tables, else carry on past.
Or any other way that would join the tables if there is a value and where it is NULL, then don't join.
Any help is appreciated!
Your current MySQL query with the above Query builder would 'build' like this:
SELECT
*
FROM
nh_client_notes
JOIN nh_note_types ON
( nh_note_types.type_id = nh_client_notes.client_notes_type )
JOIN nh_user_profiles ON
( nh_user_profiles.user_profile_user_id = nh_client_notes.client_notes_added_by )
JOIN nh_client_contacts ON
( nh_client_contacts.client_contact_id
=
nh_client_notes.client_notes_client_contact_id
)
WHERE
client_notes_client_id = 479
ORDER BY
client_notes_added_date DESC
However, this will require all the JOINs to have an matching ID Available. Thus the correct MySQL would be LEFT JOIN on client_notes_client_contact_id key, which is as you've requested to be conditional.
In this instance, adjust your Query builder to have a third parameter on the join() to make this 'left'.
<?php
$this->db->join(
'nh_client_contacts',
'nh_client_contacts.client_contact_id = nh_client_notes.client_notes_client_contact_id',
'left'
);
?>
In doing so, this'll correct your query to bring back client_notes regardless of client_notes_client_contact_id.
How to get all the posts where current user made a comment. I have a relationships between tables, but the situation is difficult to me. It should be something like this:
$posts = Yii::app()->user->comments->posts->findAll(); // don't think that is my code, it just for explanation of query chain
so I need to get all posts where user leaved a comment.
In the sql my query works fine:
SELECT tc.title, tc.content, t.post_id
FROM tbl_comment t
JOIN tbl_post tc
ON t.post_id =tc.id
WHERE author_id =43
GROUP BY t.post_id
$CD = new CDbCriteria;
$CD->condition = 'tc.author_id='.Yii::app()->user->id;
$CD->join = 'JOIN tbl_comment tc ON t.id=tc.post_id';
$posts = Post::model()->findAll($CD);
This is it.
Rather than defining your IN clause manually, you can use addInCondition(), i.e.:
$criteria=new CDbCriteria;
$criteria->addInCondition('id', $postIds)
$posts=Post::model()->findAll($criteria);
You can view the source from the Yii documentation page, and you'll see that the code there splits up the parameters and then joins them, similiar to what #Wilq is suggesting.
If you need a string for that parameter just do an implode() on your array.
Try:
$criteria->params = array(':id' => implode(',',$postIds));
Reading your code makes me think $postIds is an array of IDs. Is that right ?
If so, you can't assign $postIds to the :id query parameter, you need to do several queries whith a single posted id each time.
I have an issue getting data from three tables, which I want to return using one query.
I've done this before using a query something like this one:
$query = mysql_query("SELECT
maintable.`id`,
maintable.`somedata`,
maintable.`subtable1_id`,
subtable1.`somedata`,
subtable1.`subtable2_id`,
subtable2.`somedata`
FROM
`maintable` maintable,
`subtable1` subtable1,
`subtable2` subtable2
WHERE
maintable.`somedata` = 'some_search' AND
subtable1.`id` = maintable.`subtable1_id` AND
subtable2.`id` = subtable1.`subtable2_id`
")or die(mysql_error());
The problem this time is that the extra details might not actually apply. I need to return all results that match some_search in maintable, even if there is no subtable1_id specified.
I need something that will go along the lines of
WHERE
maintable.`somedata` = 'some_search'
AND IF maintable.`subtable1_id` IS NOT NULL (
WHERE subtable1.`id` = maintable.`subtable1_id` AND
subtable2.`id` = subtable1.`subtable2_id`
)
As you will probably guess, I am not an advanced mysql user! Try as I might, I cannot get the syntax right, and I have had no luck searching for solutions on the web.
Any help much appreciated!
It seems like the basic distinction you're looking for is between an INNER JOIN and a LEFT JOIN in MySQL.
An INNER JOIN will require a reference between the two tables. There must be a match on both sides for the row to be returned.
A LEFT JOIN will return matches in both rows, like an INNER, but it will also returns rows from your primary table even if no rows match in your secondary tables -- their fields will be NULL.
You can find example syntax in the docs.
If I got this right, you need to use MySQL LEFT JOIN. Try this:
SELECT
m.id,
m.somedata,
m.subtable1_id,
s1.somedata,
s1.subtable2_id,
s2.somedata
FROM
maintable m
LEFT JOIN
subtable1 s1
ON
m.subtable1_id = s1.id
LEFT JOIN
subtable2 s2
ON
s1.subtable2_id = s2.id
WHERE
m.somedata = 'some search'
This will return the data of maintable even if there's no equivalent data in subtable1 or 2 OR data of maintable and subtable1 if there's no equivalent data in subtable2.
How about this:
WHERE
maintable.`somedata` = 'some_search'
AND (maintable.`subtable1_id` IS NULL OR (
subtable1.`id` = maintable.`subtable1_id` AND
subtable2.`id` = subtable1.`subtable2_id` )
)
Keep in mind that this will result in a cross product of subtable1 and subtable2 with maintable when subtable1_id is NULL.