4

I have the two rails models Section & SectionRevision. A Section is mostly just a container that holds all the Revisions relating to itself. So most of the attributes for the Section are basically stored within the SectionRevision model so there's a history of Revisions that can be reverted back to at any time.

Sometimes I need to access the attributes for the latest revision from the Sections Model so I've created some Virtual Attributes to account for this.


Each model has the attributes as defined in these migrations:

Section:

class CreateSections < ActiveRecord::Migration
  def change
    create_table :sections do |t|
      t.integer "page_id", :null => false

      t.timestamps
      t.datetime "deleted_at"
    end
    add_index("sections", "page_id")
    add_index("sections", "current_revision_id")
  end
end

SectionRevision:

class CreateSectionRevisions < ActiveRecord::Migration
  def change
    create_table :section_revisions do |t|
      t.integer "section_id", :null => false
      t.integer "parent_section_id"

      t.integer "position"
      t.string "title", :default => "", :null => false
      t.text "body", :null => false
      t.timestamps
    end
        add_index("section_revisions", "section_id")
        add_index("section_revisions", "parent_section_id")
  end
end

And the models:

SectionRevision:

class SectionRevision < ActiveRecord::Base
  belongs_to :section, :class_name => 'Section', :foreign_key => 'section_id'
  belongs_to :parent_section, :class_name => 'Section', :foreign_key => 'parent_section_id'

  def parsed_json
    return JSON.parse(self.body)
  end
end

Section:

class Section < ActiveRecord::Base
  belongs_to :page
  has_many :revisions, :class_name => 'SectionRevision', :foreign_key => 'section_id'
  has_many :references

  def current_revision
    self.revisions.order('created_at DESC').first
  end

  def position
    self.current_revision.position
  end

  def parent_section
    self.current_revision.parent_section
  end

  def children
    Sections.where(:parent_section => self.id)
  end
end

As you can see Section has a couple of virtual attributes like, parent_section,current_revision & position.

The problem being now I would like to create a virtual attribute, children that selects all sections where the virtual attribute parent_section.id is equal to self.id. Is this possible at all? I know the above code won't work as its doing a query for a column that doesn't exist - and I'm not sure how to access the Model instances from within the model 'Sections' doesn't appear to work.

Can a perform a selection based on virtual attributes?


I've updated the model based on ProGNOMmers answer and get the following:

class Section < ActiveRecord::Base
  has_many :revisions, :class_name => 'SectionRevision', 
                       :foreign_key => 'section_id'
 #Need to somehow modify :child_revisions to only be selected if it is the section_id's current_revision?
  has_many :child_revisions, :class_name => 'SectionRevision', 
                             :foreign_key => 'parent_section_id'
  has_many :children, :through => :child_revisions, 
                      :source => :section
end

Circumstance 1: This works perfectly fine.

1.9.3p392 :040 > section
 => #<Section id: 3, page_id: 10, created_at: "2013-04-02 01:31:42", updated_at: "2013-04-02 01:31:42", deleted_at: nil> 
1.9.3p392 :041 > sub_section
 => #<Section id: 4, page_id: 10, created_at: "2013-04-04 10:19:33", updated_at: "2013-04-04 10:19:33", deleted_at: nil> 
1.9.3p392 :042 > revision1
 => #<SectionRevision id: 5, section_id: 4, title: "test", body: "[{\"type\":\"testbody\"}]", created_at: "2013-04-04 10:21:46", updated_at: "2013-04-04 21:55:10", position: 3, parent_section_id: nil> 
1.9.3p392 :043 > revision2
 => #<SectionRevision id: 6, section_id: 4, title: "test", body: "[{\"type\":\"testbody\"}]", created_at: "2013-04-04 12:29:19", updated_at: "2013-04-04 21:55:15", position: 3, parent_section_id: 3> 
1.9.3p392 :044 > sub_section.current_revision
  SectionRevision Load (0.6ms)  SELECT `section_revisions`.* FROM `section_revisions` WHERE `section_revisions`.`section_id` = 4 ORDER BY created_at DESC LIMIT 1
 => #<SectionRevision id: 6, section_id: 4, title: "test", body: "[{\"type\":\"testbody\"}]", created_at: "2013-04-04 12:29:19", updated_at: "2013-04-04 21:55:15", position: 3, parent_section_id: 3> 
