Models belonging to the session?

I'm implementing a shopping cart which is stored in the database rather than in the session itself. I'm using ActiveRecordStore for session. (using Edge)

I'd like to have the cart belong_to the session in a normal has_one/belongs_to relationship, e.g.,

create table cart (cart_id primary key,
                    session_id integer not null unique
                      references sessions);

class Cart < ActiveRecord::Base
   belongs_to :session
end

class Session < ActiveRecord::Base
   has_one :cart
end

Using the How to Change Session Store page on the wiki[0] and the active_record_store.rb documentation[1] as references, I believe I should be able to define a find_cart method that returns the current cart or creates a new one:

# in app/controllers/application.rb
def find_cart
   @cart = @session.model.cart || @session.model.create_cart
end

I get a NoMethodError:
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.model

However, shouldn't there already be a session created when the page is called?

As using @session is deprecated, I've also tried using session.model.cart instead:

   def find_cart
     @cart = session.model.cart || session.model.create_cart
   end

However, this returns an method missing error:

undefined method `cart' for #<CGI::Session::ActiveRecordStore::Session:0x33752c8>

My guess is that I'm not extending the Session model correctly: however, with Ruby's open classes, I think this should work. It's also interesting to me that the

What am I missing? Thanks for any suggestions or pointers to references.

Michael Glaesemann
grzm seespotcode net

[0](http://wiki.rubyonrails.com/rails/pages/HowtoChangeSessionStore)
[1](http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/session/active_record_store.rb)

undefined method `cart' for
#<CGI::Session::ActiveRecordStore::Session:0x33752c8>

My guess is that I'm not extending the Session model correctly:
however, with Ruby's open classes, I think this should work. It's
also interesting to me that the

Perhaps the problem is that you're extending Session rather than
CGI::Session::ActiveRecordStore::Session ?

Fred

That was a good idea:

class CGI::Session::ActiveRecordStore::Session
   has_one :cart
end

However, this gives me the same errors. I've got session.rb in app/models. Might it need to be somewhere else?

Michael Glaesemann
grzm seespotcode net

However, this gives me the same errors. I've got session.rb in app/
models. Might it need to be somewhere else?

I've apparently got it working by putting my session.rb file in lib/ and requiring it from my environment.rb configuration. I guess I'm supposed to interpret the wiki's

This will of course require creating a model ‘session.rb’ which extends ActiveRecord.

to mean I need to implement an alternative to CGI::Session::ActiveRecordStore::Session rather than just extend it. That's further backed up by this section from the API docs:

You may provide your own session class implementation, whether a feature-packed Active Record or a bare-metal high-performance SQL store, by setting

  +CGI::Session::ActiveRecordStore.session_class = MySessionClass+

You must implement these methods:

  self.find_by_session_id(session_id)
  initialize(hash_of_session_id_and_data)
  attr_reader :session_id
  attr_accessor :data
  save
  destroy

I'm otherwise happy with CGI::Session::ActiveRecordStore::Session, so hopefully my little extension will suit my purposes.

Thanks, Frederick, for the push!

By the way, I'd love to hear from anyone else with experience along these lines :slight_smile:

Michael Glaesemann
grzm seespotcode net

Hmn. I've been trying to do this same thing and found your thread on
Google.

I tried your recommendation and sort of got it working, but kept
getting errors when trying to load my added association.

So now I'm trying to put the model in app/models and have it inherit
from CGI::Session::ActiveRecordStore::Session, but when I go to set
session_class it uses the wrong Session class. Changing the name of my
custom session to avoid a namespace collision doesn't help, and causes
an uninitialized constant error to boot. Here's what I've got:

app/models/foo_session.rb
  class FooSession < CGI::Session::ActiveRecordStore::Session
    set_table_name :sessions
    belongs_to :user
  end

config/environment.rb
  CGI::Session::ActiveRecordStore.session_class = FooSession

