Features of the method xPDOObject::save() + transaction

recently Sergey Prokhorov aka proxyfabio wrote an article Validation object + transaction. This topic is a bit discussed here. I want to add that this topic is very important, and today it is one of the main problems in the development of major projects on MODX Revolution.

Here just to ask not to start anything like "If doing large projects, it is not necessary to do them on MODX, take your blah-blah-blah". We were doing major projects, and not just for MODX. In MODX it is possible to do big projects, and today there are only a couple of weaknesses that we run individual projects, but otherwise MODX 98% suitable for developing large projects.

So, one of these serious problems associated with the method xPDOObject::save() (called when saving xPDO-objects). The essence of the problem that inside of it is triggered method to save related objects xPDOObject::_saveRelatedObjects() twice. Raz dva. This is done in order to expose the primary and secondary keys for the related objects (see reference material from Ilya Utkin). Explain in detail with an example. Here is the code:
the
<?php
$user_data = array(
"username" = > "test",
);

$profile_data = array();

$user = $modx- > newObject('modUser', $user_data);
$user->Profile = $modx- > newObject('modUserProfile', $profile_data);
$user->save();

print '<pre>';
print_r($user->toArray());
print_r($user->Profile->toArray());


Overall probably the essence of this code is understood by many, but let's focus on the details. When we created two new facilities ($user and $user->Profile), they have not aydishnikov until they are saved. But retaining only the object $user, at the output we get and store an object $user->Profile. It would also clear why, Ilya, in his article describes it all. But the question is, which is not quite in sight hanging is "how xPDO "knows" which id the object $user to assign this id as $modx->Profile->internalKey?". To do this, let's again go over the code of a method xPDO::save();

Here we have first call to the method $user->_saveRelatedObjects(). At this point, the object $user is not yet saved (not posted), id-shnik it yet. $user->Profile is also not saved and has no id, no internalKey. Turning to the method call $user->_saveRelatedObjects(), we see that there is too much of related objects and their preservation method (xPDO::_saveRelatedObject()). Here I clarify once again that we maintain the object $user, which is an object $user->Profile is associated. And here it turns out that in fact the object $user->Profile saved before the object $user. Why? Because in the call to $user->_saveRelatedObject($user->Profile) will be the called method $user->Profile->save(), as well as at the moment for $user->Profile no associated objects, it will be recorded in the database. What do we have here? $user->Profile is already saved and he has his own id, but id have no object $user (because it has not yet been saved). For this reason, and the secondary key $user->Profile->internalKey still empty.

OK, with that understood, let's move on. Next save the object $user with a record in its database and assigns it an id. All the record made. Now we have both of these objects have id-shnik, but still no value for $user->Profile->internalKey. Here's how this method is called and $user->_saveRelatedObjects() again. Now that will persist the related object $user->Profile, he will be able to get the value of $user->id and assign it as $user->Profile->internalKey and stored.
Yes, I agree that this is confusing (and explain it even more complicated), but the logic in all of this. And, in fact, that is why I see it hard to use MyIsam instead of innoDB. Why? Yes, because in innoDB it's not going to be able to work. And just now we analyze the existing issue, and not the principle. I must say that for a full understanding of all this requires a good understanding of MySQL, namely the understanding of the transactions, primary and foreign key and etc.

Let's configure our database is correct, namely configure the primary and secondary keys, at the level of the base. To do this, perform the following:

1. Translate tables to innoDB.





2. The table modx_users field id int(10)unsigned, in modx_users_attributes field internalKey int(10) (not unsigned). Because of this we simply cannot set up a secondary key for the data types of the columns in both tables must match.
Change to unsigned



3 Create the secondary key





If the secondary key you received no errors, then great! But there are a few errors that you can get. The most common ones:
1. Data types do not match.
2. For the secondary record does not exist primary (that is, for example, you have the entry in modx_user_attributes with internalKey = 5, and the entries in modx_users with id = 5 is not present).

