CakePHP 3 Fetch associated data recursive - php

I am trying to fetch Data in a recursive-way (over associations) using CakePHP 3.1
In Cake 3 I can use the "contain" key to fetch the next level of asociated data. But I need to fetch one more level. Does anyone know how to do this? I read the docs but didn't found anything there, with google it's the same.
The 3 Levels are connected like this:
OperationalCostInvoice (belongsTo Object)
-> Object (hasMany OperationalCostTypes)
-> OperationalCostType
With OperationalCostInvoice->get($object_id, ['contain' => 'Object']) I can get the Object that is associated with the OperationalCostInvoice but I also want to fetch the OperationalCostTypes from the Object in (if possible) just one call.
I dont need tipps about association linking the reason that the entities are linked like this is I can easily implement a history function.
Thanks in advance!

I just meant one function call (on the Table object) to fetch everything. I know that more than one query is required.
Just create your own table method then and return all your results in one array or implement whatever you want and return it.
public function foo() {
return [
'one' => $this->find()...->all();
'two' => $this->Asssoc->find()...->all();
];
}
But in CakePHP 2 there was the option recursive which controlled on how many levels associated data is fetched.
The recursive was a pretty stupid thing in Cake2. First thing we've always done was to set it to -1 in the AppModel to avoid unnecessary data fetching. Using contain is always the better choice. I would stay away from using recursive at all, especially for deeper levels.
Also contain is still, as it was in Cake2 as well, able to fetch more than one level deep associations.
$this->find()->contain([
'FirstLevel' => [
'SecondLevel' => [
'ThirdLevel'
]
]
])->all();

Related

Laravel 9 how to best query JSON object in JSON column?

I've got a basic app up and running in the latest version of Laravel 9 that's utilising JSON columns for storing certain bits of data. I have a job_type_rates column on my Client model/table, where some have a value similar to:
[
{
"job_type": "8",
"pay_rate": "15.45",
"charge_rate": "18.45",
"awr_pay_rate": "21.33",
"awr_charge_rate": "26.77"
}
]
What I would like to do is select all clients that have a job_type of 8. I've tried to do Client::whereJsonContains('job_type_rates->job_type', "8")->get() but no results are returned, however that code would work if I didn't have an object in the column.
One way I can get around this is to create a pivot table and go down that route, but I was wondering if anyone had come up against this before and perhaps used a closure or similar?
Based on the comment by #RiggsFolly I tried this code:
Client::whereJsonContains('job_type_rates', ["job_type" => "8"])->get()
And it works, it returns the expected results. As far as I'm aware this isn't in the Laravel docs (which mostly show single value examples).
I think it's still better to extract this out into a pivot table or similar, but I hope this helps someone!

Active Record save or create command

$model = Person::findOne($person_id);
$model->status = $status;
$model->save();
or
Yii::$app->db->createCommand()->update('person',
['status'=>$status],
'person_id='.$person_id)
->execute();
In terms of performance, how this two snippet different from each other although the result is the same?
The firts is based on the fact the related active record is obtained by a preliminary select and then the change is performed by an update when the save() (and the related validation) method is invoked ..
The second don't perform a select for get the related activeRecord ..
and perform the update only
so the second should be more fast then the first ..
Using ActiveRecord (the first example) is generally more memory intensive (because of the object setup, teardown, validation etcetera).
The second one will be much faster but it does not validate the data. Most of the time you will want to work with a "small" set of ActiveRecords objects (that's why Yii2 has pagination in it's DataProviders). ActiveRecord is more powerful since you can traverse relations, use virtual attributes etc.
But for batch inserts the second one it is much better. And you can also use it like this to insert multiple rows in one query:
Yii::$app->db->createCommand()->batchInsert('tableName', ['id', 'title', 'created_at'], [
[1, 'title1', '2015-04-10'],
[2, 'title2', '2015-04-11'],
[3, 'title3', '2015-04-12'],
])->execute();

Swapping array values with Eloquent model IDs