The documentation implies it's all so damn easy that there doesn't
need to be an example :wink: But even with that I still get

  undefined method `user' for #<FooSession:0xb73d5a10>

Any ideas?

Why are you doing it like this? Just create your cart and store the
id in the session.

def find_cart
  @shopping_cart ||= Cart.find(session[:cart_id])
end

def cart=(cart)
  if !cart.nil?
    session[:cart_id] = cart.id
  else
    session[:cart_id] = nil
  end
end

Model objects should not know anything about the session.

Pat

Why are you doing it like this? Just create your cart and store the
id in the session.

<snip />

Model objects should not know anything about the session.

I agree. There are two reasons why I've been attempting to treat the include the Session model in a has_one/belong_to association:

(a) Right now I don't have any attributes associated with the Cart model other than its id, i.e., the table corresponding to the model is just

create table carts (cart_id serial primary key)

This feels wrong. There's really no information there at all. Plus, ActiveRecord doesn't know how to create new rows that consist of only an autogenerated primary key. It tries to INSERT INTO "carts" () VALUES () which leads to an error (at least with a PostgreSQL backend). Perhaps there's a way to educate ActiveRecord on how to do this, but I wasn't particularly motivated to figure this out.

(b) Having the carts table reference sessions allows the you to cascade deletes from sessions to the carts table (and on to the cart_items table as well). This is handy because then you're able to easily clean up stale carts when you're cleaning out stale sessions.

However, I must say it's been an uphill battle, including issues such as those raised by I.E. Smith-Heisters in another response, and figuring out a way to preload session data before creation of fixtures during testing. Learned more about the backend, but I haven't been overly satisfied with the result.

I think I'm just going to go back and add a cart_created_at column to the carts table to work around ActiveRecord's inability to deal with the situation where there's only a single attribute. I'm too tired right now to consider patching ActiveRecord. Probably the simplest all-around solution.

I wish you had piped up a couple of days ago :slight_smile: I'd love to hear your opinions on the two points I raised above. I'm sure I'm missing something.

Thanks for your response!

Michael Glaesemann
grzm seespotcode net

PS This makes me think I should move away from ActiveRecordStore entirely to prevent myself from considering something like this in the future. :slight_smile:

[Please don't top post as it makes the discussion more difficult to follow.]

So now I'm trying to put the model in app/models and have it inherit
from CGI::Session::ActiveRecordStore::Session, but when I go to set
session_class it uses the wrong Session class. Changing the name of my
custom session to avoid a namespace collision doesn't help, and causes
an uninitialized constant error to boot.

I believe load order is the root of this problem (see below). I don't think you'd have to worry about namespace issues though. I think you'd need to always explicitly refer to ActiveRecordStore::Session by it's full name.

The documentation implies it's all so damn easy that there doesn't
need to be an example :wink:

I hear you :slight_smile: During my experiments I pulled ActiveRecordStore::Session into it's own file and placed it in lib/ and ran into underlying session library errors. I thought at least that would work :confused:

I'm pretty sure one of the issues is with load order: I think the model needs to be in lib/ so it's available to when the server is started (presumably prior to when models are loaded).

But even with that I still get

  undefined method `user' for #<FooSession:0xb73d5a10>

Any ideas?

Not really. As indicated in my response to Pat Maddox, I'm through with this experiment, at least for now. Perhaps when I'm a better coder I can revisit this and see what's up.

If you figure it out, let me know. I'd love to hear details on how you got it working :wink: Good luck!

Michael Glaesemann
grzm seespotcode net

> Why are you doing it like this? Just create your cart and store the
> id in the session.

<snip />

> Model objects should not know anything about the session.

I agree. There are two reasons why I've been attempting to treat the
include the Session model in a has_one/belong_to association:

(a) Right now I don't have any attributes associated with the Cart
model other than its id, i.e., the table corresponding to the model
is just

create table carts (cart_id serial primary key)

This feels wrong. There's really no information there at all. Plus,
ActiveRecord doesn't know how to create new rows that consist of only
an autogenerated primary key. It tries to INSERT INTO "carts" ()
VALUES () which leads to an error (at least with a PostgreSQL
backend). Perhaps there's a way to educate ActiveRecord on how to do
this, but I wasn't particularly motivated to figure this out.

Just create an empty column if you want. Call it name and default it
to blank. Or, make it created_at, which brings us to...

(b) Having the carts table reference sessions allows the you to
cascade deletes from sessions to the carts table (and on to the
cart_items table as well). This is handy because then you're able to
easily clean up stale carts when you're cleaning out stale sessions.

In your session maintenance script, just add
Cart.destroy_all ["created_at < ?", 24.hours.ago]

However, I must say it's been an uphill battle, including issues such
as those raised by I.E. Smith-Heisters in another response, and
figuring out a way to preload session data before creation of
fixtures during testing. Learned more about the backend, but I
haven't been overly satisfied with the result.

Well yeah it's been an uphill battle. It should be - you're doing
things the wrong way.

I think I'm just going to go back and add a cart_created_at column to
the carts table to work around ActiveRecord's inability to deal with
the situation where there's only a single attribute. I'm too tired
right now to consider patching ActiveRecord. Probably the simplest
all-around solution.

ooh. You almost got the right solution :slight_smile:

