Yii Framework 2.0 Active Record - php

I am working with Yii framework 2.0, I have one database table that looks like the following.
id active key
1 0 xx
2 1 xx
3 0 zzz
4 0 wwww
5 1 wwww
6 1 qqqqq
I would like to get the record where 'active' is 1 and 'key' is the same as 'key' of the record where 'active' is 0. The result that I want is
id active key
2 1 xx
5 1 wwww
It might not be easy to understand the question, so I put following code sample to support the question. This codes gives me the result I want.
$allModel = Model::find()->where(['active' => 0])->all();
$arrModelActive1 = [];
foreach($allModel as $model) {
$modelActive1 = Model::find()->where['active' => 1, 'key' => $model->key]->all();
$arrModelActive1[] = $modelActive1;
}
return $arrModelActive1;
I could approach this problem with the above code sample, but the problem is that I execute the query inside of the foreach loop which might decrease the performance. So I am looking for a join or an eager loading solution which Yii 2.0 might provide which could operate with the same table or so-called self-join operation.

Example:
You have relation as public function getTest()
{
return $this->hasMany(self::className(), ['key' => 'key'])
->where('active = :active', [':active' => 1]);
}
and after that you must select items with active field == 0
$model = Model::find(['active' => 0])->all(); var_dump($model->test);

Related

CakePHP : Get Table with many conditions

I have a very specific problem. Even with the great CakePHP doc, I still don't know how to fix my pb.
I'm currently web developping using the CakePHP framework. Here is my situation :
I have a Table "TableA" which contains parameters "name", "type"(1 to 6) and "state"(OK and NOT OK) . What I want is getting all the Table lines which are type 5 OR 6 and which have not a same name line with "state" OK.
There are different lines of the table which have the same "name". I'm interesting to the lines from the same name where there is no OK state.
For example, there are :
name : example1 state : NOT OK
name : example1 state : NOT OK
name : example1 state : NOT OK
And there is no example1 with the state OK and this is this kind of line I want to get.
I would like to do this with the cakePHP syntax, with conditions in the TableRegistry::get function.
Thanks for helping. Waiting for your return.
PS:
What I achieved now is not the best solution :
$tablea_NOTOK = TableRegistry::get("TableA")->find('all', array(
'conditions' => array(
'OR' => array(
array('TableA.type' => 5),
array('TableA.type' => 6),
),
'Etudes.state' => 'NOT OK'
)
));
$this->set(compact('tablea_NOTOK'));
$tablea_OK = TableRegistry::get("TableA")->find('all', array(
'conditions' => array(
'OR' => array(
array('TableA.type' => 5),
array('TableA.type' => 6),
),
'Etudes.state' => 'OK'
)
));
$this->set(compact('tablea_OK'));
And then in my view, i compared each line of the tablea_OK with the tablea_NOTOK. But there is a lot of data so the code is not perfect and slow
You may consider creating a view table in your database which holds the combination of data needed. Since the data will all be from a single table, you wouldn't need to loop through the data and compare it.
I don't know all your table relationships, but I made a simple table with these fields and data:
id name type state
1 Harry 5 OK
2 Harry 6 NOT OKAY
3 Harry 6 NOT OKAY
4 John 5 NOT OKAY
Then I wrote a query which would group by name and count the state values:
SELECT `name`, `type`, `state`,
(SELECT COUNT(state) FROM TableA as TableA1 WHERE `state` = 'OK' AND TableA.name = TableA1.name) as okay_count,
(SELECT COUNT(state) FROM TableA as TableA2 WHERE `state` = 'NOT OKAY' AND TableA.name = TableA2.name) as not_okay_count
FROM TableA
GROUP BY name;
The results look like this:
name type state okay_count not_okay_count
Harry 5 OK 1 2
John 5 NOT OKAY 0 1
You can adjust the query as needed and create your database view table and then call that in CakePHP.
$my_view_table = TableRegistry::get("MyViewTable")->find('all');
You can learn more about MySQL view tables here

