Rails inheritance for classes of same behaviour but different attributes - ruby-on-rails

I have been researching on the best approach for my problem which I originally had implemented as a single table inheritance but am deeply concerned about the scalability, as potentially will have thousands of columns in the table.
So the problem is I would like to have products which the methods of each are exactly the same the only difference being the attributes each one contains. It seems that in this situation that mutli-class inheritance (not supported natively in rails?) would be the best approach or some sort of polymorphic associations.
I want to work towards the following
#product.rb
Class Product < ActiveRecord::Base
attr_accessible :title .....
def to_s # some arbitrary method used by all extending classes
....
end
end
#book.rb
class Book < Product
attr_accessible :author...
end
So I want the book to inherit the methods from product and not for the product to know about the attributes required by each subclass. And if possible get all of the products through one query.
I need to know the best way of approaching this, and if I am doing it completely wrong, please note the code written above is just for example to simplify my problem.

What you can do is create a module and include it in several different models.
First, create a file in your lib directory
i.e.) my_module.rb
module MyModule
def full_name
"#{first_name} #{last_name}"
end
end
Then, make sure the module is loaded when your Rails App starts:
In config/application.rb:
config.autoload_paths += %W(#{config.root}/lib)
Finally, include it in your models:
i.e.) app/models/thing.rb
class Thing < ActiveRecord::Base
attr_accessible :first_name, :last_name
include AdditionMod
end
You can test it in the console:
#thing = Thing.create(first_name: "Awesome", last_name: "Module")
#thing.full_name
=> "Awesome Module"

