Magic of UpdateAll - cakephp

I wrote the code below in cakephp for and updateAll query like
$this->loadModel('User');
$this->User->updateAll(array('stauts'=>'active'),array());
The above code's equivalent SQL query is generated like this
UPDATE User SET status='active' WHERE 0 = 1;
When I write updateAll in cakephp like below
$this->loadModel('User');
$this->User->updateAll(array('stauts'=>'active'));
This code's equivalent SQL query is generated like this
UPDATE User SET status='active';
I don't know why this happens.
If you do not understand my question let me know in comments, I'll explain in shortly.

It's a safety catch
Conditions are often dynamic based on user input. Consider a controller action like so:
function enableAll() {
$conditions = array();
...
if (whatever) {
// Update only today's records
$conditions['created > '] = $yesterday;
}
if ($this->Auth->user()) {
// Update only my records
$conditions['user_id'] = $this->Auth->user('id');
}
$this->Widget->updateAll(
array('active' => 1),
$conditions
);
}
Logically conditions can be one of two things:
An array matching some or no records
An empty array
When it's an empty array, did the developer mean to update all records, or no records?
CakePHP can't know for sure, but if passed, an empty conditions array is more likely to be an error where the intention was to update nothing. Therefore to protect developers from accidentally updating everything, a condition is used which won't match any records (WHERE 0 = 1 is false - it will match no rows, always.).
That's why this:
// I definitely want to update the whole table
$model->updateAll($update);
is treated differently than this:
// mistake? maybe the conditions have been forgotten...
$model->updateAll($update, array());

Related

Is it necessary to avoid loops for updating models in laravel?

I'm trying to sort multiple records for a model based on a field and store their ranks in DB. Like below:
$instances = Model::orderBy('field')->get();
$rank = 1;
foreach ($instances as $instance) {
$instance->update([
'rank' => $rank,
]);
$rank++;
}
I have two questions:
1- Is there any alternative ways to avoid using loop? for example I put the ranks in an array and update the whole records by only one magic method. For example:
$instances = Model::orderBy('field')->get();
$rank = 1;
$ranks_array = array();
foreach ($instances as $instance) {
array_push($ranks_array, $rank);
$rank++;
}
$instances->magicMethod($ranks_array);
2- Is it necessary at all to do so? are the loops have heavy effects on the server or not? need to say that the number of records I'm going to update may not exceed 50 at all.
For insert queries, inserting all records at once will go much faster than inserting them one by one. However for update queries, if you need to update specific rows with specific data, there is no other way than to update them one by one.
I recently came across a similar issue where I needed to update 90k+ row from my DB.
Because I needed to add specific values to each column I needed to individually update each column.
What I found was instead of doing
$DBModel = Model::get();
foreach ($DBModel as $row) {
$row->notify = $row->paid;
// the date is calculated from a specific carbon date from another column
// I did not include the logic here for the example
$row->due = "0000-00-00 00:00:00";
$row->save();
}
Running the previous query took 5m33s
But doing it like this
$DBModel = Model::get();
DB::beginTransaction();
foreach ($DBModel as $row) {
DB::update(update TABLE_NAME set due = ?, notify = ? where id = ?",
["0000-00-00 00:00:00", $row->paid, $row->id]
);
}
DB::commit();
The previous query took only 1m54s to execute.

How to implement conditional ordering?

i am trying to found out how to create custom order function in cakephp3 order function in ORM find() method.
Let's suppose i have following model
User
name
...
custom_data
custom_data
type
every user has one custom data, where type is one of [ 20, 30, 40 ].
I need order by this type in following manner
if ( type == 20 ) {
// put in first positions
} else {
// put this records after users with custom_data->type != 20
}
I need to use it in paginator so i need somehing like
$this->Users
->find()
->where([ something ] )
->order( 'ASC' => here is my custom function )
Any suggestions?
First things first, from your description it sounds as if you need to use DESC, given that you say that you want records that match the condition first. In ascending order these records would come last.
That being said, the most simple way would be to use a basic condition, ie SQL wise something like:
ORDER BY type = 20 DESC
It could also be solved using a CASE statement, but it's not really neccessary if you have such simple requirements.
$query = $this->Users->find();
$query
->where(/* .. */)
->orderDesc(
$query->newExpr()->add(['type' => 20])
);
Further ordering statements can be added via additional calls to order(), orderAsc(), orderDesc().
See also
API > \Cake\Database\Query::order()
API > \Cake\Database\Query::orderAsc()
API > \Cake\Database\Query::orderDesc()
Cookbook > Database Access & ORM > Query Builder > Selecting Data

Upsert an array of items with MongoDB

I've got an array of objects I'd like to insert into a MongoDB collection, however I'd like to check if each item already exists in the collection first. If it doesn't exist then insert it, if it does exist then don't insert it.
After some Googling it looks like I'm best using the update method with the upsert property set to true.
This seems to work fine if I pass it a single item, as well as if I iterate over my array and insert the items one at a time, however I'm not sure whether this is the correct way to do this, or if it's the most efficient. Here's what I'm doing at the moment:
var data = [{}, {}, ... {}];
for (var i = 0; i < data.length; i++) {
var item = data[i];
collection.update(
{userId: item.id},
{$setOnInsert: item},
{upsert: true},
function(err, result) {
if (err) {
// Handle errors here
}
});
}
Is iterating over my array and inserting them one-by-one the most efficient way to do this, or is there a better way?
I can see two options here, depending your exact needs:
If you need to insert or update use an upsert query. In that case, if the object is already present you will update it -- assuming your document will have some content, this means that you will possibly overwrite the previous value of some fields. As you noticed, using $setOnInsert will mitigate that issue.
If you need to insert or ignore add an unique index on the related fields and let the insert fail on duplicate key. As you have several documents to insert, you might send them all in an array using an unordered insert (i.e.: setting the ordered option to false) so MongoDB will continue to insert the remaining documents in case of error with one of them.
As about performances, with the second option MongoDB will use the index to look up for a possible duplicate. So chances are good this would perform better.

