inconsequentia's avatar

How to save related ('hasMany') model before an ID exists?

Let’s say I have a Page that hasMany(‘Photos’), and, on the page’s edit form, I have an AJAX photo uploader.

Editing an existing Page poses no problems. I pass the page’s id to the controller method that the images upload to, and when the images are uploaded, their page_id property is set.

However, if I try this when creating a new Page, this obviously doesn’t work as there is no id yet.

I’ve come up with a few ideas on how to deal with this but I don’t like any of them and was wondering if maybe there’s a pattern here that I’m overlooking. Here are my solutions:

  1. In the Page’s create() method, instead of doing it the usual way, do new Page(), save it, then redirect to the edit method. This way we can’t ever edit a page without an id. This may be problematic if the user navigates away from the Editing page as we run the risk of ending up with a bunch of blank rows in the DB. Also problems will occur if certain fields are not nullable, you’d have to fill them with placeholders, which is not great. Maybe problematic in other ways?

  2. Create the photos without a page_id or with an arbitrary integer like PHP_INT_MAX then do a sweep when the Page is saved and change those out for the Page’s id. This would become a problem the instant you have more than one user creating pages. Wouldn’t happen in my app, but still smells bad.

  3. Create a hidden field with a random integer and pass that to both the photos’s and the page’s creation methods. Then do a sweep when the page is saved. This muddies up the model design and the database with crap that only gets used once and shouldn’t really be in the database if you think about it.

Any thoughts? I can’t be the first person to face this problem. Am I missing something obvious?

0 likes
9 replies
thepsion5's avatar

The typical way I've seen this handled is via #3 - there's an application service that generates the next primary key for the entity (either a GUID, or by getting the next auto-increment value and then setting it manually for that table so it doesn't get re-used) and then passes it to the form as a hidden field, so any related entities can use it and be saved before the parent entity is.

Here's an alternative, though:

  1. On the create page view, have the upload AJAX submit to a different URL
  2. At that URL, temporarily store the files somewhere and save their location to the session under a pending_page_images key or something.
  3. When storing a new page, have the controller/service/repository check the session for pending_page_images and create and save the associated models if they exist.
bestmomo's avatar

Setting a reservation in table seems to be a good option, but the problem is : how to clean it if it's not used ? Maybe a field "time_end" could do the job.

RemiC's avatar
  1. Add a 'draft' boolean column to your page table.

  2. Prior to enter the edit form, create a new page with 'draft' on true, and pass the ID to the form

  3. Make any ajax call with this ID.

  4. When page is saved, set draft to false.

inconsequentia's avatar

Thanks for the replies, some good ideas here. @thepsion5, which application service would that be? I figured I could get the next id somehow. My concern with just getting the next auto-increment was the hypothetical situation of there being more than one user creating a page at the same time, in which case the IDs could get mixed up. Does the service you’re talking about prevent that somehow?

RemiC's avatar

Also, another approach would be to have your Ajax Upload return the created Photo Ids and pass these ids along with the page when saving, then you just have to update the page_id on the photo model once the page is created.

bestmomo's avatar

@magiclantern I think the only way to be sure to reserve the id is to create a draft record in the table with a special datetime field to purge it if it s not used.

thepsion5's avatar

@magiclantern @bestmomo Not entirely true, you can manually set the auto-increment value. You'd have to create the service class to do it yourself, and to be honest it's a bit ugly. It'd look something like this:

class DatabaseIdFactory()
{

//inject the DB connection in the constructor
    public function getNextId($table)
    {
        $this->db->transaction(function() use($db, $table)
        {
            $current = $db->unprepared("SELECT IDENT_CURRENT('$table')");
            $next = $current + 1;
            $db->unprepared("ALTER TABLE $table AUTO_INCREMENT = $next");
        });
        return $current;
    }
}

None of the solutions is perfect, annoyingly. I would personally go with the "draft" idea:

  • Create a status field in your Page table, have it set to 1 for "draft" and 2 for "complete" or "active", and have it default to 1
  • Create and save a new Page with placeholder info, pass it to the form
  • Associate new photos with that page object

Advantages of this method are that it doesn't require you compromise your foreign key constraints, doesn't require screwing with the primary key in any way, and is pretty simple, conceptually and code-wise.

Disadvantages are that you need to periodically clean up drafted pages so they don't clutter the table and requires an additional column to hold the page's status, though you might want this later anyway depending on your business needs.

1 like
inconsequentia's avatar

Thanks for your replies, everyone! I will go with having a 'draft' column.

Will clean up either with a cron job or maybe on sign-in/-out.

bestmomo's avatar

@thepsion5 yes it's a bit ugly but must work. For info (because I looked for it...) there is no IDENT_CURRENT in MySQL but LAST_INSERT_ID.

1 like

Please or to participate in this conversation.