Getting all items on Amazon Product API

Amazon's Product API limits us to get only 10 items per page, and only 10 pages at a certain query.
I have developed a code that would almost get all items;
first, I have supplied a params that looks like this:
$item_params = [
"Service" => "AWSECommerceService",
"Operation" => "ItemSearch",
"AWSAccessKeyId" => env('AWS_ACCESS_KEY_ID'),
"AssociateTag" => env('AWS_ASSOCIATE_TAG_ID'),
"SearchIndex" => "HomeGarden",
"ResponseGroup" => "ItemAttributes,SalesRank,Offers",
"Sort" => "-price",
"BrowseNode" => $item_params['BrowseNode'],
"MaximumPrice" => $max_price,
"MinimumPrice" => "0"
];
then, the code will get all items under that browse node (category), SORTED BY PRICE (desc) also by specifying the MAX and MIN Price of the items to limit the search.
the pseudo-code (original code is too long)
function getProducts($item_params, $max_price = null){
$products = //request to amazon
foreach ($product as $key=>$value){
//add product to db
}
// if the total number of results on the query is not equal to zero, continue looping
if (!$products->totalResults() == 0){
$product = //get the first lowest priced item on the db
$this->getProducts($item_params, $product->price);
}
}
however I am experiencing this scenario :
Sample request output (assuming all items from amazon):
ASIN(unique id) | Price
1 | 201
2 | 194
3 | 195
.
.
n | 33
n+1 | 33
n+2 | 33
.
n+120 | 33
n+121 | 34
n+122 | 35
wherein the products from n to n+120 are equal. This will create an infinite loop to my getProducts function. How can I avoid this? Knowing that only 10 items are returned on each request and only 10 pages.
How can I avoid this?
I don't think you can with just using price. You have to divide your search into multiple sub-searches by using additional keywords. For example, if you're searching for "laptop", instead do searches on "laptop asus", "laptop dell", etc.
You can also filter on Browse node IDs, so if your results come from multiple browse nodes, you can do two or more searches.
Add the ItemPage parameter and increment it in a loop. You should be able to get up to 100 unique ASINs (10 pages of 10 products per page).
$page = 1;
while($page <= 10) {
$item_params = [
"Service" => "AWSECommerceService",
"Operation" => "ItemSearch",
"AWSAccessKeyId" => env('AWS_ACCESS_KEY_ID'),
"AssociateTag" => env('AWS_ASSOCIATE_TAG_ID'),
"SearchIndex" => "HomeGarden",
"ResponseGroup" => "ItemAttributes,SalesRank,Offers",
"Sort" => "-price",
"BrowseNode" => $item_params['BrowseNode'],
"MaximumPrice" => $max_price,
"MinimumPrice" => "0",
"ItemPage" => $page
];
// execute query and save data
//increment page number
$page++;
}

GroupBy Query Laravel 5.1

