Most recently, Sergei Prokhorov aka
proxyfabio wrote an article
Validation of objects + transactions . A little this topic was
discussed here . From myself I want to add that this topic is extremely important, and today it is one of the main problems in the development of large projects on MODX Revolution.
Here I will immediately ask you not to start anything like “If you are doing major projects, do not do them on MODX, take the blah blah blah”. We did major projects, and not only on MODX. On MODX, it is quite possible to do large projects, and today there are only a couple of weak points that we rule on individual projects, otherwise the MODX is 98% suitable for the development of large projects. So, one of these serious problems is associated with the xPDOObject :: save () method (called when saving xPDO objects). The essence of this problem is that inside it, the method of saving the associated objects xPDOObject :: _ saveRelatedObjects ()
twice works.
One and
two . This is done in order to expose the primary and secondary keys for these related objects (see the
reference material from Ilya Utkin). I will explain in more detail by example. Here is the code:
<?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());
')
In general, the essence of this code is certainly understandable to many, but let's focus on the details. When we created two new objects ($ user and $ user-> Profile), they do not yet have aisers until they are saved. But having saved only the $ user object, we get the saved $ user-> Profile object at the output. It’s also clear why, Ilya describes all this in his article. But a question that doesn’t quite hang out is “how does xPDO" know "what id of the $ user object to assign this id as $ modx-> Profile-> internalKey?". To do this, again, let's go over the method code xPDO :: save ();
Here we have the
first call to the $ user -> _ saveRelatedObjects () method . At this moment, the $ user object has not yet been saved (not written to the database), it has no id yet. $ user-> Profile is also not saved and has neither id nor internalKey. Moving on to calling the $ user -> _ saveRelatedObjects () method, we see that the
related objects are being searched and
saved (xPDO :: _ saveRelatedObject () method). Here I will clarify once again that we are saving the $ user object for which the $ user-> Profile object is related. And it is here that it turns out that in fact the object $ user-> Profile will be preserved before the object $ user. Why? Because in a call to $ user -> _ saveRelatedObject ($ user-> Profile), the
method $ user-> Profile-> save ( ) will be
called , and since there are currently no related objects for $ user-> Profile, it will be written to the database. And what do we get here? $ user-> Profile has already been saved and it has its own id, but the $ user object has no id (because it has not been saved yet). For this reason, the secondary key $ user-> Profile-> internalKey is still empty.
OK, we figured it out, we’re going on. And then we have to save the object itself $ user with writing it to the database and assigning it an id. Everything, record is made. Now we both have these id-objects, but still there is no value for $ user-> Profile-> internalKey. This is exactly how the
$ user -> _ saveRelatedObjects () method is called again. Now that the associated $ user-> Profile object is saved, it will be able to get the value of $ user-> id and assign it as $ user-> Profile-> internalKey and save it.
Yes, I agree that all this is very confusing (and I explain it even more confusing), but there is logic in all this. And, actually, for this reason I see such persistent use of MyIsam instead of innoDB. Why? Yes, because it simply cannot work at innoDB. And just now we will analyze the problem, and not the principle of operation. I’ll say right away that a complete understanding of all this requires a good understanding of MySQL, namely the understanding of transactions, primary and foreign key, etc.
Let's configure our database more correctly, namely, we will configure primary and secondary keys at the level of the database itself. To do this, do the following:
1. Translate tables into the innoDB engine. 2. In the modx_users table, the id field is int (10) unsigned, and in modx_users_attributes the internalKey int (10) field (not unsigned). Because of this, we simply cannot configure the secondary key, because the data types in the columns of both tables must match completely.
If you did not get any errors while saving the secondary key, then great! But there are a few mistakes you can get. The most common ones are:
1. Data types do not match.
2. There is no primary record for the secondary record (that is, for example, you have a record in modx_user_attributes with internalKey = 5, and there are no record in modx_users with id = 5).
And now let's look at the essence of the problem on an example. To do this, execute
the following code in the
console :
<?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 have not seen any problem, everything is preserved without comment.
Approximate output on successful execution 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] => )
And now let's change our code a bit:
<?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);
What do we get now when executing this code?
1. SQL error message
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 objects are still preserved and have the correct id and internalKey.
Why it happens? When saving a secondary object, xPDO
checks if there is a primary key value , and only if it exists, then sets its value as a secondary key and saves this object. In our case, we manually specified the primary key id and the secondary object managed to get its value and tried to write to the database, but since the primary record is actually not there, we get a SQL error about the inability to write the secondary record without the primary object. But the preservation of the primary object on this is not interrupted. After that, the primary $ user object is successfully written to the database, and when you try to save the associated $ user-> Profile object again, everything is normally saved, since there is a primary record.
From all this two conclusions follow.1. When saving related objects, it is impossible to track the errors of saving secondary objects and somehow react to them. That is, it is never possible to say with certainty why the secondary object was not saved (whether or not the primary object is still available, and it can later register when the xPDOObject :: _ saveRelatedObjects () method is called again, or if there is any unique key that conflicts the record cannot be recorded in principle, or validation at the level of the mapa did not pass there, etc., etc.).
2. For this reason, it is impossible to fully use transactions.
Possible solution to this problem.We see the solution to this problem in distinguishing the first and second calls of the xPDOObject :: _ saveRelatedObjects () method by the types of related objects, namely, the first call is for primary objects, and the second call is for secondary objects. In this case, there will definitely be no confusion with the keys, and if the object has not been preserved for some reason, then this will definitely indicate an error and it will be possible to interrupt the save process (including rollback of transactions).