#119 Session Based Model
Jul 21, 2008 | 13 minutes |
Models
If you have a lot of logic associated with the data inside a session, you'll need some central location to put this logic. See how to create a session based model in this episode.
- Download:
- source codeProject Files in Zip (93.2 KB)
- mp4Full Size H.264 Video (21.3 MB)
- m4vSmaller H.264 Video (14.2 MB)
- webmFull Size VP8 Video (35.8 MB)
- ogvFull Size Theora Video (31.4 MB)
Great as usual.
Is there someway you could set the session to expire a particular comment_id in 15 mins?
Thanks for re-pointing out that you can (and should) still have classes that are not part of Active Record. Sometimes I forget that. A Shopping Cart could be another good example.
@taelor, I agree. Maybe this should be tagged under 'Models' instead of or in addition to 'Active-record'.
@Morten it's coming soon. :)
@taelor, not sure what you mean by expiring a particular comment_id. Could you give an example?
@Karmen, fixed.
Another good one. Thanks again.
This is a pretty off-scope, but:
I have added a login_at-column to my User-model to keep track of the user-logins. However, I feel that there must be a better solution than updating the database on each and every action.
I guess sessions won't work as they are stored client side, but isn't it possible to cache the time of the last login?
However, I'm still trying to figure out how to write the changes from the cache back to database, as soon as it expires.
Maybe someone can give me a hint on this.
Excellent stuff!
Maybe you've already touched on this before (forgive me if so), but I'm curious to know what are some other scenarios where non-Active Record models are helpful?
Also, how does Rails know how to find that user_session model? Does it just load any .rb files inside the models directory?
Usually, if i use a kind of non-database-stored class, i don't put it into app/model. I prefer putting it into lib, but that's just my preference.
@Jonathan, non Active Record models are usually good when you have business logic that doesn't really fit anywhere else. This varies from app to app.
One thing to watch out for is RESTful controllers which don't map directly to an existing model. Instead of filling the controller up with the logic, make a custom model.
Edit: I forgot to mention. The model directory is included in the load paths, so that's how it can find the UserSession class.
@Rainer, that'll work. I prefer to blur the lines a bit between Active Record models and other models. What concerns me more is the type of logic which they contain - and in that sense they are very similar.
I usually keep the lib directory for logic which is not specific to my application - it has no knowledge of existing models and is usually more generic. Usually these are good candidates to move into a plugin or gem.
is there any problem with rss links?
I cannot display it
Since the session store is now saved in a cookie, wouldn't this permit any user to edit any comment added to the system if they typed the id of any comment added within the last 15 minutes into their cookie?
MySQL seems to start auto incrementing ids at 1 and increments by 1. So, it'd be fairly trivial to guess the id of the latest comment by just looking at the URL of a create comment URL.
Why not just key the comment_ids off the Rails _session_id from a Rails.cache rather than risk a privilege escalation attempt? Or, recommend switching the session store to memcached or the old PStore.
@xhan the RSS feeds are working for me. Which link specifically doesn't work?
@Tony, the cookie session store is encoded and doesn't allow users to edit it. I'm not entirely sure how it works, but I know it's secure.
One thing it doesn't do is hide the contents from the owner of the session, but as long as you don't mind them seeing what's inside the session then you don't have to worry about it. Here we don't care if they see the comment ids so a session works fine.
My bad, I never bothered to look at how the cookie store actually saved the data in the browser. Since it seems to be encrypted, my privilege escalation point was totally wrong. Thanks for clearing this up, and keep up the good work.
surely it would be cleverer to find out if there have been any comments on the post since, and if not then let them edit it... that would work far better than 15 mins IMHO. I don't know how easy that would be to do though
great podcasts though, i really love them
@Lenary, yep that would work if you like that logic better. You would just have to check if another comment has been created after the given comment with that same article_id.
What you have to be careful of is that if the user is currently editing a comment and in the meantime another user posts one. Then they can't submit their edit. Using 15 minutes tells the editor they have that amount of time guaranteed to make the change.
Its funny you should post this now. I had to come in last weekend to solve this issue for my intranet site. I ended up implementing a version of the method layed out here http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/ which uses a user info module that stores info in the mongrel thread. Can you think of any advantages / disadvantages to doing it that way? Perhaps you could compare and contrast for extra credit? :p
@Isaac, functionally it sounds like that solution works, however I'd prefer to use a sweeper instead of an observer if you need access to request specific info on a callback (such as the session).
Generally I like to keep my models (and observers) very dumb about the request details. This UserSession model is pushing that limit a bit, but since I'm not passing it directly to other models I feel it's okay.
This is a great video, and I think it's good to avoid requiring user logins whenever possible.
- Depending on the number of comments stored in the session, it might be worth replacing the Array of comment ids with a Set.
- While it looks cleaner, moving the initialization of session into 'initialize' rather than on-demand will end up giving every visitor to your site a session, most of which will be empty.
- Could you simply make a helper module that is included in the controller and exposed to the view? It has direct access to session, and could privately define UserSession to avoid the HTTP/Model layering debate.
I didn't know you could have non-ActiveRecord based models.
Could you please do a screencast on how to make plugin/gemplugins?
Ryan,
I'm still trying to get my head around the advantages of storing the session in a cookie instead of on the server... I guess it allows to have multiple servers running with no state at all (since the state is completely stored on the client)... what other reason you'd see for using the cookie session store?
@Matthew, great points.
1. Yeah, a Set could be better since the order isn't important. I was also considering storing just the last comment id.
2. I was wondering about this. Isn't a session automatically created anyway (for the flash message)? An empty array would be very small but I suppose could build up.
3. A helper module could work and may be better for this simple example. But if things get more complex I think having its own class will scale better. This way it can have instance variables outside of what is in the session, and you can have private methods, etc and not have to worry about conflicting with existing controller methods.
@Bryce, check out episode #33. It's pretty old, but most of it still applies. I hope to do a new one on gems sometime in the future.
@Jean-Marc, as you mentioned having shared state across multiple servers is one of the things it solves. Another reason is that sessions are a pain to manage. You need to set up a separate cron job or something to remove old sessions. I think storing sessions in Memcachd is probably preferred, but not everyone has that setup.
I noticed some really unimportant detail while watching this brilliant episode: How comes your example post has the post id 53582982? That must be a really vast example application ;)
@klickverbot:
Still on the "noticed unimportant details", its funny how the screencast jumps through time. It goes to show that either:
a) Ryan's clock moves faster than ours.
b) Ryan's editing work is very good (I never notice it)
If you look at the timestamp on his comments on the screencast, they are spread over more than 1 hour, and yet the screencast is a little over 13min.
Ryan,
I looked at the actual cookie... call me stupid if you want but I could not make heads or tails of the encoding used to get the info in there.
having access to the actual data of the session would allow some interesting behaviors on the client site (like displaying a different set of links when a user_id is available)
(forget the heads and tails.... I just found it... before the "-" the information is encoded in BASE64... should be easier to parse at that point)
@klickverbot, I generated the models from fixtures, which if you don't specify an id, generates one based on the name of the fixture record. That's what you're seeing there.
@Felipe, heh, I record the show in very tiny segments (usually less than a minute each) and edit it all together. Recording a full screencast in one go is very difficult and usually ends up being more work than editing.
Awesome screen cast, thanks!
I'm curious: why didn't you create/edit/update the comments through the user model association:
@user.comments.build
@user.comments.find(params[:id])
Seems like this might have simplified some of the logic in you UserSession model.
@Steven, there isn't a User model in this app. We have a UserSession model, but that's not based on Active Record so you can't build associations through it.
Dude, Ryan Bates. You are the man. That was AWESOME. I learned so much in that action-packed video. Thanks so much for the free Rails University courses. You are the man! I hope Obama nominates you for the position of vice-president, so that the entire nation will learn how to bitch-slap rails code like you!!
IMO the comment should know if it can be edited ( http://pragmatig.wordpress.com/2008/06/29/separate-rights-management-from-controllers/ ) and then link_to_edit(comment) ( http://pragmatig.wordpress.com/2008/07/09/generic-smart-link_to_s-link_to_edit-link_to_destroy/ )
Hi Ryan,
I can't get my head around something. When providing a new object to your session... why not just override the session method on the controller? This way no matter if you wrote the code or not you could make sure you are controlling the usage of the session data.
Any thoughts on that?
Jean-Marc
@grosser: yes you'd think that "the comment should know if it can be edited", but remember that in this case we're talking about *anonymous* comments, so their "ownership" is really tied to the session.
@ Ryan Bates Jul 21, 2008 at 12:01
Quote: Instead of filling the controller up with the logic, make a custom model.
I don't agree with this statement you've made Ryan. Your rails casts are great, I really like the effort you put into the rails community, but business logic shouldn't be near the models. Imo, they are strictly for the storage and handling of data, nothing more. If you want to keep the controller free of business logic (which is awesome), custom modules.etc. I find better suit this requirement. Otherwise you're mixing the "roles" of MVC.
Keen to hear your thoughts :)
@kirk bushell said:
"...business logic shouldn't be near the models. Imo, they are strictly for the storage and handling of data, nothing more."
The Model-View-Controller pattern is:
Model: data; business rules to manipulate data.
View: user interface.
Controller: manage communication to model of user actions.
See http://en.wikipedia.org/wiki/Model-view-controller
Business logic does belong in the model. You can only "handle" (to use your term) data according to business rules. For example, if you are displaying a record (handling data) you need to know whether the current user should be able to view that record (even if the answer is always "yes", because the site is publicly accessible). So, there's a business rule associated with handling the data.
@chris: Good point, I can certainly see that -some- business logic can be applied in the model. Given your example however, wouldn't this be applied in the controller or view? (Unless of course it's a query limitation)
First, I must say this tip is really interesting. Anyway, I have some problems with partials, they don't seem to be able to access the helper method, for a reason I don't know :(
@me : Now I can access the helper in partials. Sounds like you CAN'T use a helper (or method) similar to a model Class (or any class, I didn't test all night).
So, you can't use a method :
def user
@user ||= SessionUser.new(session)
end
helper_method :user
This will work for standard views, but not in partials (user is nil) :(
Thank you so much for this Railscast. It is exactly what I have been searching for!
Ryan,
Nice post, I have an issue here.
The caching does not seem to work for the @user_session in the ApplicationController
Each time I reload the page it is re-initialized (the __new__ method is executed) and I cannot figure out why. Is it because I'm working on the development environment ?
Thanks for your help.
I used this trick a lot since you first published it.
Every time I needed some smart behaviour with stuff stored into a session I moved it into the session object.
- storing only the user_id in the session
- let the session retrieve the user and cache it
- sign out the user if he hasn't browsed for a few hours.
I am wondering how would you approach making the session smarter that could play well with rails 3 and warden.
My first thought is to include a SessionSmart module into the session hash, the second one to post about it here.
I love these patterns to dry up my code! - thanks
Hi Ryan,
Thanks for the episode. I am wondering how can I create the session model object from the console. I tried new_session but not able to initialize session model with that.
Ritesh