mr_muscle
mr_muscle

Reputation: 2900

Rails model referring to new fake model

I've got below model structure where I stored Books and SellingInfo (which represents presale and after sale costs):

class Books < ApplicationRecord
  STATUSES = %w[planned purchased].freeze

  has_many :selling_infos, dependent: :destroy
end

class SellingInfo < ApplicationRecord
  belongs_to :book
end

Because of ActiveAdmin requirements I want to have new relation to a model which will be the same as selling_infos table (the same columns, types etc.) but I'll use it only when Book.status == 'purchased'. In order not to overload the database I don't want to create new model, let's say purchased_selling_info, with the same data structure but create some kind of 'fake model' and use it as purchased_selling_info.

I was trying to add something like below to Book model:

# models/book.rb
  has_one :purchased_selling_info,
          -> { where(status: 'purchased') },
          class_name: 'SellingInfo',
          dependent: :destroy

But when I'm trying check if it works in rails console I'm getting an error:

2.7.2 :009 > Book.purchased_selling_info.size

NoMethodError (undefined method `purchased_selling_info' for #<Class:0x00007f9916f33200>)

Upvotes: 1

Views: 305

Answers (1)

max
max

Reputation: 102240

The solution to storing records with different behaviors in one table is Single Table Inheritance. By adding a type column you can have different assocations:

class Book < ApplicationRecord
  has_many :selling_infos, dependent: :destroy
end

class PurchasedBook < Book 
  has_one :purchased_selling_info,
          class_name: 'SellingInfo',
          dependent: :destroy
end

When you load the records from the DB ActiveRecord reads the inheritance column (which is type by default) and will initialize that class.

If you don't want to use STI you can restrict the creation of purchased_selling_info either through custom validations or association callbacks.

class SellingInfo
  validates :validates_is_purchased, if: ->{ |si| si.purchased? }

  def validates_is_purchased
    errors.add(:book, 'wrong book type')
  end
end

What you're currently doing wont work since:

  1. Adding a lambda to an assocation just applies a filter to the assocation. Associations are class level and you can't make them dependent on the attributes of an instance.
  2. Book.purchased_selling_info.size gives a NoMethod error since you're calling an instance method on the class.

In order not to overload the database I don't want to create new model, let's say purchased_selling_info, with the same data structure but create some kind of 'fake model' and use it as purchased_selling_info.

You should carefully weigh "overloading the database" against the added complexity and mainence cost of a hacky solution. Sounds like a case of premature optimization to me.

Upvotes: 2

Related Questions