Rails initialize new object with defaults - ruby-on-rails

I have the following class:
class Question < ActiveRecord::Base
attr_accessible :choices
end
I want to initialize new objects to have 4 blank strings in an array, so choices = ['','','','']. I've tried doing it in the controller:
def new
#question = Question.new(:choices => ['','','',''])
end
That works, but it seems like this should be done in the model to promote data integrity. Is there a better way to do this?

You have several solutions. In order of preference
Create a custom method and use it whenever you need such feature
class Question < ActiveRecord::Base
attr_accessible :choices
def self.prepare
new(:choices => ['','','',''])
end
end
Use the after_initialize callback
class Question < ActiveRecord::Base
attr_accessible :choices
after_initialize :default_choices
protected
def :default_choices
self.choices ||= ['','','','']
end
end
Override choices
I encourage the first approach for several reasons
You will have more control of the code. The defaults will be assigned only when you explicitly want it. In fact, there are several cases where an instance of that record is initialized (think about tests) and the majority of time that assignment may not be required
It's a good step towards exposing a custom API that is not tightly coupled to ActiveRecord
It exposes a more maintainable approach
You will love it if your app complexity will increase

Another solution:
4 Set as default in migration
class CreateQuestions < ActiveRecord::Migration
def change
create_table :questions do |t|
t.string :choices, default: ['', '', '', '']
t.timestamps
end
end
end
but I agree the first is the best solution.
In any case do not override the initialize http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html, use the after_initialize callback described in the second solution.

Related

Reset PK number based on association

I have a Post and Comments table.
Post has many comments, and Comment belongs to a post.
I want to have primary keys which start at 1 when I create a comment for a Post, so that I can access comments in a REST-ful manner, e.g:
/posts/1/comments/1
/posts/1/comments/2
/posts/2/comments/1
/posts/2/comments/2
How can I achieve that with Rails 3?
I am using MySQL as a database.
Bonus: I am using the Sequel ORM; an approach compatible with Sequel, not only ActiveRecord, would be awesome.
Well, you can't use id for this, as id is a primary key here. What you can do is to add an extra field to your database table like comment_number and make it unique in the scope of the post:
#migration
def change
add_column :comments, :comment_number, :integer, null: false
add_index :comments, [:post_id, :comment_number], unique: true
end
#Class
class Comment < ActiveRecord::Base
belongs_to :post
validates :post_id, presence: true
validates :comment_number, uniqueness: { scope: :post_id }
end
Now with this in place you need to ensure this column is populated:
class Comment < ActiveRecord::Base
#...
before_create :assign_comment_number
private
def assign_comment_number
self.comment_number = (self.class.max(:comment_number) || 0) + 1
end
end
Last step is to tell rails to use this column instead of id. To do this you need to override to_param method:
class Comment < ActiveRecord::Base
#...
def to_param
comment_number
end
end
Update:
One more thing, it would be really useful to make this field read-only:
class Comment < ActiveRecord::Base
attr_readonly :comment_id
end
Also after rethinking having uniqueness validation on comment_number makes very little sense having it is assigned after validations are run. Most likely you should just get rid of it and rely on database index.
Even having this validation, there is still a possible condition race. I would probably override save method to handle constraint validation exception with retry a couple of time to ensure this won't break application flow. But this is a topic for another question.
Another option without changing models:
get 'posts/:id/comments/:comment_id', to: 'posts#get_comment'
And in the posts controller:
def get_comment
#comment = post.find(params[:id]).comments[params[:comment_id] -1]
end
Asumptions: Comments bookmarks might change if coments deletion is allowed.

How to inherit from models in Rails, where one type extends another without intertwining

I'm aware of the number of posts on this, but still I can't figure out how to do this. I have a model "InspirationItem", which is basically a blog posts. Now I also want a second model, "Special". Specials are like inspiration items but they have extra properties, such as an "excerpt" and a "theme". So I want to extend the "InspirationPost" model.
I've tried to create a model "Post", which both "InspirationItem" and "Special" extend, but "InspirationItem" doesn't really add any properties to. Then, I create a "has_one" relation from InspirationItem/Special and try to use "delegate" to handle all logics in the "Post" model. However this does not work like I'd expect at all.
Here's some of my code. This would be my InspirationItem:
class InspirationItem < ActiveRecord::Base
has_one :post, :as => :item
delegate :title, :title=,
:body, :body=,
:category_names, :category_names=,
:hide_from_overview, :hide_from_overview=,
:to => :post, :allow_nil => true
end
And this is a short version of post:
class Post < ActiveRecord::Base
attr_accessible :title, :body, :embed, :hide_from_overview, :visual, :thumbnail, :category_names
# All sorts of logics
end
What's important is that I don't want InspirationItem.all to return Specials too, that's why I use the Post model. I also want regular error handling to work for all models. Thanks in advance!
If you want an ActiveRecord subclass of a model, but don't want the parent to search any of the children, then something like this should work (I'll use your InspirationItem class):
class InspirationItem < ActiveRecord::Base
def self.descendants
super.reject {|klass| klass == Special}
end
end
class Special < InspirationItem
end
This is a bit hacky, but will force ActiveRecord to only return InspirationItems when you search InspirationItem.all. And this shouldn't affect validations.
EDIT: Re: What the tables would look like for this.
create_table :inspiration_items do |t|
t.string :type # needed for the Single Table Inheritance mechanism
# whatever other columns you need for InspirationItems
end

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Struct Objects with ActiveRecord methods? (Ruby on Rails)

