Rails, sessions, and associations?

I’m running Rails 1.2.3, as a precursor.

I’ve recently discovered some odd behavior in how Rails treats objects
saved in the session and its associations.


class Foo < ActiveRecord::Base
has_one :bar
has_one :barfoo
end

foo = Foo.new
foo.build_bar
session[:myfoo] = foo

POST

foo = session[:myfoo]
foo.new_record? -> true
foo.bar.nil? -> false

foo.save
foo.build_barfoo
foo.barfoo.nil? -> false
foo.barfoo.new_record? -> true
session[:myfoo] = foo

POST

foo = session[:myfoo]
foo.new_record? -> false
foo.barfoo.nil? -> true # This is the problem!


It looks like what happens is that if you store an object in the
session, associations get stripped from the object before
serialization IF object.new_record? == false. This is good, most of
the time, as it keeps the session size down. However, when the
association is unsaved, this is bad. I’m doing a process like so:

Object created -> association #1 created -> object and association
updated with form data and stored in the session -> preview is
rendered -> on confirm, object in session is saved.

Later,

Object loaded from DB -> association #2 created -> association #2
updated with form data -> object stored in session -> preview rendered
-> on confirm, should save the object and its association, but the
association is now nil.

Is this a bug? Should unsaved associations not be stripped, or am I
using the wrong design pattern here?

My workaround was to store the association object in the session and
to manually reassociate it with the object on each page load, but
that’s really kludgy and I’d prefer a more elegant solution if
possible. This feels like a bug, though, and it caused me to lose
multiple hours tracking it down - it is certainly not intuitive in the
slightest.

Antiarc wrote:

Object loaded from DB -> association #2 created -> association #2
updated with form data -> object stored in session -> preview rendered
-> on confirm, should save the object and its association, but the
association is now nil.

Is this a bug? Should unsaved associations not be stripped, or am I
using the wrong design pattern here?

My workaround was to store the association object in the session and
to manually reassociate it with the object on each page load, but
that’s really kludgy and I’d prefer a more elegant solution if
possible. This feels like a bug, though, and it caused me to lose
multiple hours tracking it down - it is certainly not intuitive in the
slightest.

Everything stored in session will be serialized, so yes, the design is
incorrect. You should save the record id in the session instead of
storing the ActiveRecord object in a session O_O.

Wai T. wrote:

Everything stored in session will be serialized, so yes, the design is
incorrect. You should save the record id in the session instead of
storing the ActiveRecord object in a session O_O.

Ok, I may be approaching this incorrectly then. I want to achieve the
following:

a) Create a new object (Foo) that may be saved to the database at a
future point. If the user abandons the process, then nothing should be
saved to the database.
b) Be able to create association objects (Bars and Barfoos) to be
associated with the object, and then possibly saved at some point in the
future. If the user abandons the process, the associations should not be
saved.

My process so far has been:

  • Create a Foo, set some properties based on form input, and put it in
    the session.

  • Add any Bar associations and set their properties via form input

  • Save the Foo to the session, which serializes its Bar as well.

  • Preview the Foo with its Bar association

  • If we decide to commit the changes, Foo (and its Bar) are read in from
    the session, validated, and saved.

  • At some point later, a user may load a Foo from the database and want
    to attach a Barfoo to it. build_barfoo is called, and some properties
    are set on both the Foo and the Barfoo from form input.

  • The Foo (and its associated Barfoo) should be saved to the session
    (this is where it fails)

  • The user may then preview the results

  • If the user chooses to commit the changes, the Foo should be saved
    (UPDATE) and its associated Barfoo should be saved (INSERT).

How else could I achieve this workflow?

For situations like this, we always save to the database. We’ll use
before_filters to load the data from the database on each request…
Every
night we run a process to purge incomplete records from the database by
checking the created_at date We’re usually wiping out records that are
7
days old or more just to be safe. Using a status flag on the table lets
me
control what “state” the object’s in, and I can have validations that
only
get fired on certain states, allowing me to save records at any point in
the
process.

If that idea doesn’t sit well with you, consider the following:

  1. If a user’s browser crashes, they lose their work. With the above
    method,
    they could come back and finish.
  2. Marshalling to the session is prone to problems and can be a
    performance
    problem.

Just my .02. I’m sure that lots of folks will disagree with my
approach.

I ended up going with this solution (heavily simplified, of course!):

class Foo
has_one :bar
has_one :barfoo
end

foo = Foo.new params[:foo]
foo.build_bar params[:bar]
session[:myfoo] = foo

post to preview -> confirm save

foo = session[:myfoo]
if foo.save then
session[:myfoo] = nil
end


foo = Foo.find params[:id]
foo.build_barfoo params[:barfoo]
session[:myfoo_id] = foo.id
session[:myfoo_barfoo] = foo.barfoo

post to preview -> confirm save

foo = Foo.find session[:myfoo_id]
foo.barfoo = session[:myfoo_barfoo]
if foo.save then
session[:myfoo_barfoo] = nil
session[:myfoo_id] = nil
end


This seems to work well. Only unsaved objects are persisted in the
session, which gives me pretty good concurrency-proofing, doesn’t force
me to deal with the marshaling quirks of an saved object with unsaved
associations, and doesn’t leave cruft in the database.

It isn’t browser-crash-proof, but this is “cheap data” that doesn’t
really need to be protected against crashes.