Is there a way to make saveAll() remove extraneous objects? - php

My host object hasMany option objects associated with it. In the edit form, users can (de)select options and save that new set of associations. This is implemented using saveAll() on the posted data. The result is that
the host (main) object is updated,
option (associated) objects that are included both in the prior and the new association are updated, and
option objects that were not included in the prior association but are included in the new one are created.
But what does not happen is
that option objects that were included in the prior association but not in the new one are deleted.
Question: Can saveAll() do that as well, and how would the data structure have to look like to achieve this effect?
Related information:
My code to handle the edit form is actually more complex (hence I haven't quoted it here) but it results in the data structure as described in the book:
( [Host] => ( ... host object fields ... ),
[Option] => ( [0] => ( ... first option object fields ... ),
...
[n] => ( ... nth option object fields ... )
)
)
Now, if the original host had an associated option that is not included in the 0..n array then saveAll() won't detect this and won't delete that associated object.
Not sure if this is relevant but I am using CakePHP 1.3 .

Not really an elegant solution but works for me.
if ($this->Main->saveAll($this->data))
{
$this->Main->query(sprintf(
'DELETE '
. 'FROM extraneous '
. 'WHERE main_id = \'%s\' AND modified < (SELECT modified FROM main WHERE id = \'%1$s\')'
, mysql_real_escape_string($this->Main->id)
));
}
Note that your tables need to have a modified field.

You can ensure that everything gets executed atomically if you manually wrap everything into a transaction.
This can be done with the begin(), rollback() and commit() methods of the datasource:
$this->Main->begin();
if ( !$this->Main->save(...) ) {
$this->Main->rollback();
return false;
}
// Perform saves in related models...
if ( !$this->Main->MainRelatedModel->save(...) ) {
$this->Main->rollback();
return false;
}
// Perform deletes in extraneous records...
if ( !$this->Main->MainRelatedModel->delete(...) ) {
$this->Main->rollback();
return false;
}
// Everything went well, commit and close the transaction
$this->Main->commit();
The main disadvantage here is that transactions cannot be nested, hence you cannot use saveAll(). You have to save/delete everything step by step, instead of doing it in a single call.

saveAll() wont delete anything from your database.
I guess the best way is to delete options related to the current host before saving, and then adding them. If however, you need to update those that already exists (do you?) for some reason (like: options being related to some other models), I guess you can try to write a piece of code, that will delete unselected options.

Looking for this, I noticed there still isn't a solution built-in CakePHP. To achieve this, I added the following code to my model:
private $oldBarIds = array();
public function beforeSave($options = array() {
parent::beforeSave($options);
$this->oldBarIds = array();
if ($this->id && $this->exists() && isset($this->data['Bar'])) {
$oldBars = $this->Bar->find('all', array(
'fields' => array('id'),
'conditions' => array(
'Bar.foo_id' => $this->id
)
));
$this->oldBarIds = Hash::extract($oldBars, '{n}.id');
}
}
This checks if Bar exists in the saving data. If it does, it'll get the current id's of the current ones, setting them to $this->oldBarIds. Then when the save succeeds, it should delete the old ones:
public function afterSave($created, $options = array()) {
parent::afterSave($created, $options);
if (!$created && $this->oldBarIds) {
$this->Bar->deleteAll(array(
'Bar' => $this->oldBarIds
));
}
}
This way the deletion is handled by the model, and only occurs when the save succeeded. Should be able to add this to a behavior, might do this some day.

HABTM deletes all associated records then recreates what is needed. As PawelMysior suggests, you could achieve this with your hasMany by manually deleting the associated records immediately before the save. The danger, though, is that the save fails you lose the previous state.
I would go with a variant of GJ's solution and delete them after a successful save, but instead loop over an array of redundant IDs and use Cake's Model->del() method. This way you retain all the built-in error handling.

Related

How to simplify this Laravel PHP code to one Eloquent query?

I assume that this should all be in one query in order to prevent duplicate data in the database. Is this correct?
How do I simplify this code into one Eloquent query?
$user = User::where( 'id', '=', $otherID )->first();
if( $user != null )
{
if( $user->requestReceived() )
accept_friend( $otherID );
else if( !$user->requestSent() )
{
$friend = new Friend;
$friend->user_1= $myID;
$friend->user_2 = $otherID;
$friend->accepted = 0;
$friend->save();
}
}
I assume that this should all be in one query in order to prevent
duplicate data in the database. Is this correct?
It's not correct. You prevent duplication by placing unique constraints on database level.
There's literally nothing you can do in php or any other language for that matter, that will prevent duplicates, if you don't have unique keys on your table(s). That's a simple fact, and if anyone tells you anything different - that person is blatantly wrong. I can explain why, but the explanation would be a lengthy one so I'll skip it.
Your code should be quite simple - just insert the data. Since it's not exactly clear how uniqueness is handled (it appears to be user_2, accepted, but there's an edge case), without a bit more data form you - it's not possible to suggest a complete solution.
You can always disregard what I wrote and try to go with suggested solutions, but they will fail miserably and you'll end up with duplicates.
I would say if there is a relationship between User and Friend you can simply employ Laravel's model relationship, such as:
$status = User::find($id)->friends()->updateOrCreate(['user_id' => $id], $attributes_to_update));
Thats what I would do to ensure that the new data is updated or a new one is created.
PS: I have used updateOrCreate() on Laravel 5.2.* only. And also it would be nice to actually do some check on user existence before updating else some errors might be thrown for null.
UPDATE
I'm not sure what to do. Could you explain a bit more what I should do? What about $attributes_to_update ?
Okay. Depending on what fields in the friends table marks the two friends, now using your example user_1 and user_2. By the example I gave, the $attributes_to_update would be (assuming otherID is the new friend's id):
$attributes_to_update = ['user_2' => otherID, 'accepted' => 0 ];
If your relationship between User and Friend is set properly, then the user_1 would already included in the insertion.
Furthermore,on this updateOrCreate function:
updateOrCreate($attributes_to_check, $attributes_to_update);
$attributes_to_check would mean those fields you want to check if they already exists before you create/update new one so if I want to ensure, the check is made when accepted is 0 then I can pass both say `['user_1' => 1, 'accepted' => 0]
Hope this is clearer now.
I'm assuming "friends" here represents a many-to-many relation between users. Apparently friend requests from one user (myID) to another (otherId).
You can represent that with Eloquent as:
class User extends Model
{
//...
public function friends()
{
return $this->belongsToMany(User::class, 'friends', 'myId', 'otherId')->withPivot('accepted');
}
}
That is, no need for Friend model.
Then, I think this is equivalent to what you want to accomplish (if not, please update with clarification):
$me = User::find($myId);
$me->friends()->syncWithoutDetaching([$otherId => ['accepted' => 0]]);
(accepted 0 or 1, according to your business logic).
This sync method prevents duplicate inserts, and updates or creates any row for the given pair of "myId - otherId". You can set any number of additional fields in the pivot table with this method.
However, I agree with #Mjh about setting unique constraints at database level as well.
For this kind of issue, First of all, you have to enjoy the code and database if you are working in laravel. For this first you create realtionship between both table friend and user in database as well as in Models . Also you have to use unique in database .
$data= array('accepted' => 0);
User::find($otherID)->friends()->updateOrCreate(['user_id', $otherID], $data));
This is query you can work with this . Also you can pass multiple condition here. Thanks
You can use firstOrCreate/ firstOrNew methods (https://laravel.com/docs/5.3/eloquent)
Example (from docs) :
// Retrieve the flight by the attributes, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// Retrieve the flight by the attributes, or instantiate a new instance...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
use `firstOrCreate' it will do same as you did manually.
Definition of FirstOrCreate copied from the Laravel Manual.
The firstOrCreate method will attempt to locate a database record using the given column / value pairs. If the model can not be found in the database, a record will be inserted with the given attributes.
So according to that you should try :
$user = User::where( 'id', '=', $otherID )->first();
$friend=Friend::firstOrCreate(['user_id' => $myId], ['user_2' => $otherId]);
It will check with both IDs if not exists then create record in friends table.

How to use the cascade_delete config option in CodeIgniter Datamapper

I use MySQL using InnoDB tables with CodeIgniter Datamapper in my PHP application. Often, the user is given the option of deleting a record through the app by initiating a ->delete function call. When a record has child records (one-to-one or one-to-many), I would also like these records to be deleted along with the parent record, if it is stated by FK constraints in the database.
In this case, I have 2 tables, items and input_lines. I have confirmed that both are using InnoDB. Each item can have many input_lines, so input_lines has a field called item_id, which is set to NULL, indexed, and have FK constraints (ON CASCADE DELETE and ON CASCADE UPDATE). I have set the config element in the DM config file as
$config['cascade_delete'] = FALSE
Because in the documentation it says you should do that if you are using ON UPDATE/DELETE CASCADE. However, when the user initiates the $item->delete() method, only the item is deleted, and the item_id fields on the input_line records associated with the item are set to null.
My models look like this:
class Item extends DataMapper {
public $has_many = array('labour', 'item_type', 'input_line', 'custom_item_type');
...
}
class Input_line extends DataMapper {
public $has_one = array('item');
...
}
I have tried this with cascade_delete = false and true and it won't work. I know the constraints work because deleting the record with MySQL directly works as expected, deleting the child records.
What am I missing? Why is it setting the FK fields to null instead of deleting the record?
EDIT 1:
I decided against my better judgment to debug the delete function in datamapper.php (libraries directory).
I noticed this code in that function:
// Delete all "has many" and "has one" relations for this object first
foreach (array('has_many', 'has_one') as $type)
{
foreach ($this->{$type} as $model => $properties)
{
// do we want cascading delete's?
if ($properties['cascade_delete'])
{
....
So I var_dumped the contents of $properties, and I saw this:
array (size=8)
'class' => string 'labour' (length=6)
'other_field' => string 'item' (length=4)
'join_self_as' => string 'item' (length=4)
'join_other_as' => string 'labour' (length=6)
'join_table' => string '' (length=0)
'reciprocal' => boolean false
'auto_populate' => null
'cascade_delete' => boolean true
It appears the default for when the model doesn't have the property specifically initialized is overriding the config value. This seems like too glaring a mistake so there's definitely something I'm doing wrong somewhere...right? I really, really want to avoid hacking the DM core files...
EDIT 2:
I was thinking maybe the config file wasn't being found, but I checked the logs and there're entries stating that the Datamapper config file was successfully loaded, so that's not the issue.
Doesn't look like anyone has any answers.
My solution was to change the property in the datamapper library $cascade_delete to false, since it's set to true right now. It's unfortunate that I have to resort to hacking the core, but DM won't respect my changes in the config file for cascade_delete so I have no other choice.
If anyone comes across this question and has encountered an issue like this before, please comment.
I have come across the same problem, but finally I just did like this:
$sql = "DELETE FROM EVENT WHERE event_id=".$event_id.";";
$this->db->query ($sql );
In case we set "ON DELETE CASCADE" for the foreign key which refers to event_id, the above SQL works fine, so I just call it directly.

Phalcon's save method not working as I expected

I am trying to use Phalcons save() function to either create or update a record depending on whether the record is already in the database from here
I am doing the following:
$vars = $this->request->getPost();
$code = new Code();
$code->save( $vars, array("code_type", "code", "name") );
When I am sending an update to my controller, the "id" field has the primary key populated, whereas it is blank (but the array key still exists) if I am creating the record.
My understanding is that the ORM should either create or update the record depending on whether the primary key exists or not. The problem I am having is that it is always creating the record- not updating.
I've also tried something like the below, however the reverse is true, when I use find:
$code = Code::findFirst($vars["id"]);
$code->save( $vars, array("code_type", "code", "name") );
Any idea what I could be doing wrong? I want to get to a point where I have a single controller for my insert/update actions.
It's not surprising that the first example is only creating new records, because you're neither loading an existing record nor including the "id" in the white list of fields to save. Instantiating your object as follows should solve the problem (assuming the "id" is an integer):
$code = ($id = (int) $vars['id']) ? Code::findFirst($id) : new Code();

Which cakephp callback methods to choose?

I have table user which have fields username,password, and type. The type can be any or combination of these employee,vendor and client i.e a user can be vendor or client both or some another combination. For type field I have used the multiple checkbox, see the code below. This is the views/users/add.ctp file
Form->create('User');?>
Form->input('username');
echo $this->Form->input('password');
echo $this->Form->input('type', array('type' => 'select', 'multiple' => 'checkbox','options' => array(
'client' => 'Client',
'vendor' => 'Vendor',
'employee' => 'Employee'
)
));
?>
Form->end(__('Submit', true));?>
This is the code I have used in the model file. A callback method beforeSave
app/models/user.php
function beforeSave() {
if(!empty($this->data['User']['type'])) {
$this->data['User']['type'] = join(',', $this->data['User']['type']);
}
return true;
}
This code saves the multiple values as comma separated value in db.
The main problem comes when Im editing a user. If a user has selected multiple types during user creation I can't find the checkbox checked for that user types.
you should never be saving serialized data, json or csv in a field. This makes your life real hard later on down the line.
While habtm is one way to do things, if your binary maths is reasonable you might want to checkout bitmasks for this. here is a great post http://mark-story.com/posts/view/using-bitmasks-to-indicate-status
basics would be
1 = employee
2 = vendor
4 = client
// 8 = next_type
then, if the user was type employee & vendor the type would be 3 (1 + 2) and if it was a vendor & client the type would be 6 (2 + 4)
as you can see there is no way to mix it up, and bitwise works pretty good in mysql aswell so finds are pretty easy. See the post for much more detailed information
You should have a table types and a join table users_types.
What you're looking at is a HABTM relationship, so you should handle it like one.
In the joining UsersType model you should add a custom validation rule that checks if the current combination of types is allowed.
If you want to modify data after it's been found in the database, you can use the afterFind() callback in your model.
So in your case, put something like this is your user model:
function afterFind($results) {
$results['User']['type'] = explode(',', $results['User']['type']);
return $results;
}
There's more info on afterFind in the CakePHP manual.
That being said, it might be worth considering another approach, like a HABTM relationship as deceze first suggested above.

Saving with HABTM in CakePHP

I am creating multiple associations in one go and there are a few problems when it comes to saving.
I have the following code:
<?php
foreach($userData as $user) {
$data = array('User' => array('id' => $user['id']), 'Site' => array('id' => $user['site_id']));
$this->User->save($data);
}
?>
I have experimented with formatting the data array in different ways although I always encounter the same problems. Either the previous entries get moved when a new one is inserted or the current one gets updated.
I could just use the following although I need a behavior to trigger.
$this->User->SiteUser->save($data);
Edit: Also $this->User->create(); doesn't seem to do much.
The IRC helped work out what was wrong, once the unique key was set to false everything was able to save correctly.
//In the user model
var $hasAndBelongsToMany = array(
'Site' => array(
'className' => 'Site',
'unique' => false
)
);
Try resetting the id before a new save(), possibly on both models:
$this->User->id = null;
Cake decides whether to update or insert entries based on the set id, and save() sets an id automatically. Not sure why create() doesn't take care of this for you.
Also, if you want to save HABTM data, you should need to use saveAll() instead of save(). Also see this question.

Categories