0

I am trying the following - I have Models: Tales, Books, Keywords

class Tale < ActiveRecord::Base
has_many :tale_culture_joins
has_many :cultures, through: :tale_culture_joins
has_many :tale_purpose_joins
has_many :purposes, through: :tale_purpose_joins
has_many :tale_book_joins
has_many :books, through: :tale_book_joins
has_many :tale_keyword_joins
has_many :keywords, through: :tale_keyword_joins

class Book < ActiveRecord::Base
has_many :tale_book_joins
has_many :tales, through: :tale_book_joins
end

class TaleBookJoin < ActiveRecord::Base
belongs_to :tale
belongs_to :book
end

class Keyword < ActiveRecord::Base
has_many :tale_keyword_joins
has_many :tales, through: :tale_keyword_joins
end

class TaleKeywordJoin < ActiveRecord::Base
belongs_to :tale
belongs_to :keyword
end

These are the migrations

class CreateTales < ActiveRecord::Migration
  def change
    create_table :tales do |t|
    t.text :name, null: false, unique: true
    t.boolean :exists, default: nil
    t.timestamps null: false
    end
  end
end

class CreateBooks < ActiveRecord::Migration
  def change
  create_table :books do |t|
    t.text :name, null: false, unique: true
    t.boolean :exists, default: nil
    t.timestamps null: false
  end
end
end

class CreateKeywords < ActiveRecord::Migration
  def change
    create_table :keywords do |t|
      t.text :name, null: false, unique: true
      t.boolean :exists, default: nil
      t.timestamps null: false
    end
  end
end

What I want to happen is that everytime i delete a join between (Tale, Book) or (Tale,Keyword) through following method tale_instance_object.book_ids = []

It should go and check if the books for whom the relations have been broken have any other tale relations. If not then set :exists in Book object instance to false.

I am able to do this through controller code. Wondering how CallBacks or ActiveModel can be used

  • 2
    It would be better if you make the question a bit more readable. Maybe by adding some code – usmanali Jun 07 '15 at 10:03
  • You don't need all those join classes, just use a `has_and_belongs_to` relationship instead. Using join classes is a waste of memory when the join class does not actually have any logic of its own. `doctor -> appointments <- patients` is a valid use case of a join model - `tale_book_joins ` is not. http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association – max Jun 07 '15 at 13:05

1 Answers1

1

Join classes should only really be used when the relation is an object on its own. Consider these cases:

doctors -> appointments <- patients
years -> days <- hours

In these cases the relation object has data of its own (appointments.time, days.weekday) and logic. Otherwise you are just wasting memory since a object has to instantiated for every relation. Use has_and_belongs_to instead.

class Tale < ActiveRecord::Base
  # has_and_belongs_to_many :cultures
  # has_and_belongs_to_many :purposes
  has_and_belongs_to_many :books
  has_and_belongs_to_many :keywords 
  after_destroy :update_books!
end

class Book
  has_and_belongs_to_many :tales

  def check_status!
    self.update_attribute(status: :inactive) unless books.any
  end
end

class Keyword
  has_and_belongs_to_many :tales
end 

Also exists is a really bad naming choice for a model field since it collides with Rails built in exists? methods. This will have unexpected consequences.

A better alternative would be to use an integer combined with an enum.

class Book
  # ...
  enum :status [:inactive, :active] # defaults to inactive
end

class CreateBooks < ActiveRecord::Migration
  def change
  create_table :books do |t|
    t.text :name, null: false, unique: true
    t.integer :status, default: 0
    t.timestamps null: false
  end
end

This will also add the methods book.inactive? and book.active?.

You could add a callback to Tale which tells Book to update when a tale is updated but this code really stinks since Tale now is responsible for maintaining state of Book.

class Tale < ActiveRecord::Base
  # ...
  after_destroy :update_books!

  def update_books!
    self.books.each { |b| b.check_status! }
  end
end

class Book
  has_and_belongs_to_many :tales

  def check_status!
    self.update_attribute(status: :inactive) unless tales.any?
  end
end

A better alternative to add a callback on Books or to do it in the controller when destroying a tale:

class Book
  has_and_belongs_to_many :tales

  before_save :check_for_tales!, if: -> { self.tales_changed? }

  def check_for_tales!
    self.status = :inactive unless self.tales.any?
  end
end
max
  • 96,212
  • 14
  • 104
  • 165
  • I initially used HABTM, but the joins will eventually become objects of their own. Relation types are going to get defined and all that information will be stored with the join. Is boolean not a good choice for a flag, can always change the name. I now get the logic of how callback works. So after_destroy is a callback. I guess I will then be putting this callback on the join table. Whenever the join is deleted it will check if either of the sides have any other relations and can set the flags on the objects if not. – Kapil Aggarwal Jun 09 '15 at 17:48
  • 1
    I have been avoiding Active Records callbacks more and more in my own code since it can get pretty complicated to set then up to run only when needed and so that they don't have unexpected side effects. Instead I prefer service objects. Using booleans for state can be problematic. http://www.railstips.org/blog/archives/2012/10/10/booleans-are-baaaaaaaaaad/ – max Jun 09 '15 at 17:54