Back to Home

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

More recently · Sergey Prokhorov aka proxyfabio wrote an article Validation of objects + transactions. This topic has been discussed a bit here. From myself I want to add that this topic is extremely important · and today ...

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

    More recently, Sergey Prokhorov aka proxyfabio wrote an article Validation of objects + transactions . This topic has been discussed a bit here . I want to add on my own that this topic is extremely important, and today it is one of the most important problems in the development of large projects on MODX Revolution.

    Here I’ll immediately ask you not to start anything like “If you are doing large projects, don’t do them on MODX, take blah blah blah.” We did large projects, and not only on MODX. It is quite possible to make large projects at MODX, and today there are only a couple of weaknesses that we rule on individual projects, otherwise MODX is 98% suitable for developing 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 related objects xPDOObject :: _ saveRelatedObjects () twice is triggered . One and two . This is done in order to set up primary and secondary keys for these related objects (see reference material from Ilya Utkin). I will explain in more detail with an 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());
    


    On the whole, for sure, the essence of this code is clear to many, but let's focus on the details. When we created two new objects ($ user and $ user-> Profile), they still do not have identifiers until they are saved. But saving only the $ user object, we get the saved $ user-> Profile object as well. This, as it were, is also understandable why, Ilya describes all this in his article. But the question that doesn't quite hang out is “how does xPDO“ know ”what id the $ user object is in order to assign this id as $ modx-> Profile-> internalKey?”. To do this, let's go over the xPDO :: save () method code again;

    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 does not have an id yet. $ user-> Profile is also not saved and has neither id nor internalKey. Turning to the call to the $ user -> _ saveRelatedObjects () method, we see that the related objects are being enumerated 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 here it turns out that in fact the $ user-> Profile object will be saved earlier than the $ user object. Why? Because in the call $ 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 does not have an 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 are saving the $ user object itself, writing it to the database and assigning it an id. That's it, the record is done. Now we both have these id-shniki, but still there is no value of $ user-> Profile-> internalKey. This is exactly what the $ user -> _ saveRelatedObjects () method is called for .again. Now, when 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 on innoDB it simply will not be able to fully work. And just now, we will analyze the existing problem, and not the principle of work. I must say right away that a complete understanding of all this requires a good understanding of MySQL, namely an understanding of transactions, primary and foreign key, etc.

    Let's configure our database even more correctly, namely, we will configure primary and secondary keys at the level of the database itself. To do this, do the following:

    1. Let's transfer tables to the innoDB engine.





    2. In the modx_users table, the id field is int (10) unsigned, and in modx_users_attributes the internalKey field is int (10) (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.
    Change to unsigned



    3 Create a secondary key





    If you did not receive any errors while saving the secondary key, then wonderful! But there are a few mistakes you may get. The most common of them:
    1. Data types do not match.
    2. For the secondary record, there is no primary one (that is, for example, you have a record in modx_user_attributes with internalKey = 5, but there is no record in modx_users with id = 5).

    Now let's look at the essence of the problem with 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" => "[email protected]",
    );
    $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 was preserved without comment.
    Sample output on success
    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] => [email protected]
        [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 let's change our code a bit:
    <?php
    $user_data = array(
        "username"  => "test_". rand(1,100000),
    );
    $profile_data = array(
        "email" => "[email protected]",
    );
    $user = $modx->newObject('modUser', $user_data);
    $user->Profile = $modx->newObject('modUserProfile', $profile_data);
    // Заранее установим id первичному объекту. Здесь следует указать свой какой-нибудь id, убедившись, что в БД он не занят.
    $user->id = 40;
    $user->save();
    print'<pre>';
    print_r($user->toArray());
    print_r($user->Profile->toArray());
    


    What will we get now when we execute 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 to see if there is a primary key value, and only if it is, then it already 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 there is actually no primary record there, we get an SQL error about the inability to write the secondary record without the primary object. But preservation of the primary object does not stop there. 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.

    Two conclusions follow from all this.

    1. When saving related objects, it is impossible to track errors in saving secondary objects and somehow react to them. That is, one can never say with certainty for what reason the secondary object was not saved (whether there is no primary object yet, and it will be able to write later when the xPDOObject :: _ saveRelatedObjects () method is called again, or whether some unique key clashed there and in principle, a record cannot be recorded, whether validation at the map level did not pass there, etc. etc.).

    2. For this reason, it is not possible to use fully transactions.

    A possible way to solve this problem.

    We see a solution to this problem in distinguishing between the first and second calls to the xPDOObject :: _ saveRelatedObjects () method according to the types of related objects, namely the first call is for primary objects and the second call is for secondary. In this case, there will definitely not be confusion with the keys, and if the object was not saved for some reason, this will definitely mean an error and it will be possible to interrupt the saving process (including rollback of transactions).

    Read Next