I wish you had piped up a couple of days ago :slight_smile: I'd love to hear your
opinions on the two points I raised above. I'm sure I'm missing
something.

Thanks for your response!

Michael Glaesemann
grzm seespotcode net

PS This makes me think I should move away from ActiveRecordStore
entirely to prevent myself from considering something like this in
the future. :slight_smile:

No, just don't consider stuff like this. ActiveRecordStore is an
implementation detail for your sessions, but your app code doesn't
care about it. Basically you set it up and then forget all about it.

Just create an empty column if you want. Call it name and default it
to blank.

Again, that's meaningless. No more meaningful than a table of integers.

In your session maintenance script, just add
Cart.destroy_all ["created_at < ?", 24.hours.ago]

It'd be a little more involved than that, as what you really want to look at is the last time the cart has been modified. I'd want to know the last time the cart was active: when items were last added or deleted. Maybe even the last time it was opened. This is all taken care of in the updated_at column of the sessions table. Adding the proper update code in either my app or the database is repeating myself, and prone to error.

Yes, ActiveRecord/Rails is opinionated, so I'll just hold my tongue now.

PS This makes me think I should move away from ActiveRecordStore
entirely to prevent myself from considering something like this in
the future. :slight_smile:

No, just don't consider stuff like this. ActiveRecordStore is an
implementation detail for your sessions, but your app code doesn't
care about it. Basically you set it up and then forget all about it.

This was partly motivated by what I've heard about the cookie-session store. I've heard it's faster, and recently Ara Howard has been doing some research showing that database-backed sessions in Rails slowly leak memory.

Michael Glaesemann
grzm seespotcode net

> Just create an empty column if you want. Call it name and default it
> to blank.

Again, that's meaningless. No more meaningful than a table of integers.

> In your session maintenance script, just add
> Cart.destroy_all ["created_at < ?", 24.hours.ago]

It'd be a little more involved than that, as what you really want to
look at is the last time the cart has been modified. I'd want to know
the last time the cart was active: when items were last added or
deleted. Maybe even the last time it was opened. This is all taken
care of in the updated_at column of the sessions table.

It's also taken care of by an updated_at column in the Cart table.
Rails automatically updates that whenever you save the cart. If you
want whenever it was last accessed, you can do a before_filter that
updates the cart every time the page loads.

Adding the proper update code in either my app or the database is repeating
myself, and prone to error.

Hrm. Prone to error in relation to what? Because it'll work a
loooooooooot better than the solutions you've discussed in this
thread.

Yes, ActiveRecord/Rails is opinionated, so I'll just hold my tongue now.

>> PS This makes me think I should move away from ActiveRecordStore
>> entirely to prevent myself from considering something like this in
>> the future. :slight_smile:
>
> No, just don't consider stuff like this. ActiveRecordStore is an
> implementation detail for your sessions, but your app code doesn't
> care about it. Basically you set it up and then forget all about it.

This was partly motivated by what I've heard about the cookie-session
store. I've heard it's faster, and recently Ara Howard has been doing
some research showing that database-backed sessions in Rails slowly
leak memory.

Bottom line, what you're trying to do is retarded. Anyone who has a
clue about Rails will confirm that.

Pat

It's also taken care of by an updated_at column in the Cart table.
Rails automatically updates that whenever you save the cart.

Thanks for reminding me. I tend to think in terms of the database, where adding or deleting an item from the cart would only require an update to the cart_items table. (That would be the most efficient way to go about it from the database side as well: no need to hit the carts table.)

Bottom line, what you're trying to do is retarded. Anyone who has a
clue about Rails will confirm that.

Wow. Retarded? Perhaps not just as experienced as others so there may be areas where I might be ignorant, but retarded? C'mon, Pat. You can do better than that.

Michael Glaesemann
grzm seespotcode net

I didn't call you retarded. I said what you're trying to do is
retarded. There's nothing wrong with being inexperienced. In fact,
when I started using Rails, I tried to do the exact same thing you're
doing. I was building a shopping cart and I wanted to connect the
cart to the session. That's how I know it's so bad :slight_smile:

Sorry to use the harsh language, but I showed you a simpler, far less
error-prone, and idiomatic way to do what you want, and you're
fighting it. I wanted to heavily discourage you from using an
atrocious solution. This is no reflection on you personally, but
simply has to do with not being familiar with Rails and how the pieces
work together.

You'll find that the Rails community is incredibly nice, so when bad
words come out, you're probably doing something horribly, horribly
wrong.

Pat

I have also been playing with this same idea, of linking sessions to the
cart. Has anyone had success with this? I am also a little confused
why this would be a horrible idea if someone could explain in a little
more detail.