1.9.3p392 :045 > section.children
 => [#<Section id: 4, page_id: 10, created_at: "2013-04-04 10:19:33", updated_at: "2013-04-04 10:19:33", deleted_at: nil>] 

Circumstance 2:

1.9.3p392 :021 > section
 => #<Section id: 3, page_id: 10, created_at: "2013-04-02 01:31:42", updated_at: "2013-04-02 01:31:42", deleted_at: nil> 
1.9.3p392 :022 > sub_section
 => #<Section id: 4, page_id: 10, created_at: "2013-04-04 10:19:33", updated_at: "2013-04-04 10:19:33", deleted_at: nil> 
1.9.3p392 :023 > revision1
 => #<SectionRevision id: 5, section_id: 4, title: "test", body: "[{\"type\":\"testbody\"}]", created_at: "2013-04-04 10:21:46", updated_at: "2013-04-04 10:24:22", position: 3, parent_section_id: 3> 
1.9.3p392 :024 > revision2
 => #<SectionRevision id: 6, section_id: 4, title: "test", body: "[{\"type\":\"testbody\"}]", created_at: "2013-04-04 12:29:19", updated_at: "2013-04-04 12:29:19", position: 3, parent_section_id: nil> 
1.9.3p392 :025 > sub_section.current_revision
  SectionRevision Load (0.7ms)  SELECT `section_revisions`.* FROM `section_revisions` WHERE `section_revisions`.`section_id` = 4 ORDER BY created_at DESC LIMIT 1
 => #<SectionRevision id: 6, section_id: 4, title: "test", body: "[{\"type\":\"testbody\"}]", created_at: "2013-04-04 12:29:19", updated_at: "2013-04-04 12:29:19", position: 3, parent_section_id: nil> 
1.9.3p392 :026 > section.children
  Section Load (0.6ms)  SELECT `sections`.* FROM `sections` INNER JOIN `section_revisions` ON `sections`.`id` = `section_revisions`.`section_id` WHERE `section_revisions`.`parent_section_id` = 3
 => [#<Section id: 4, page_id: 10, created_at: "2013-04-04 10:19:33", updated_at: "2013-04-04 10:19:33", deleted_at: nil>] 

In circumstance 2 I would like section.children to return => [] as sub_section.current_revision.parent_section_id = nil and not section.id.

In other words section.children should return all Sections where .current_revision.parent_section_id = section.id but I can't query that as .current_revision is a virtual attribute.

Is it possible to maybe turn Section.current_revision in to some sort of association? Or maybe the only way is to add a current_revision column to the sections table?

Ryan King
  • 3,538
  • 12
  • 48
  • 72

2 Answers2

3

I think custom relationships are well suited for this cases:

class Section < ActiveRecord::Base
  has_many :revisions, :class_name => 'SectionRevision', 
                       :foreign_key => 'section_id'
  has_many :child_revisions, :class_name => 'SectionRevision', 
                             :foreign_key => 'parent_section_id'
  has_many :children, :through => :child_revisions, 
                      :source => :section
end

Section.find(42).children 
#=> SELECT ... WHERE ... AND section_revisions.parent_section = 42

I didn't tried the code, there could be mistakes, but the idea should be correct.

I deleted the part about :conditions, since is not useful after the last edits

mdesantis
  • 8,257
  • 4
  • 31
  • 63
  • Could you explain what `:conditions => proc` does? – Ryan King Apr 04 '13 at 10:10
  • I had to change it to `:conditions => proc{ { :parent_section_id => id } }` but I get an empty array in return. I've updated the question above with the details. – Ryan King Apr 04 '13 at 10:35
  • I updated the answer with more informations. An empty array should mean that there aren't revisions which match the conditions; check `children.to_sql`, in order to verify that the generated SQL is right – mdesantis Apr 04 '13 at 10:42
  • 1
    looking at your irb output `section_id` should not match (`s.section_id` is 3 and `sr.section_id` is 4); maybe the relation is wrong: try with `has_many :children, :class_name => 'SectionRevision', :foreign_key => 'parent_section_id'` (without `:conditions`) – mdesantis Apr 04 '13 at 10:49
  • Yes that works, but I would like it to return ss, that is sr's section_id - in other words find the `SectionRevision` that has `parent_section_id` that matches `self.id` and return the `Section` that matches it's `section_id`. – Ryan King Apr 04 '13 at 11:30
  • Maybe a :through association? – Ryan King Apr 04 '13 at 11:33
  • 1
    yes, something like `has_many :child_revisions, :class_name => 'SectionRevision', :foreign_key => 'parent_section_id'` and `has_many :children, :through => :child_revisions, :source => :section` – mdesantis Apr 04 '13 at 12:12
  • That works great but right now it `s.children` will return `ss` if it has any `SectionRevisions` that have a `parent_id` equal to `s.id`. What I would like is for it to return `ss` only if the latest `SectionRevision`(ss.current_revision) `parent_id` is equal to `s.id`. Is there some way we could do that? Sorry it's so complicated. – Ryan King Apr 04 '13 at 12:38
  • I didn't understand: do you want the section related to `current_revision`? or maybe the parent section related to it? – mdesantis Apr 04 '13 at 14:03
  • Section I think... Say `ss` has 2 `SectionRevisions`, `sr1` & `sr2` - `sr1` was created yesterday and `sr2` was created today. That would make `sr2` the `current_revision` as it is the latest record... If `sr2.parent_id` = 3 then `s.children` should return `ss`. However if `sr2.parent_id` = null and `sr1.parent_id` = 3 then `s.children` returns nothing as the `current_revision` doesn't have a `parent_id` that matches `s` - `sr1` is ignored because it is not the `current_revision` – Ryan King Apr 04 '13 at 14:23
2

Looks like you should improve your model as ProGNOMmers stated; you could use some of following gems:

But answering your question literally, you can try adding 'children' method to your SectionRevision model and delegate Section#children to current_revision.

class SectionRevision
  def children
    SectionRevision.where(:parent_section => self.id) # parent_section IS a column of SectionRevision
  end
end

class Section
  def children
    current_revision.children
  end
end

BTW you could use #delegate for delegating:

class Section
  delegate :children, :position, :parent_section, to: :current_revision
  def current_revision
    Section.where(:parent_section => self.id)
  end
end

http://apidock.com/rails/Module/delegate

cthulhu
  • 3,749
  • 1
  • 21
  • 25
  • Thanks, this doesn't quite work as `:parent_section` is a virtual attribute in the `Section` and doesn't exist as a column that can be queried. I'll try improving the model as stated above althoughI don't full understand how `:conditions => proc` works. – Ryan King Apr 04 '13 at 10:13
  • I made a mistake - plz see my edit in SectionRevision#children – cthulhu Apr 04 '13 at 11:39