rc3.org

Strong opinions, weakly held

Fun with saving things in Ruby on Rails

I’m running into a bit of a conundrum when it comes to designing objects in a Ruby on Rails application, and thought I’d toss it up here for discussion. I have an application with two persistent objects, User and Address. Address is a child of user, and user holds a pointer to their current address (so that a history of addresses can be maintained. So users has an ID column and a current address ID coumn, and addresses has an ID column and a user ID column. As you might imagine, this leads to a bit of a chicken and egg problem.

I have one “new user” form that enables someone to create a user. If everything goes well, when the user submits the form, a new User object will be created and a new Address object will be created. The question is, how do I design the method that saves the two objects? Here’s why this question is a little bit complicated:

  • I don’t want to save anything if there are problems with either of the objects. So if the address is invalid, I don’t want to save the user.
  • User ID is a required field for the address object, so in order to save the address, I have to save the user object first.
  • One purpose of this process is to save the objects to the database if they are valid. The other is to record the problems with the objects if they are not valid, so that they can be presented to the user for correction. One option would be to validate both objects before trying to save either of them and then save only if both are valid. Unfortunately, the address will always be treated as invalid if the user has not been saved.
  • Once the user and address have been saved, you have to go back and save the user again to populate he user’s current address ID with the newly assigned ID fof the address.

Currently the most straightforward to make this work is to instantiate and save the user object and then instantiate and save the address object. If you try to instantiate the address object and link it back to the user object, you get a stack error because ActiveRecord recurses through the circular relationships between the user and the address. The problem here is that once you’ve created the user object, if there’s an error in the address object, the user has already been saved. That breaks the expected experience in that the data will be partially saved.

So the question is, what’s the easiest and most idiomatic Rails way to handle this situation? Here are the options as I see them:

  1. Wrap all of the database operations in a transaction. If the save of the address fails, roll back the transaction and wipe out the newly created user.
  2. Muck around with the validation so that you can validate the address before you save the user. Then validate both the user and the address without attempting to save them.
  3. Split the form into two forms, one which creates the user and another which allows them to submit their address.
  4. If the address validation fails but the user validation succeeds, report to the user that the user object was saved but ask them to correct their address.

There are probably other options as well. I’m still trying to determine which makes the most sense. Any ideas?

Update: I’ve posted how I solved this problem.

7 Comments

  1. One approach would be to forgo the validation on addresses.user_id, and prevent “addresses of no user” with a NOT NULL constraint at the database level. (I’m assuming that you never reassign an address to a different user, so you don’t need validations to help the UI with that).

    BTW, this schema illustrates why it’s hard to come up with a completely general solution for getting fixtures to work with foreign key constraints. Let’s say that your fixtures include a user and that user’s most recent address. These are two database rows, each of which has a foreign key on the other. The fixtures machinery has to insert one of these rows into the database first; whichever row it is, the row in the other table won’t be present yet, and the foreign key will be invalid. When deleting fixtures, you have the same problem in reverse; you can’t delete either row without invalidating the FK in the other.

    In schemas without this kind of circularity, there’s an easy (though ugly) workaround: order the tables so that each has foreign keys only on tables that were already named (something like “users, blogs, posts, comments”), put something like this in your test_helper

    def self.use_all_fixtures
    fixtures :users, :blogs, :posts, :comments
    end
    

    and have all your tests “use_all_fixtures”. But with the circularity, the only workable thing I can think of would be to find a place to hook the fixtures machinery to delay setting the FK’s on insertion, and null them out on deletion (paying due attention to whichever FK’s have been declared NOT NULL)…

  2. How about, just for the validation, using a known-good user-id (maybe 1 for the admin or whatever). That way that constraint is satisfied. If the user is also valid, save it, replace the dummy id in address and save that one. If either is valid, combine the .errors arrays and show them to the users.

  3. *If either is invalid 🙂

  4. Sticking in a “known-good”, but incorrect, user ID would scare me a bit — it opens you up to bugs in which the address object gets saved before its user ID has been changed from 1 (or whatever) to the correct value.

    BTW, yet another approach would be to put the check for a non-nil address.user_id in a before_save callback on the address class, rather than in validations.

    (And while I’m at it, another approach to fixtures-with-fks would be to use db-specific voodoo to disable the foreign key constraints while the fixtures machinery is messing around, and reenable them after it’s finished — tricky, but still a game that real DBAs sometimes play when using, say, Oracle export/import on tables full of live data…)

  5. As far as database constraints go, you can have foreign keys on both sides but you have to allow one or the other to be null. current_address_id is the one I allow to be null, since all addresses need an owner, but a user may not have a current address.

  6. Addressing only the database design issues and not the Ruby-specific questions.

    Are current addresses and previous addresses really the same kind of thing? If each user has one & only one current address, could it be a property of the user object, with previous addresses by themselves in a previous address object? Then there’s no circularity, as the user object has no foreign key into the previous address object.

    I had this very exact situation today in a database I was training some new employees on. The above is how I drew the conceptual data model diagram. (I’m afraid the actual implementation isn’t so clean.) We don’t need to keep track of as much data for previous addresses as we do for current ones, which also suggested storing them separately.

  7. I don’t want to save anything if there are problems with either of the objects. So if the address is invalid, I don’t want to save the user.

    My question is, “why not?” The cost is relatively inexpensive; sure, you’re burning a PK increment, but you’d need to burn through too many to care about before you run into trouble.

    I’m assuming that your address model is validating and will raise in the event of invalid address…

    I think you had it correct; you can create a new model to interact with User/Address, then here’s your method:

    ...
    def create
    newGuy = User.create ( /assign values to fields / )
    
    newGuy.save
    
    newAddress = Address( /assign values to fields /)
    
    begin
    newAddress.save
    rescue
    newGuy.destroy
    raise 'Invalid address... start over, you pig!'
    end
    end
    ...
    

Leave a Reply

Your email address will not be published.

*

© 2024 rc3.org

Theme by Anders NorenUp ↑