STI with many types - ruby-on-rails

I have a rails 4 app with STI models:
# models/person.rb
def Person < ActiveRecord::Base
end
# models/director.rb
def Director < Person
end
# models/actor.rb
def Director < Person
end
But because one person can be an actor and an director simultaneously, I want STI with many types like:
person = Person.first
person.type = "Director, Actor"
person.save
Actor.first.id => 1
Director.first.id => 1
Is there mechanism in rails or gem for realize this?

Rails does not support this and I'm not aware of any gems that support this as described (i.e. multiple subclass names in the type column).
There is gem at https://github.com/mhuggins/multiple_table_inheritance which uses separate tables for the subclasses and you can always use mixins as an alternative to inheritance.

I believe the more Rails idiomatic way to do something similar would be via scopes, which would allow you to do:
person = Person.first
person.position = 'Director, Actor'
person.save
person.directors.first.id => 1
person.actors.first.id => 1
And you would just have to define a pair of scopes in your Person class:
scope :actors, -> { where('position like ?', '%Actor%') }
scope :directors, -> { where('position like ?', '%Director%') }
You would lose the ability to do person.is_a? with this, but Ruby doesn't really do multiple inheritance in such a way as to allow #is_a? to return true when passed sibling classes anyway. You can also get effectively similar functionality with a simple test method:
def is_actor?
self.position =~ /Actor/
end
def is_director?
self.position =~ /Director/
end
EDIT: I haven't done a lot of Rails 4, so my scope syntax MAY not be right, I just glanced at the docs. The principle should be sound, though.

Thank to all answerers above!
I found solution that most appropriate for me:
I've created hmt association Person-ProfessionsPerson-Profession and leave descendants for Person class (Director and Actor).
# models/profession.rb
Profession < ActiveRecord::Base
has_many :professions_people, dependent: :destroy
has_many :people, through: :professions_people
end
# models/person.rb
def Person < ActiveRecord::Base
has_many :professions_people, dependent: :destroy
has_many :professions, through: :professions_people
end
# models/director.rb
def Director < Person
include PeopleFromProfession
end
# models/actor.rb
def Actor < Person
include PeopleFromProfession
end
I've seed 2 professions with column "class_type" (which should not change in app's work) "Actor" and "Director"
I've also add concern PeopleFromProfession for share some code:
# models/concerns/actor.rb
module PeopleFromProfession
extend ActiveSupport::Concern
included do
default_scope { includes(:professions).where(professions: {class_type: self.name}) }
after_create :create_join_table_record
end
module ClassMethods
def model_name
Person.model_name
end
end
private
def create_join_table_record
self.professions << Profession.where(class_type: self.class.name).first
end
end
default_scope is for scoping only people with specific profession, create_join_table_record callback is monkey-patch for create missed join table record.
Class method model_name was overwriting for purposes, that covered here Best practices to handle routes for STI subclasses in rails
If you will find some problems in that approach, please tell me.

Related

Rails namespace class same as existing active_record model