I'm having problem in fetching the data using groupBy, I don't where I'm wrong, I have done it many times before, but today I'm wrong some where and I don't know where. Following is the Table from which I want to select the Data:
Table Name: user_questions
id | user_id | message | read_status_user | read_status_support | answered
Now suppose if one user sends more than one messages, then user_id will be repeated, So to want all the message from one particular user I'm firing the query like following:
UserQuestion::groupBy('user_id')->get();
This should give me the result like
user_id = 1 > message1
user_id = 1 > message2
....
user_id = 1 > message...(if any)
user_id = 2 > message1
user_id = 2 > message2
.....
So on...
But this is always giving me only one message from the particular user. I don't know why. Is there any mistake? I have tried another queries too, but all are giving me the same result.
Please help me with this. Everybody's help will be highly appreciated. Thanks to all of you in advance.
The issue here is that you are calling the groupBy function of the query builder object, which is what generates the query for your database. When you call the ->get() method, the query is executed and a Collection object containing the results is returned. What you are looking to use is the groupBy method of Laravel's Collection class, which means you need to put the ->groupBy('user_id') after the ->get().
Assuming you have the following data:
user_question
user_id question_id
1 1
1 2
1 3
2 4
3 5
3 6
Your current code
UserQuestion::groupBy('user_id')->get();
executes this query
select * from user_question group by user_id;
returning one row per user, since that's what group by does in MySQL.
user_id question_id
1 1
2 4
3 5
If instead, you do the following
$collection = UserQuestion::get();
the query is simply
select * from user_question
and when you call $collection->groupBy('user_id') on this collection, you get data structured like
[
1 => [
[ 'user_id' => 1, 'question_id' => 1 ],
[ 'user_id' => 1, 'question_id' => 2 ],
[ 'user_id' => 1, 'question_id' => 3 ]
],
2 => [
[ 'user_id' => 2, 'question_id' => 4 ],
],
3 => [
[ 'user_id' => 3, 'question_id' => 5 ],
[ 'user_id' => 3, 'question_id' => 6 ]
]
]
Try like this
$users = DB::table('table_name')
->groupBy('user_id')
->get();
after that push that to foreach loop
foreach ($users as $user)
{
var_dump($user->name);
}
ordering-grouping-limit-and-offset in Laravel
You've probably found the solution to your problem by now but otherwise, I would suggest to use the relationships. In the User model, I would do:
public function questions()
{
return $this->hasMany('App\UserQuestion');
}
Then I would get all the users and loop through them to get their messages.
$users = User::all();
$users->each(function ($user) {
$questions = User::find($user->id)->questions;
});

Checking users friendship

Continuing this question,
in my web app, I want to allow users to add friends, like facebook, in my previous question, I finally decided to have the database structure as #yiding said:
I would de-normalize the relation such that it's symmetric. That is,
if 1 and 2 are friends, i'd have two rows (1,2) and (2,1).
The disadvantage is that it's twice the size, and you have to do 2
writes when forming and breaking friendships. The advantage is all
your read queries are simpler. This is probably a good trade-off
because most of the time you are reading instead of writing.
This has the added advantage that if you eventually outgrow one
database and decide to do user-sharding, you don't have to traverse
every other db shard to find out who a person's friends are.
So, now if user 1 adds user 2, and user 5 adds 2, something like this will go into the db:
ROW_ID USER_ID FRIEND_ID STATUS
1 1 2 0
2 2 1 0
3 5 2 0
4 2 5 0
As you see, we insert the row of the "REQUEST SENDER" first, so now imagine that user 5 is logged in, and we want to show him the friendship requests, here is my query:
$check_requests = mysql_query("SELECT * FROM friends_tbl WHERE FRIEND_ID = '5'");
the above query, will fetch ROW_ID = 4, this means with the above query shows us that user 2 has added 5, but he has NOT, actually the user 5 added user 2, so here we should not show any friendship requests for user 5, instead we need to show it for user 2.
How I'm supposed to check this correctly?
This is an edited answer.
Your SQL query should look like this:
SELECT USER_ID, FRIEND_ID FROM friends_tbl WHERE FRIEND_ID = '5' OR USER_ID = '5'
Then you have to parse your result in this way. Assuming you have got a php array like this:
$result = array(
0 => array(
'USER_ID' => 5,
'FRIEND_ID' => 2
),
1 => array(
'USER_ID' => 2,
'FRIEND_ID' => 5
)
2 => array(
'USER_ID' => 5,
'FRIEND_ID' => 8
),
3 => array(
'USER_ID' => 8,
'FRIEND_ID' => 5
)
)
You just have to get the even rows:
$result_final = array();
for($i = 0; $i < count($result); $i++) {
if($i % 2 == 0) $result_final[] = $result[$i];
}
Then you will have an array like this:
$result = array(
0 => array(
'USER_ID' => 5,
'FRIEND_ID' => 2
),
1 => array(
'USER_ID' => 5,
'FRIEND_ID' => 8
)
)
Alternative method: Make your SQL look like this:
SELECT FRIEND_ID FROM friends_tbl WHERE USER_ID = '5'
That's all.
Friendship query notifies should be placed in something like message inbox. Relation you described is meant to hold, well, friendship relations, not the fact of the event happening itself. You should consider create relation to hold notifies and fill it properly alongside with two inserts on friends_tbl
You'll need to hold a temporary table (or fixed - for data mining) which has all the requests made from one user to another, for example:
table: friendRequest
inviterId inviteeId status tstamp
2 5 0 NOW()
5 8 0 NOW()
assuming that 0 is unapproved.
Than you'll query for all pending requests
SELECT * FROM friendRequest WHERE invitee_id = :currentLoggedUserId AND status = 0
Once a user approved a user, you'll create a transaction, describing this newly formed relation and updating the friendRequests table
You could also query this way assymetric relations, where a user has many followers, by looking for un-mutual friendships.

