#121
Aug 04, 2008

Non Active Record Model

This episode will show you how to make a model which isn't based on Active Record. You may want to do this if a resource isn't backed by the database.
Tags: models
Download (18.8 MB, 12:51)
alternative download for iPod & Apple TV (14.8 MB, 12:51)
# models/letter.rb
class Letter
  attr_reader :char
  
  def self.all
    ('A'..'Z').map { |c| new(c) }
  end
  
  def self.find(param)
    all.detect { |l| l.to_param == param } || raise(ActiveRecord::RecordNotFound)
  end
  
  def initialize(char)
    @char = char
  end
  
  def to_param
    @char.downcase
  end
  
  def products
    Product.find(:all, :conditions => ["name LIKE ?", @char + '%'], :order => "name")
  end
end

# letters_controller.rb
def index
  @letters = Letter.all
end

def show
  @letter = Letter.find(params[:id])
end
<!-- letters/index.html.erb -->
<% title "Select a Letter" %>

<% for letter in @letters %>
  <%= link_to letter.char, letter %>
<% end %>

<!-- letters/show.html.erb -->
<% title "Products for Letter #{@letter.char}" %>

<p><%= link_to "Select a Letter", letters_path %></p>

<%= render :partial => @letter.products %>

Full Source Code

RSS Feed for Episode Comments 25 comments

1. Eric Duminil Aug 04, 2008 at 07:23

Nice!

Note that if no model uses ActiveRecord in your app, you can add

<<<<<<<<<<<<<<
Rails::Initializer.run do |config|
  config.frameworks -= [:active_record ]
end
>>>>>>>>>>>>>>>

to your environment.rb

And what about caching Letter.all with

<<<<<<<<<<<<<<
 def self.all
    @@all||=('A'..'Z').map { |c| new(c) }
  end
>>>>>>>>>>>>>>>

?


2. cbmeeks Aug 04, 2008 at 07:28

Awesome as always!


3. Tim F Aug 04, 2008 at 07:29

How about using an anonymous scope for the products method. That way products would behave even more like an ar association.

Thanks for the great screencasts. Alway looking forward to Mondays.


4. Shuaib Zahda Aug 04, 2008 at 07:32

Thanks, it is great. in fact i need it in my application WebVZ


5. Ryan Bates Aug 04, 2008 at 08:04

@Eric, yep, caching the "all" method is a possibility. But I tend to not add this kind of caching up front unless I know for sure it is worth it.

Caching usually comes with some hidden costs. I couldn't, for example, cache the "products" method as well because that will result in all products staying in memory for my entire app.

If you determine adding a global cache is worth it (with @@all) then it's best to call "freeze" on each object in the cache. This way you ensure the state of the object doesn't change.

@Tim, that's a great idea. Using scopes instead of a simple find adds a lot of potential.


6. Kenneth Kalmer Aug 04, 2008 at 09:03

Good way to do it, just prompted me to update my activerecord-tableless-models plugin. Find the plugin at http://github.com/kennethkalmer/activerecord-tableless-models


7. Steven Aug 04, 2008 at 10:37

@Kenneth,

Here's what I did for quick and dirty validations without DB backing:

http://pastie.org/247701

[Admin Edit: moved to pastie]


8. Adam Hill Aug 04, 2008 at 20:50

Great stuff & good timing for a current project :)


9. Rob Anderton Aug 05, 2008 at 00:52

I'm looking forward to the day when ActiveModel (http://github.com/rails/rails/tree/master/activemodel) is complete and stable enough to use: it'll be good to create tableless models that can still use the cool stuff like validations and observers.

Until that day though...thanks for the screencast Ryan.

ps. oooh a preview button :D


10. Ryan Bates Aug 05, 2008 at 07:13

@Rob, I'm looking forward to Active Model as well. I think it will have a big impact in how we work with these kinds of models. I was hesitating to make this episode yet for that very reason, but figured it will be a while before Active Model is released.


11. Clemens Kofler Aug 05, 2008 at 11:46

Finally ... I've seen questions regarding this stuff at Railsforum and WorkingWithRails. Now, instead of explaining it myself, I can link to this screencast! ;-)

Small suggestion: You override the to_param method which is really cool. However, if you do that, you might as well override the to_s method for the Letter class because I think it's a really underused feature in Rails (and probably Ruby in general).


12. Ryan Bates Aug 05, 2008 at 15:52

@Clemens, I considered defining to_s here too, but decided not to. Although it may work well in this example, for the majority of cases I don't think it does. There are often many different ways to represent a model as a string.

I prefer to be explicit each time so there's no confusion as to how it's being displayed. For example, let's say we had a Month model which represented a specific month of the year. How would you display this as a string? The full month name? The abbreviated version? Would you include the year? etc.

I think this decision of how to represent a model as a string belongs in the view, not the model.


13. JR Aug 05, 2008 at 18:47

Ryan, thanks a bunch for this (and all screencasts).

I had a case similar to this in an app i'm working on and this is how i originally designed it.

But I was hesitant at first to have ActiveRecord methods inside a non-ActiveRecord class ... somehow it just didn't seem right. i thought the find methods should have been helpers in the controller, but this seems to make more sense.


14. pedz Aug 07, 2008 at 05:59

I discovered a few weeks ago that if you use a form_for for these models, you need to define the "id" method or you get nasty-grams in your web server log file.


15. Andy Ferra Aug 07, 2008 at 14:53

I think that the letters should be factored out of the 'all' method.

For example:
http://pastie.textmate.org/249561

What do you think Ryan?


16. Ryan Bates Aug 07, 2008 at 16:13

@Andy, what is the reasoning behind moving the letters into their own method? Is it for self documentation? I think the "all" method is simple enough to not warrant further refactoring.

However, if you need to add more characters to the "all" method in addition to alphabetical letters, then I would say refactoring out just those letters is worth it.


17. Andy Ferra Aug 07, 2008 at 19:28

@Ryan, it just feels a little more clear to me. To me, "all" isn't the dataset, the collection of letters is.

Also, I tend to preemptively factor things out that I know I'm going to refer to in a lot of different places.

Maybe, I'm getting ahead of the implementation but, I could easily see it becoming appropriate as the class grows. So why not just do it up front?


18. Cassiano Sep 15, 2008 at 07:18

Ryan,

There is a missing '=' before 'render' in:

http://github.com/ryanb/railscasts-episodes/tree/master/episode-121/store/app/views/products/index.html.erb

so instead of:

<% render :partial => @products %>

it should read:

<%= render :partial => @products %>

I have also noticed that the 'log' folder is not present, so it needs to be created manually (not a big deal, but providing it would avoid an unnecessary error when trying to boot up the development server).


19. Ryan Bates Sep 20, 2008 at 11:36

@Cassiano, thanks for pointing out this error! I'll update the code.


20. Ryan Heneise Oct 02, 2008 at 11:57

I just created a plugin called PassiveRecord that makes a lot of this stuff very easy. Check it out on Github: http://github.com/artofmission/passiverecord


21. wotlk cd key Nov 13, 2008 at 01:19

this help me so much, stole it :P


22. wow gold Nov 19, 2008 at 19:56

OK,it is good.


23. cheap Perfect World Gold Nov 20, 2008 at 18:53

Thanks Ryan,I think this is one of the most wonderful sites. I have great admiration for you.


24. NosTale online gold Nov 24, 2008 at 23:38

I have great admiration for you.


25. lily Nov 27, 2008 at 20:40

Thank you Ryan, your screencast is good. Please look at our URL, if necessary we can learn from each other.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player