I need to make an import method that takes the CSV file and imports everything in the database.
I've done the parsing with one of Laravel's CSV addons and it works perfectly giving me a big array of values set as:
[
'col1_name' => 'col1 value',
'col2_name' => 'col2 value',
'col3_name' => 'col3 value,
'...' => '...'
]
This is also perfect since all the column names fit my model which makes the database inserts a breeze.
However - a lot of column values are strings that i'd like to set as separate tables/relations. For example, one column contains the name of the item manufacturer, and i have the manufacturer table set in my database.
My question is - what's the easy way to go through the imported CSV and swap the strings with the corresponding ID from the relationship table, making it compatible with my database design?
Something that would make the imported line:
[
'manufacturer' => 'Dell',
]
into:
[
'manufacturer' => '32',
]
I know i could just do a foreach loop comparing the needed values with values from the relationship models but I'm sure there's an easier and more clean way of doing it.
I don't think theres any "nice" way to do this - you'll need to look up each value for "manufacturer" - the question is, how many queries will you run to do so?
A consideration you need to make here is how many rows you will be importing from your CSV file.
You have a couple of options.
1) Querying 1 by 1
I'm assuming you're going to be looping through every line of the CSV file anyway, and then making a new model? In which case, you can add an extra database call in here;
$model->manufacturer_id = Manufacturer::whereName($colXValue)->first()->id;
(You'd obviously need to put in your own checks etc. here to make sure manufacturers exist)
This method is fine relatively small datsets, however, if you're importing lots and lots of rows, it might end up sluggish with alot of arguably unnecessary database calls.
2) Mapping ALL your Manufacturers
Another option would be to create a local map of all your Manufacturers before you loop through your CSV lines;
$mappedManufacturers = Manufacturer::all()->pluck('id', 'name');
This will make $mappedManufacturers an array of manufacturers that has name as a key, id as a value. This way, when you're building your model, you can do;
$model->manufacturer_id = $mappedManufacturers[$colXValue];
This method is also fine, unless you have tens of thousands of Manufacturers!
3) Where in - then re-looping
Another option would be to build up a list of manufacturer names when looping through your CSV lines, going to the database with 1 whereIn query and then re-looping through your models to populate the manufacturer ID.
So in your initial loop through your CSV, you can temporarily set a property to store the name of the manufacturer, whilst adding it to another array;
$models = collect();
$model->..... = ....;
$model->manufacturer = $colXValue;
$models->push($colXValue);
Then you'll end up with a collection of models. You then query the database for ONLY manufacturers which have appeared:
$manufacturers = Manufacturer::whereIn('name', $models->lists('manufacturer'))->get()->keyBy('name')->toArray();
This will give you array of manufacturers, keyed by their name.
You then loop through your $models collection again, assigning the correct manufacturer id using the map;
$model->manufacturer_id = $manufacturers[$model->manufacturer];
Hopefully this will give you some ideas of how you can achieve this. I'd say the solution mostly depends on your use case - if this was going to be a heavy duty ask - I'd definitely Queue it and be tempted to use Option 1! :P

Update fields from an array with CakePHP

I have an array that is sent to my controller, such as this:
$array = array(
[0]=>array(
[id]=>5,
[position]=>6
),
[1]=>array(
[id]=>8,
[position]=>2
)
);
And I need to save the position of each item using its id. What is the best way to do this in cakePHP? I can only imagine looping an update function or pulling the entire database, changing the correct values, then saving the database. Both ideas seem very bodged.
Ha, Cake magic to the rescue again. You don't have to tell Cake to save it by id. There's a big long amazing way Cake does this, but the short and skinny is -
if your array contains an 'id' key, Cake presumes it is the table primary key and generates an UPDATE statement instead of an INSERT. Looks like this:
UPDATE table as Table SET Table.position = $position WHERE Table.id = $id;
And, Cake knows to iterate for you if you use saveAll() instead of save():
$this->Model->saveAll($array);
If you have any save callbacks in your model, such as beforeSave(), you have to call them manually before calling saveAll() - they only autofire on save(), not saveAll() or updateAll().
You'll want to top your array with the name of your model ($array['Model'][0], $array['Model'][1], etc). If you need to magically saveAll() with multiple models, you top your array with indexed keys, then model names - like $array[0]['Model1'], $array[0]['Model2']) and Cake knows to save / update the associated data for all the models in each index batch.
Cake does ALL the legwork for you:
http://book.cakephp.org/view/1031/Saving-Your-Data - especially the saveAll() entry.
Edit - and topping your array with your model name? Cake makes that ridiculously easy, too. Check out Cake's built-in Set library for all your array / object manipulation needs.
http://book.cakephp.org/view/1487/Set
Have you tried CakePHP's saveAll() ? Details here.

Querying data based on 3rd level relationship in CakePHP

I have the following relationships set up:
A HABTM B
B belongsTo C
C hasMany B
Now, for a given A, I need all C with the B's attached. I can write the SQL queries, but what's the proper CakePHP way? What method do I call on which model, and with which parameters?
I'd go with Aziz' answer and simply process the data as it comes in. If you need C to be your primary model though, you'll have to do a little workaround. Cake is not terrifically good with conditions on related models yet, especially on removed 3rd cousins kind of queries. It usually only does actual JOIN queries on belongsTo or hasMany relations; not on HABTM relations though, those it gets in separate queries. That means you can't include conditions on related HABTM models.
Your best bet then might be something like this:
// get related records as usual with the condition on A, limit to as little data as necessary
$ids = $this->A->find('first', array(
'conditions' => array('A.id' => 'something'),
'recursive' => 2,
'fields' => array('A.id'),
'contain' => array('B.id', 'B.c_id', 'B.C.id') // not quite sure if B.C.id works, maybe go with B.C instead
));
// find Cs, using the ids we got before as the condition
$Cs = $this->C->find('all', array(
'conditions' => array('C.id' => Set::extract('/B/C/id', $ids)),
'recursive => 1
);
Note that this produces quite a bunch of queries, so it's not really an optimal solution. Writing your own SQL might actually be the cleanest way.
EDIT:
Alternatively, you could re-bind your associations on the fly to make them hasMany/belongsTo relationships, most likely using the join table/model of A and B. That might enable you to use conditions on related models more easily, but it's still tricky to fetch Cs when the condition is on A.
$this->A->find(
'first',
array('conditions'=>array('id'=>'someword'),
'recursive'=>2)
);
like this?
this might work
$this->C->find('all',
array('conditions'=>array('id'=>'someword','B.aid'=>$a.id)));
I'd think of "Containable" behaviour (http://book.cakephp.org/view/474/Containable)... gives a lot control on finding related data.

Categories