I have a limited set of objects (20 - 30) which I need to be able to combine with ActiveRecord Objects. Putting them into the DB just seems awful because I already have two other join models hooked up to the model.
So let's say i have a class
class Thing < ActiveRecord::Base
has_many :other_things, :class_name => 'OtherThing'
end
with an existing table. How would I be able to combine this with a class not inheriting from ActiveRecord (here's my best guess)
class OtherThing < ActiveRecord::Base
OtherThing = Struct.new(:id, :name, :age, :monkey_fighting_ability)
belongs_to :thing, :class_name => 'Thing'
validate :something
def self.search_for(something)
MY_GLOBAL_HASH[something].map do |hash|
instance = OtherThing.new
hash.each_pair do |k,v|
instance.send(:"#{k}=", v)
end
instance
end
end
#if AR wants to call save
def save
return true
end
alias save save!
protected
def something
self.errors.add(:monkey_fighting_ability, 'must be unlimited') if self.class.search_for(something).empty?
end
end
Point being that I want to use ActiveRecord methods and so on without ever hitting the db. Help is greatly appreciated.
I'd suggest reading the post on "Make any Ruby Object Feel Like An Active Record" by Yehuda Katz. It goes over how to convert any object into a model-like class, without the database backing.
Good Luck!

How can I extend an object returned from an ActiveRecord association at runtime?

I have a model as follows:
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values
end
What I would like to do is to extend any value returned by a find on the property_values extension with a module that is determined by an attribute of the Property object. I've attempted something like this:
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values, :extend => PropertyUtil::Extensible
def enrich(to_extend)
modules.split(/\s*,\s*/).each do |mod|
to_extend.extend(Properties.const_get(mod.to_sym))
end
end
end
module PropertyUtil
module Extensible
def self.extended(mod)
mod.module_eval do
alias old_find find
end
end
def find(*args)
old_find(*args).map{|prop| proxy_owner.enrich(prop)}
end
end
end
Where all modules that may be selected are defined in the Properties module. In attempting to run with this code, though, there are a couple of problems; first, to my surprise, none of the dynamic finders (property_values.find_by_name, etc.) appear to delegate to find; second, something with how I've done the aliasing leads to a stack overflow when I try to run the find directly.
Is there a way to do what I'm attempting? What method can I alias and override such that all results returned by the association extension, irrespective of how they are retrieved, are extended with the appropriate modules?
Thanks, Kris
I never tried to do this but you may want to try the following (I just changed how the aliases are done):
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values, :extend => PropertyUtil::Extensible
def enrich(to_extend)
modules.split(/\s*,\s*/).each do |mod|
to_extend.extend(Properties.const_get(mod.to_sym))
end
end
end
module PropertyUtil
module Extensible
def self.extended(mod)
mod.module_eval do
alias_method :old_find, :find
alias_method :find, :new_find
end
end
def new_find(*args)
old_find(*args).map{|prop| proxy_owner.enrich(prop)}
end
end
end
If it does not work here is another idea you may wanna try:
class Value < ActiveRecord::Base
self.abstract_class = true
end
class ExtendedValue < Value
end
class ExtendedValue2 < Value
end
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values, :class_name => 'ExtendedValue'
has_and_belongs_to_many :property_values_extended, :class_name => 'ExtendedValue'
has_and_belongs_to_many :property_values_extended2, :class_name => 'ExtendedValue2'
end
The idea is to have one hatbm association per "type" (if you can group your extensions that way) and use the one you want at a given time, if you can do what you want that way I am also pretty sure it will have a smaller impact performance than patching every returned object after activerecord returned them.
I am kinda curious at what you are trying to achieve with this :)
It is much easier to simply use classes to change the functionality. You can have classes of PropertyValues with the appropriate behavior and use either STI (Single Table Inheritance) to instantiate the appropriate instance or you can over-ride the 'instantiate' ActiveRecord class method to set the class using the #becomes instance method:
class PropertyValue < AR:Base
def self.instantiate(record)
property_value = super
case property_value.sub # criteria for sub_class
when 'type1' then property_value.becomes(Type1)
when 'type2' then property_value.becomes(Type2)
end
end
end
class Type1 < PropertyValue
def some_method
# do Type1 behavior
end
end
class Type2 < PropertyValue
def some_method
# do Type2 behavior
end
end
I have found that using classes and inheritance provides much cleaner, simpler code and is easier to test.
I ended up using an after_find call on the value class to resolve this problem. This is a pretty suboptimal solution, because it means that the module information ends up needing to be duplicated between the property referent and the value, but it's workable, if less than exactly performant. The performance hit ended up being large enough that I had to cache a bunch of data in the database with the results of computations over large numbers of properties, but this turned out not to be all bad, in that it simplified the process for extraction of report data considerably.
In the end, here are some bits of what I ended up with:
module Properties::NamedModules
def modules
(module_names || '').split(/\s*,\s*/).map do |mod_name|
Property.const_get(mod_name.demodulize.to_sym)
end
end
end
module Properties::ModularProperty
def value_structure
modules.inject([]){|m, mod| m + mod.value_structure}.uniq
end
end
module Properties::Polymorphic
include NamedModules, ModularProperty
def morph
modules.each {|mod| self.extend(mod) unless self.kind_of?(mod)}
end
end
class Property < ActiveRecord::Base
include Properties::NamedModules, Properties::ModularProperty
has_and_belongs_to_many :property_values, :join_table => 'property_value_selection'
def create_value(name, value_data = {})
property_values.create(
:name => name,
:module_names => module_names,
:value_str => JSON.generate(value_data)
)
end
end
class PropertyValue < ActiveRecord::Base
include Properties::Polymorphic
has_and_belongs_to_many :properties, :join_table => 'property_value_selection'
after_find :morph
end

Resources