CakePHP edit multiple records at once

I have a HABTM relationship between two tables: items and locations, using the table items_locations to join them.
items_locations also stores a bit more information. Here's the schema
items_locations(id, location_id, item_id, quantity)
I'm trying to build a page which shows all the items in one location and lets the user, through a datagrid style interface, edit multiple fields at once:
Location: Factory XYZ
___________________________
|___Item____|___Quantity___|
| Widget | 3 |
| Sprocket | 1 |
| Doohickey | 15 |
----------------------------
To help with this, I have a controller called InventoryController which has:
var $uses = array('Item', 'Location'); // should I add 'ItemsLocation' ?
How do I build a multidimensional form to edit this data?
Edit:
I'm trying to get my data to look like how Deceze described it below but I'm having problems again...
// inventory_controller.php
function edit($locationId) {
$this->data = $this->Item->ItemsLocation->find(
'all',
array(
"conditions" => array("location_id" => $locationId)
)
);
when I do that, $this->data comes out like this:
Array (
[0] => Array (
[ItemsLocation] => Array (
[id] => 16
[location_id] => 1
[item_id] => 1
[quantity] => 5
)
)
[1] => Array (
[ItemsLocation] => Array (/* .. etc .. */)
)
)
If you're not going to edit data in the Item model, it probably makes most sense to work only on the join model. As such, your form to edit the quantity of each item would look like this:
echo $form->create('ItemsLocation');
// foreach Item at Location:
echo $form->input('ItemsLocation.0.id'); // automatically hidden
echo $form->input('ItemsLocation.0.quantity');
Increase the counter (.0., .1., ...) for each record. What you should be receiving in your controllers $this->data should look like this:
array(
'ItemsLocation' => array(
0 => array(
'id' => 1,
'quantity' => 42
),
1 => array(
...
You can then simply save this like any other model record: $this->Item->ItemsLocation->saveAll($this->data). Adding an Item to a Location is not much different, you just leave off the id and let the user select the item_id.
array(
'location_id' => 42, // prepopulated by hidden field
'item_id' => 1 // user selected
'quantity' => 242
)
If you want to edit the data of the Item model and save it with a corresponding ItemsLocation record at the same time, dive into the Saving Related Model Data (HABTM) chapter. Be careful of this:
By default when saving a HasAndBelongsToMany relationship, Cake will delete all rows on the join table before saving new ones. For example if you have a Club that has 10 Children associated. You then update the Club with 2 children. The Club will only have 2 Children, not 12.
And:
3.7.6.5 hasAndBelongsToMany (HABTM)
unique: If true (default value) cake will first delete existing relationship records in the foreign keys table before inserting new ones, when updating a record. So existing associations need to be passed again when updating.
Re: Comments/Edit
I don't know off the top of my head if the FormHelper is intelligent enough to autofill Model.0.field fields from a [0][Model][field] structured array. If not, you could easily manipulate the results yourself:
foreach ($this->data as &$data) {
$data = $data['ItemsLocation'];
}
$this->data = array('ItemsLocation' => $this->data);
That would give you the right structure, but it's not very nice admittedly. If anybody has a more Cakey way to do it, I'm all ears. :)

Categories