Found out that I can use H-store in conjunction with postgres that allows me to have a column that contains a schema less hash that can be used with the power of postgres (for an example take a look at http://hstoredemo.herokuapp.com/)

Related

Override gem behaviour

I'm using the acts_as_bookable gem for some basic reservation/booking stuff in a Rails app, and I need to add an additional validation to the Booking model that the gem creates.
What I mean by that is, inside the gem, located at lib/acts_as_bookable/booking.rb is the following module/class:
module ActsAsBookable
class Booking < ::ActiveRecord::Base
self.table_name = 'acts_as_bookable_bookings'
belongs_to :bookable, polymorphic: true
belongs_to :booker, polymorphic: true
validates_presence_of :bookable
validates_presence_of :booker
validate :bookable_must_be_bookable,
:booker_must_be_booker
# A bunch of other stuff
end
end
Which is fine. However, I want to add an additional piece of logic that stops a booker from booking the same instance of a bookable. Basically, a new validator.
I thought I could just add a file in my /models directory called acts_as_bookable.rb and just modify the class like this:
module ActsAsBookable
class Booking
validates_uniqueness_of :booker, scope: [:time, :bookable]
end
end
But this doesn't work. I could modify the gem itself (I've already forked it to bring a few dependencies up to date, since it's a pretty old gem) but that doesn't feel like the right solution. This is logic specific to this app's implementation, and so my gut feeling is that it belongs in an override inside this specific project, not the base gem.
What am I doing wrong here? And is there a better/alternative approach that would be more suitable?
A clean way to create monkeypatches/augmentations to objects outside of your control is to create a seperate module:
module BookingMonkeyPatch
extend ActiveSupport::Concern
included do
validates_uniqueness_of :booker, scope: [:time, :bookable]
end
end
This lets you test the monkeypatch seperately - and you can "turn the monkeypatch on" by including the module:
ActsAsBookable::Booking.include(BookingMonkeyPatch)
This can be done in an initializer or anywhere else in the lifecycle.
Altough if bookable is a polymorpic assocation you need to use:
validates_uniqueness_of :booker_id, scope: [:time, :bookable_id, :bookable_type]
The uniqueness validation does not work correctly when just passed the name of an assocation as it creates a query based on database columns. This is an example of a leaky abstraction.
See:
Justin Weiss - 3 Ways to Monkey-Patch Without Making a Mess

Is an Empty Table for implementing Reverse Polymorphism and ActiveRecord::Base okay?

I have spent a lot of thought on this situation and cannot figure out what the best modeling system is:
There is a Test. A test can have a variety of of TestItems. These TestItems can (currently) consist of TrueFalseQuestions, MultipleChoiceQuestions, ShortAnswerQuestions, and TestInfo.
All of the models will implement some sort of Printable module. They will all be printable, but each model handles its printing in a different way. All models will also have a position as they are sortable in relation to all other models. All models can belong to a test.
All models of type XXXQuestion will print numbers when they print. The TestInfo will not do that.
MultipleChoiceQuestions will have Answers as children.
I have tried creating a TestItem class that uses reverse polymorphism and a shareable question module:
class TestItem < ActiveRecord::Base
belongs_to :test
belong_to :item, polymorphic: true
db_fields: :main_text, :position, :item_id, :item_type
def sort(params)
...
end
end
module QuestionPrintable
def get_print_number
...
end
def print
raise NotImplementedError
end
end
module Question
def self.included(klass)
klass.class_eval do
include QuestionPrintable
has_one :test_item, as: :item, dependent: :destroy
delegate :test, :main_text to: :test_item
end
end
end
class MultipleChoiceQuestion < ActiveRecord::Base
include Question
has_many :answers
def print
number = get_print_number
...
end
end
This would work, except that some models (like TrueFalseQuestion) would not actually expand the TestItem class. They would have no extra information in the TrueFalseQuestions table, but they would implement methods unique to TrueFalseQuestions. I realize I could also wrap a TestItem in a TrueFalseQuestion wrapper whenever it's instantiated but then I would need to store the kind of the question on the TestItem to know when to do that. So, in some sense, the TrueFalseQuestion < ActiveRecord::Base class is actually storing the kind implicitly just by existing. I don't know if that is a valid use of ActiveRecord::Base.
All the questions do share the printing features of a number (and several behaviors I anticipate needing, just not quite yet) that are not shared with other types of TestItems (i.e. TestInfo). Additionally, some Question types will store extra data right now. And I believe that all of them will store more data as this problem evolves. So I do think that abstraction is helpful. Is it okay to have an table that more or less exists to allow the implementation of a polymorphic ActiveRecord model?
Also, having the text on the TestItem prevents a crazy amount of joins to display the main text of all items for a test.
The big difficulty, is if I do this a different way (for example not having a TestItem class and just a bunch of shared modules or storing these all as TestItems with a :kind attribute), I need to start switching behavior on the class type or an attribute, and I try to avoid any code that tests on class type or has so much behavior switch based on a attribute value.
I think in general those solutions can be achieved with duck typing, which would work with my empty ActiveRecord class, but this one just has me puzzled.
EDIT:
Another solution that occurred to me, that would prevent switching on kind would be to use some sort of kind value in the TestItem and use it to create a wrapper:
class TestItem < ActiveRecord::Base
belongs_to :test
attr_accessor :main_text, :position, :kind
def wrapped_object
klass = kind.constantize
klass.new(_needed_params)
end
end
class TrueFalseQuestion # DO NOT INHERIT
attr_accessor :kind, :position
def print
...
end
end
I left out the various modules to not distract from the general solution, those can be easily implemented.
So now my potential debate is:
Empty Database Tables
Positives:
No wrappers needed
More extendable in the future
Negatives:
It's an empty table....
Possible YAGNI
Method that returns wrapped object
Positives:
Solves the immediate problem without introducing extra database tables
Allows for all the same abstractions in the previous solution
Negatives:
Relies on the kind attribute (maybe not bad in this case?)
If the domain changes this could easily become too complex to maintain

Rails: Using different model fields in ActiveRecord Concern

Quite new to Rails and have run into an issue I just can't seem to figure out.
I have 2 models, User & Post. Users will have a "name" attribute, Posts will have a "title" attribute.
In both cases, I would like to also maintain a slug that will, on before_save, convert the appropriate column to a "sluggified" version and store that as the slug. I've already got the logic I want in place and have had this working, however, I'd like to abstract the behavior into a Concern.
I cannot seem to figure out a way to set this up - mostly because of the dynamic nature of the source field. I'd like to be able to do something like the following:
class User < ActiveRecord::Base
include Sluggable
act_as_slug :name
end
class Post < ActiveRecord::Base
include Sluggable
act_as_slug :title
end
Unfortunately, no matter what I've tried on the implementation of the concern, I've run into walls.
While I'd like to know what type of implementation is possible either way, I'd also be interested in hearing if this is a good use case for concerns or not?
This seems to work, in the event anyone else is looking for an answer (definitely open to better suggestions from those with more experience). The models look as suggested in the original post.
module Sluggable
extend ActiveSupport::Concern
included do
before_save :generate_slug
class_attribute :sluggable_attribute
def generate_slug
self.sluggify(self.class.sluggable_attribute)
end
def sluggify(attribute)
# Sluggify logic goes here
end
end
module ClassMethods
def acts_as_slug(value)
self.sluggable_attribute = value
end
end
end

Ruby on Rails Activemodel with Associations

Acutally i face some hard exercises in computer science (hard for me i think, haha).
We're doing some basic stuff with Ruby on Rails an i have to open a csv file to get additional information on my 'User' model which is a normal rails scaffold.
So at the moment i open the csv file in my users_controller.rb file and search for the right row an add them to an instance variable.
But i wonder if i can write a class that acts like an ActiveRecord Model. So i change the code to use ActiveModel. But as i read in some google results, ActiveModel can't make use of ActiveRecord like associations. But it would great to have them.
So i hope you can help me. How can i provide my model with ActiveRecors like associations?
Greetings
Melanie
It's absolutely right that the CSV file should be represented as a model, as it's data.
However, trying to incorporate Active Model sounds tricky and would almost certainly require a great deal of hacking or monkey patching.
Unless you really need associations to other models, I would create a standalone class (i.e. not inheriting from ActiveRecord::Base) in the models directory, and put the logic for parsing the CSV in there:
class User
attr_accessor :name, :email, ...
def initialize(name,email,...)
# set data
end
def self.find(param_for_search)
# Parse CSV file, find line you want
# return a User instance
self.new(name, email)
end
end
I don't know exactly how your system works, but this way you can make it behave in a similar way to Active Model stuff. You can add similar class methods and each instance method represents a CSV file row.
Every time , when you are creating your own model , it is inheritance of ActiveRecord :
class Project < ActiveRecord::Base
attr_accessible :content, :name, :user
end
Then you can tell your model to have many (let's say) Project's Tasks , which creates an association . Please , provide an example of your app's logic.
Here is a quote from RailsCasts.com :
"In Rails 3 the non-database functionality of Active Record is extracted out into Active Model. This allows you to cleanly add validations and other features to tableless models."
There is also a nice description how to add functionality in you model by adding modules .
I understand, that using ActiveRecord to use an non database source is difficult, but i think it would be vewy charming if i could write something like this:
user.worktimes.first.value
in my view and get the information like it is a database table. I visit railscast.com an i found a episode where this ist discussed. But i would like to digg deeper in this. Are there any further ressources i could read?
As i understand, ActiveModel does not support associations? I wonder why associations wasn't moved to ActiveModel as it is a very useful thing. :)
So here is my code, that i was working on:
User-Model:
class User < ActiveRecord::Base
attr_accessible :department_id, :name
belongs_to :department
end
Department-Model:
class Department < ActiveRecord::Base
attr_accessible :name
has_many :users
end
And here is my CSV Model, that i created:
class Worktime
attr_accessor :user_id,:date,:value
def initialize(params)
dir = Rails.root.join('app', 'models', 'worktimes.csv').to_s
source = File.open(dir,'r')
while(line=source.gets)
data = line.split(';')
if data[0] = params[:user_id] && data[1] = params[:date]
#value = data[2]
end
end
end
end
I am very thankful for your help as its my first time using rails.

Should models be namespaced in Rails?

Usually there are a lot of models in a Ruby on Rails project, so:
Is it a good practice to namespace them (in modules/folders)? What are the downsides?
EG:
Shop
category.rb
details.rb
Products
category.rb
base.rb
etc
(instead of ShopCategory, to have Shop::Category?)
Should also the controllers be namespaced in the same manner?
I've recently found this post but back from 2007 by Pratik Naik. Says there namespace in models doesn't really resemble databases. Uses something like below. Even there's a quote from DHH too.
Rails::Initializer.run do |config|
# Your existing stuff
config.load_paths << "#{RAILS_ROOT}/app/models/pets"
end
http://m.onkey.org/2007/12/9/namespaced-models
p/s: I don't know whether the post is still relevant or not, just something I found recently when I wanted namespaces in my models.
I'm doing that a lot.
So yes I think that's something you should do.
It'll be be a lot easier for you to view models if you have them subdivided in subdirectories instead of having them all in the same one.
The same recommendation is also valid for your controllers and your views.
I recommend using single table inheritance for your category model. For example:
Category < ActiveRecord::Base end
ShopCategory < Category end
ProductCategory < Category end
Shop < ActiveRecord::Base
belongs_to :shop_category
end
Product < ActiveRecord::Base
belongs_to :product_category
end
This will encapsulate commonly used category behaviour and attributes into a single model and could allow you to reuse a lot of code and have a single controller. Using namespacing only makes sense to me when the underlying classes have some sort of data/functionality in common. (example: acts_as_versioned creates a Version class namespaced under the model)

Resources