Now let's see the problem with an example. To do this, perform console the following code:
the

<?php

$user_data = array(
"username" => "test_". rand(1,100000),
);

$profile_data = array(
"email" => "test@local.host",
);

$user = $modx- > newObject('modUser', $user_data);
$user->Profile = $modx- > newObject('modUserProfile', $profile_data);

$user->save();

print '<pre>';
print_r($user->toArray());
print_r($user->Profile->toArray());


Now we no problem do not see, all preserved without comment.
example output if successful

Array
(
[id] => 59
[username] => test_65309
[password] => 
[cachepwd] => 
[class_key] = > modUser
[active] = > 1
[remote_key] => 
[remote_data] => 
[hash_class] => hashing.modPBKDF2
[salt] => 
[primary_group] => 0
[session_stale] => 
[sudo] => 
)
Array
(
[id] => 54
[internalKey] => 59
[fullname] => 
[email] => test@local.host
[phone] => 
[mobilephone] => 
[blocked] => 
[blockeduntil] => 0
[blockedafter] => 0
[logincount] => 0
[lastlogin] => 0
[thislogin] => 0
[failedlogincount] => 0
[sessionid] => 
[dob] => 0
[gender] => 0
[address] => 
[country] => 
[city] => 
[state] => 
[zip] => 
[fax] => 
[photo] => 
[comment] => 
[website] => 
[extended] => 
)



Now slightly modify our code:
the

<?php

$user_data = array(
"username" => "test_". rand(1,100000),
);

$profile_data = array(
"email" => "test@local.host",
);

$user = $modx- > newObject('modUser', $user_data);
$user->Profile = $modx- > newObject('modUserProfile', $profile_data);

// Pre-set the id of the primary object. Here you should enter your any id, ensuring that the database is not busy.
$user->id = 40;

$user->save();

print '<pre>';
print_r($user->toArray());
print_r($user->Profile->toArray());


What we now get when you run this code?

1. The message about the SQL error
the

Array
(
[0] => 23000
[1] => 1452
[2] => Cannot add or update a child row: a foreign key constraint fails (`shopmodxbox_test2`.`modx_user_attributes`, CONSTRAINT `modx_user_attributes_ibfk_1` FOREIGN KEY (`internalKey`) REFERENCES `modx_users` (`id`))
)


2. Both of our object is still preserved and has the correct id and internalKey.

Why is this happening? While maintaining the secondary object xPDO checks whether a primary key value, and only if it is, then it sets its value as secondary key, and saves the object. In our case, we manually indicated the primary key id and a secondary object was able to get its value and tried to register in the database, but since actually the primary record is not there, we get a SQL error about the impossibility to record secondary record with no primary object. But maintaining the primary object in this is not interrupted. After that, the primary object of $user is successfully recorded in the database, but when re-trying to save related object, $user->Profile normally have all saved as a primary document is available.
all Of this leads to two conclusions.

1. When saving related objects impossible to track bugs saving the secondary objects, and how to react. That is never safe to say, for whatever reason, was not saved as the secondary object (whether no is the primary object, and he will be able to enroll later if you re-call the method xPDOObject::_saveRelatedObjects(), whether there is some unique key conflictual and recording in principle, can not be recorded, whether there is validation at the level of maps not passed, etc. etc.).

2. For this reason, it is impossible to use a full transaction.

Possible solution to this problem.

We see the solution to this problem is to distinguish between the first and second method call to xPDOObject::_saveRelatedObjects() the types of related objects, namely the first call for the primary objects, and a second for the secondary. In this case, it definitely will not be confusion with the keys, and if the object for some reason has not survived, it sure would mean a mistake and you will be able to interrupt the save process (including transaction rollback).
Article based on information from habrahabr.ru

Комментарии

Популярные сообщения из этого блога

ODBC Firebird, Postgresql, executing queries in Powershell

garage48 for the first time in Kiev!

The Ministry of communications wants to ban phones without GLONASS