I'm having a bit trouble with the namespaces in Rails 4.
I have ActiveRecord models Shop, Order, and OrderItem
# model/shop.rb
class Shop < ActiveRecord::Base
# model/order.rb
class Order < ActiveRecord::Base
has_many :order_items
# model/order_item.rb
class OrderItem < ActiveRecord::Base
belongs_to :orderable, polymorphic: true
belongs_to :order
I'm replicating the relationship between Order and OrderItem in a namespace like this
# model/shop/order.rb
class Shop::Order
attr_accessor :order_items
def initialize
self.order_items = []
self.order_items << Shop::OrderItem.new
end
# model/shop/order_item.rb
class Shop::OrderItem
attr_accessor :orderable_type, :orderable_id
def initialize(params = {})
if params
self.orderable_type = params['orderable_type'] if params['orderable_type']
self.orderable_id = params['orderable_id'] if params['orderable_id']
end
end
def price
orderable.price
end
def orderable
orderable_type.constantize.find_by(id: orderable_id)
end
def to_h
Hash[
orderable_type: self.orderable_type,
orderable_id: self.orderable_id,
price: self.price
]
end
end
So my problem is that when I initialize Shop::Order.new, sometimes its order_items is an array of OrderItems instead of Shop::OrderItems, and when I test it in the controller, if I type Shop::OrderItem, it will return OrderItem.
I'm wondering if Shop::OrderItem wasn't initialized before OrderItem and cause the issue?
You are running into a namespace collision. Depending on where the code is executing, Shop could be the ActiveRecord model that you've defined in models/shop.rb, or it could be the module namespace that you've defined under models/shops/*.rb. Not only will this cause unpredictable execution, it's also confusing to read.
I recommend using a module namespace other than "Shop". Even calling it "MyShop" would be an improvement. However you'll probably still run into naming collisions between Shop and MyShop::Shop. You should probably rename the Shop class under the MyShop module to avoid this:
For example:
# model/my_shop/my_order.rb
class MyShop::MyOrder
# ...
end
# model/my_shop/my_order_item.rb
class MyShop::MyOrderItem
# ...
end
Having said all that, I feel like you're setting yourself up for a world of hurt. This problem might be better solved using service objects. Google up "Rails Service Objects" for some really good examples.

How to hide records, rather than delete them (soft delete from scratch)

Let's keep this simple. Let's say I have a User model and a Post model:
class User < ActiveRecord::Base
# id:integer name:string deleted:boolean
has_many :posts
end
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
end
Now, let's say an admin wants to "delete" (hide) a post. So basically he, through the system, sets a post's deleted attribute to 1. How should I now display this post in the view? Should I create a virtual attribute on the post like this:
class Post < ActiveRecord::Base
# id:integer user_id:integer content:string deleted:boolean
belongs_to :user
def administrated_content
if !self.deleted
self.content
else
"This post has been removed"
end
end
end
While that would work, I want to implement the above in a large number of models, and I can't help feeling that copy+pasting the above comparative into all of my models could be DRYer. A lot dryer.
I also think putting a deleted column in every single deletable model in my app feels a bit cumbersome too. I feel I should have a 'state' table. What are your thoughts on this:
class State
#id:integer #deleted:boolean #deleted_by:integer
belongs_to :user
belongs_to :post
end
and then querying self.state.deleted in the comparator? Would this require a polymorphic table? I've only attempted polymorphic once and I couldn't get it to work. (it was on a pretty complex self-referential model, mind). And this still doesn't address the problem of having a very, very similar class method in my models to check if an instance is deleted or not before displaying content.
In the deleted_by attribute, I'm thinking of placing the admin's id who deleted it. But what about when an admin undelete a post? Maybe I should just have an edited_by id.
How do I set up a dependent: :destroy type relationship between the user and his posts? Because now I want to do this: dependent: :set_deleted_to_0 and I'm not sure how to do this.
Also, we don't simply want to set the post's deleted attributes to 1, because we actually want to change the message our administrated_content gives out. We now want it to say, This post has been removed because of its user has been deleted. I'm sure I could jump in and do something hacky, but I want to do it properly from the start.
I also try to avoid gems when I can because I feel I'm missing out on learning.
I usually use a field named deleted_at for this case:
class Post < ActiveRecord::Base
scope :not_deleted, lambda { where(deleted_at: nil) }
scope :deleted, lambda { where("#{self.table_name}.deleted_at IS NOT NULL") }
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
destroy
end
def deleted?
self.deleted_at.present?
end
# ...
Want to share this functionnality between multiple models?
=> Make an extension of it!
# lib/extensions/act_as_fake_deletable.rb
module ActAsFakeDeletable
# override the model actions
def destroy
self.update(deleted_at: DateTime.current)
end
def delete
self.destroy
end
def undestroy # to "restore" the file
self.update(deleted_at: nil)
end
def undelete
self.undestroy
end
# define new scopes
def self.included(base)
base.class_eval do
scope :destroyed, where("#{self.table_name}.deleted_at IS NOT NULL")
scope :not_destroyed, where(deleted_at: nil)
scope :deleted, lambda { destroyed }
scope :not_deleted, lambda { not_destroyed }
end
end
end
class ActiveRecord::Base
def self.act_as_fake_deletable(options = {})
alias_method :destroy!, :destroy
alias_method :delete!, :delete
include ActAsFakeDeletable
options = { field_to_hide: :content, message_to_show_instead: "This content has been deleted" }.merge!(options)
define_method options[:field_to_hide].to_sym do
return options[:message_to_show_instead] if self.deleted_at.present?
self.read_attribute options[:field_to_hide].to_sym
end
end
end
Usage:
class Post < ActiveRecord::Base
act_as_fake_deletable
Overwriting the defaults:
class Book < ActiveRecord::Base
act_as_fake_deletable field_to_hide: :title, message_to_show_instead: "This book has been deleted man, sorry!"
Boom! Done.
Warning: This module overwrite the ActiveRecord's destroy and delete methods, which means you won't be able to destroy your record using those methods anymore. Instead of overwriting you could create a new method, named soft_destroy for example. So in your app (or console), you would use soft_destroy when relevant and use the destroy/delete methods when you really want to "hard destroy" the record.

Detect changes on existing ActiveRecord association

I am writing an ActiveRecord extension that needs to know when an association is modified. I know that generally I can use the :after_add and :after_remove callbacks but what if the association was already declared?
You could simply overwrite the setter for the association. That would also give you more freedom to find out about the changes, e.g. have the assoc object before and after the change E.g.
class User < ActiveRecord::Base
has_many :articles
def articles= new_array
old_array = self.articles
super new_array
# here you also could compare both arrays to find out about what changed
# e.g. old_array - new_array would yield articles which have been removed
# or new_array - old_array would give you the articles added
end
end
This also works with mass-assignment.
As you say, you can use after_add and after_remove callbacks. Additionally set after_commit filter for association models and notify "parent" about change.
class User < ActiveRecord::Base
has_many :articles, :after_add => :read, :after_remove => :read
def read(article)
# ;-)
end
end
class Article < ActiveRecord::Base
belongs_to :user
after_commit { user.read(self) }
end

Extending Active Record to reduce redundancy

Suppose I have two classes in a Rails application:
class Subject < ActiveRecord::Base
def children?
Subject.where(:parent_id => self.id).length > 0
end
def children
Subject.where(:parent_id => self.id)
end
end
class Region < ActiveRecord::Base
def children?
Region.where(:parent_id => self.id).length > 0
end
def children
Region.where(:parent_id => self.id)
end
end
What would be the best way to reduce the redundant class methods? Would I extend ActiveRecord with two new methods? If so, how could I write those two new methods to be available for both classes?
Thanks,
Mike
Actually what are you dealing with is has_many association.
DRY principle is very good one, but not for this case. You want to extract very simple and native stuff out off model, while it will complicate main picture.
So you can just refactor a little
class Subject < ActiveRecord::Base
has_many :children, :class_name => "Subject", :foreign_key => :parent_id
def children?
children.present?
end
end
Have a look at acts_as_tree to see what it does or use it as it looks like you are trying to perform the same tasks.
if you are dealing with small objects a quick fix is to create 2 generic methods in your application helper file:
def children?(myObject)
myObject.where(:parent_id => myObject.id).length > 0
end
def children(myObject)
myObject.where(:parent_id => myObject.id)
end
Edit:
for anything more resource intensive you can define a method in ActiveRecord::Base since they both inherit from it.
def children?
self.where(:parent_id => self.id).length > 0
end
def children
self.where(:parent_id => self.id)
end
I agree with #abdollar's suggestion for using acts_as_tree. Or you could just create an association onto the same table (which is what acts_as_tree does).
If you wanted to roll your own using those methods you've given as examples, you could create a module in lib which you can require and include in your models...
# lib/children.rb
module Children
def children
self.class.where(:parent_id => self.id)
end
def children?
children.present?
end
end
# app/models/subject.rb (or any other model)
require 'children'
class Subject < ActiveRecord::Base
include Children
end
If you're using rails 2, you won't need to do require 'children' since lib is autoloaded. If you're using rails 3, you could put that into an initializer or something to clean it up.

ActiveRecord Problems using callbacks and STI

Hey folks, following problem with Rails and STI:
I have following classes:
class Account < AC::Base
has_many :users
end
class User < AC::Base
extend STI
belongs_to :account
class Standard < User
before_save :some_callback
end
class Other < User
end
end
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
type.new(*args, &block)
end
end
end
And now the problem:
Without rewriting User.new (in module STI), the callback inside User::Standard gets never called, otherwise the account_id is always nil if I create users this way:
account.users.create([{ :type => 'User::Standard', :firstname => ... }, { :type => 'User::Other', :firstname => ... }])
If I'm using a different approach for the module like:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
Then instance variables are not shared, because it's creating a new object.
Is there any solution for this problem without moving the callbacks to the parent class and checking the type of class?
Greetz
Mario
Maybe there's something I don't know, but I've never seen Rails STI classes defined in that manner. Normally it looks like...
app/models/user.rb:
class User < AC::Base
belongs_to :account
end
app/models/users/standard.rb:
module Users
class Standard < User
before_save :some_callback
end
end
app/models/users/other.rb:
module Users
class Other < User
end
end
It looks as though you are conflating class scope (where a class "lives" in relation to other classes, modules, methods, etc.) with class inheritance (denoted by "class Standard < User"). Rails STI relationships involve inheritance but do not care about scope. Perhaps you are trying to accomplish something very specific by nesting inherited classes and I am just missing it. But if not, it's possible it's causing some of your issues.
Now moving on to the callbacks specifically. The callback in Standard isn't getting called because the "account.users" relationship is using the User class, not the Standard class (but I think you already know that). There are several ways to deal with this (I will be using my class structure in the examples):
One:
class Account
has_many :users, :class_name => Users::Standard.name
end
This will force all account.users to use the Standard class. If you need the possibility of Other users, then...
Two:
class Account
has_many :users # Use this to look up any user
has_many :standard_users, :class_name => Users::Standard.name # Use this to look up/create only Standards
has_many :other_users, :class_name => Users::Other.name # Use this to look up/create only Others
end
Three:
Just call Users::Standard.create() and Users::Other.create() manually in your code.
I'm sure there are lots of other ways to accomplish this, but there are probably the simplest.
So I solved my problems after moving my instance variables to #attributes and using my second approach for the module STI:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
class User < AR:Base
extend STI
belongs_to :account
validates :password, :presence => true, :length => 8..40
validates :password_digest, :presence => true
def password=(password)
#attributes['password'] = password
self.password_digest = BCrypt::Password.create(password)
end
def password
#attributes['password']
end
class Standard < User
after_save :some_callback
end
end
Now my instance variable (the password) is copied to the new User::Standard object and callbacks and validations are working. Nice! But it's a workaround, not really a fix. ;)

Resources