checking null return from database using getSingleResult of entity manager

I have to retrieve the db entry. I know the query I have written returns only one row. Therefor I am using getSingleResult. However when the query returns a null, I am not able to catch it. How do I solve this?
Here is the piece of code I have
try {
result = em.createQuery("SELECT d FROM fields d WHERE d.fieldID = :fieldID", Field.class)
.setParameter("fieldID", fieldID)
.getSingleResult();
//manual null check only seems to work. But it seems tedious to check every DB column for null value :(
if((result.getValsText() == null)){
result = new Field();
result.setValText("empty");
}
} catch (NoResultException e) {
result = new Field();
result.setValText("empty");
}
Please advise.
Thanks
For non-Id attributes you can either
send a COUNT query before your regular query or better
call getResultList() on your Query
getResultList will return an empty list if no results have been found. To get your single result, check whether the list is empty and call get(0) on it.
If the attribute is the #Id attribute, call entityManager.find(MyEntity.class, id); - find returns null if no results have been found and a single instance of your entity if it has been found.
EDIT- the last option is preferable for reasons of perfomance and readability. Use the second option (the list) where you cannot use the last one.

CakePHP update field value based on other value in same row?

I have been trying to figure out how to do this and it seems that its not something that many people are trying to do in cakephp or I am just completely misunderstanding the documentation.. I am using the following query below to get a field value so I get a value where the "findbycreated" is 0... this part works fine
$unregisteredemail = $this->Testmodel->findBycreated('0');
$emailaddress = $unregisteredemail['Testmodel']['emailaddress'] ;
$emailpassword = $unregisteredemail['Testmodel']['password'] ;
But now, after I do some things with this data that I retrieved, I want to mark a field, in the same row, in the same model / table as a value of '1' to indicate that an action has taken place (email address has been successfully created, for example)... I just can't figure out how to do this in cakephp despite my efforts of going through the documentation and searching, this should be rather simple, I am tempted, at this point, to just use a regular mysql query as its a simple query.. basically the query is (please excuse my syntax as I haven't used direct mysql queries in a while) "update (database / table) set 'created'='1' where 'emailaddress'=$emailaddress"
Or I could use the row ID, if needed, as cakephp seems to prefer this, but still can't get how to do this.. this is my attempt below that is not working:
// update database to show that email address has been created
$this->Testmodel->read('emailaddress', $this->Testmodel->findBycreated('0'))
$this->Testmodel->id = 1;
$this->Testmodel->set(array(
'created' => '1'
));
$this->Testmodel->save();
There are, as you can see from the previous answers, several ways to achieve the same end. I'd just like to explain a little about why your way didn't work.
In the model, CakePHP has abstracted the database row(s) into an array according its implementation of ORM . This provides us with a handy way of manipulating the data and chucking it around the MVC architecture.
When you say:
$this->Testmodel->set(array(
'created' => '1'
));
You are dealing directly with the model, but the data is actually stored, as an array, in a class variable called $data. To access and manipulate this data, you should instead say:
$this->data['Testmodel']['created'] => '1';
The reason for specifying the model name as the first index is that where associated tables have been retrieved, these can be accessed in the same way, so you might have , for instance:
Array([Testmodel] => Array ([id] => 1,
[created] => [1],
...
)
[Relatedmodel] => Array ([id] => 1,
[data] => asd,
...
)
)
...and so on. Very handy.
Now, when you use $this->MyModelName->save() with no parameters, it uses $this->data by default and uses the part of the array of data appropriate to the model you are calling the save method on. You can also pass an array of data, formatted in the same way if, for some reason, you don't (or can't) use $this->data.
Your use of the method read() is incorrect. The first parameter should be null, a string or an array of strings (representing fieldname(s)). The second parameter should be the id of the record you wish to read. Instead, for param 2, you are passing the result of a find, which will be an array. The result, which you are not capturing, will be empty.
I would write your code like:
$this->data = $this->Testmodel->read('emailaddress',1);
$this->data['Testmodel']['created'] = 1;
$this->Testmodel->save();
or more succinctly:
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created', 1);
In this situation I would let Cake deal with the id's and just focus on changing the row data and resaving it to the database
$row = $this->Model->findBycreated(0);
$row['Model']['emailaddress'] = 1;
$this->Model->save($row);
This way, you don't have to worry about the id's, as the id will be in your dataset anyway, so just change what you want and then tell Cake to save it.
Ninja edit, Be sure that you are returning a full row with an id from your findBycreated() method.
There're many ways to do your work.I suggest you to read the cookbook about saving data in cakephp.And besides david's solution another simple way would be
$this->Testmodel->id = 1;
$this->Testmodel->saveField('created' =>'1');
Ok, I think I finally found the solution, I was able to get this to work:
$this->Test->updateAll(
array(
'Test.field' => 'Test.field+100'
),
array(
'Test.id' => 1
)
);
I think you have to use updateAll as anything else will just create a new row.. basically CakePHP, for whatever reason, neglected to include a function for updating just one field so you have to put it into an array with the updateAll to make it work...
the +100 is where the updated info goes, so in this case "100" would be what the field is updated to.
In cakephp 3.x the syntax seems to be different. This is what worked for me in 3.x:
$this->Tests->updateAll(
[
'Tests.field = Tests.field+100'
],
[
'Tests.id' => 1
]
];
The difference is that the entire expression needs to be in the value of the first array.

Resources