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?
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
Looks like you should improve your model as ProGNOMmers stated; you could use some of following gems:
ancestry ( http://railscasts.com/episodes/262-trees-with-ancestry )
awesome_nested_set ( https://github.com/collectiveidea/awesome_nested_set )
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
Related
I have three model classes: Games, Teams, and Players:
class Game < ActiveRecord::Base
has_many :teams
end
class Team < ActiveRecord::Base
belongs_to :game
has_many :players
end
class Player < ActiveRecord::Base
belongs_to :team
end
class CreateGames < ActiveRecord::Migration
def change
create_table :games do |t|
t.string :play_date
t.string :date
t.timestamps null: false
end
end
end
class CreateTeams < ActiveRecord::Migration
def change
create_table :teams do |t|
t.integer :team_number
t.references :game, index: true
t.timestamps null: false
end
add_foreign_key :teams, :games
end
end
class CreatePlayers < ActiveRecord::Migration
def change
create_table :players do |t|
t.string :name
t.references :team, index: true
t.timestamps null: false
end
add_foreign_key :players, :teams
end
end
I am trying to write an Active Record query to return an object that contains nested objects for 1 game, M number of teams, N number of players. So, for example, I am expecting my returned data to look something like this:
{ "id" => "1", "play_date"=>"2016-01-28",
"teams" =>
[{"id" => "1", "game_id" => "1", "team_number" => "1",
"players" =>
[{"id" => "1", "team_id" => "1", "name" => "Jim"},
{"id" => "2", "team_id" => "1", "name" => "Bob"},
{"id" => "3", "team_id" => "1", "name" => "Sally"}
]
},
{"id" => "2", "game_id" => "1", "team_number" => "2",
"players" =>
[{"id" => "4", "team_id" => "2", "name" => "Ed"},
{"id" => "5", "team_id" => "2", "name" => "Molly"},
{"id" => "6", "team_id" => "2", "name" => "Tim"}
]
}
]
}
The closest I've been able to get this to working is the following:
#data = Player.select("players.*, teams.*").joins(team: :game).where(games: {id: params[:id]})
which returns something like:
[
#<Player id: 1, name: "Jim", team_id: 1, created_at: "2016-01-02 19:27:52", updated_at: "2016-01-02 19:27:52">,
#<Player id: 2, name: "Bob", team_id: 1, created_at: "2016-01-02 19:27:52", updated_at: "2016-01-02 19:27:52">,
#<Player id: 3, name: "Sally", team_id: 1, created_at: "2016-01-02 19:27:52", updated_at: "2016-01-02 19:27:52">,
#<Player id: 4, name: "Ed", team_id: 2, created_at: "2016-01-02 19:27:52", updated_at: "2016-01-02 19:27:52">,
#<Player id: 5, name: "Molly", team_id: 2, created_at: "2016-01-02 19:27:52", updated_at: "2016-01-02 19:27:52">,
#<Player id: 6, name: "Tim", team_id: 2, created_at: "2016-01-02 19:27:52", updated_at: "2016-01-02 19:27:52">
]
I've tried multiple variations of the above code to try to get a set of nested objects like in my expected output example, but I always get results that return only a single level of objects.
Can anyone help return results that contain nested objects? Thanks.
I guess I needed to take a break, I was able to figure this out now.
I think my issue was with relying on the .inspect command to check and see if my multiple levels of objects were being loaded. Turns out if I just simplified my query to:
#data = Game.joins(teams: :players).find(params[:id])
and then actually tried looping through the data in my view:
<% #data.teams.each do |team| %>
<%= team.players[0].name %>
<% end %>
This allowed me to access all of the data I needed. I'm guessing due to lazy loading (but maybe not) inspect doesn't show all of the data.
Given I have this...
create_table "competitors", :force => true do |t|
t.integer "review_id"
end
create_table "questions", :force => true do |t|
t.integer "product_id"
end
create_table "reviews", :force => true do |t|
t.integer "product_id"
end
class Competitor < ActiveRecord::Base
belongs_to :review
def product
self.review.product
end
def questions
self.review.product.questions
end
end
class Review < ActiveRecord::Base
belongs_to :product
def questions
self.product.questions
end
end
class Product < ActiveRecord::Base
has_many :questions
has_many :reviews
end
class Question < ActiveRecord::Base
belongs_to :product
end
...then how is this even possible?
> #competitor.questions
=> []
> #competitor
=> #<Competitor id: 14, review_id: 7, name: "Colonial FirstState", url: "http://firststate.com.au", created_at: "2013-06-07 06:05:37", updated_at: "2013-06-07 06:05:37", logo_file_name: nil, logo_content_type: nil, logo_file_size: nil, logo_updated_at: nil, logo_processing: false, logo: nil>
> #review = #competitor.review
=> #<Review id: 7, product_id: 9, client_name: "BHP", url: "http://bhp.com.au", created_at: "2013-06-07 06:05:37", updated_at: "2013-06-07 06:05:37">
> #product = #competitor.review.product
=> #<Product id: 9, name: "Retail Site Search", created_at: "2013-06-07 05:54:31", updated_at: "2013-06-07 05:54:31", description: nil>
> #competitor.product
=> #<Product id: 9, name: "Retail Site Search", created_at: "2013-06-07 06:05:37", updated_at: "2013-06-07 06:05:37", description: nil>
> #product.questions
=> []
> Product.find(9).questions
Product Load (1.0ms) SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT 1 [["id", 9]]
Question Load (0.3ms) SELECT "questions".* FROM "questions" WHERE "questions"."product_id" = 9
=> [#<Question id: 13... etc etc ....>]
In other words, when I find the product via it's ID, I can see the product's questions, but when I find the product at the end of chain, I can't see them.
You'll be much better off if you don't define your own questions methods, and instead use has_many :questions, through: :product. See http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association for more details.
The answer was the delegate method http://api.rubyonrails.org/classes/Module.html#method-i-delegate
I have...
app/models/report.rb:
has_and_belongs_to :standards
app/models/standard.rb:
has_and_belongs_to :reports
db/schema.rb:
create_table "reports_standards", :id => false, :force => true do |t|
t.integer "report_id"
t.integer "standard_id"
end
When I log into rails console, all seems OK initially...
> #report = Report.create :name => "foo"
=> #<Report id: 2, name: "foo", created_at: "2013-02-21 03:10:06", updated_at: "2013-02-21 03:10:06">
> #standard = #report.standards.build :name => "bar"
=> #<Standard id: nil, name: "bar", created_at: nil, updated_at: nil>
> #report.standards
=> [#<Standard id: nil, name: "bar", created_at: nil, updated_at: nil>]
...but then it turns strange...
> #standard.reports
=> []
Isn't it meant to be:
> #standard.reports
=> [#<Report id: 2, name: "foo", created_at: "2013-02-21 03:10:06", updated_at: "2013-02-21 03:10:06">]
Why isn't it? How do I fix it?
you are running #report.standards.build :name => "bar" which only builds the record and not create it in the database. if you change build to create, you should be able to see the associations.
I apologize if the question title is confusing. I have the following situation: I have a Person model which stores people in the standard way in the people table.
I need to add a has_many relationship for emergency_contacts on the Person model. I tried doing this in the following way:
Migrations:
create_table :people do |t|
t.string :first
t.string :middle
t.string :last
t.timestamps
end
create_table :emergency_contacts, :id => false do |t|
t.integer :person_id
t.integer :emergency_contact_id
t.timestamps
end
Models:
class Person < ActiveRecord::Base
has_many :emergency_contacts
validates :first, :presence => true
validates :last, :presence => true
end
class EmergencyContact < ActiveRecord::Base
belongs_to :person
has_one :person, :foreign_key => 'emergency_contact_id'
end
This allows me to do:
ruby-1.9.2-p180 :001 > p = Person.new(first: "John", last: "Doe")
=> #<Person id: nil, first: "John", middle: nil, last: "Doe", created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :002 > ec = EmergencyContact.new
=> #<EmergencyContact person_id: nil, emergency_contact_id: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :003 > ec.emergency_contact = Person.new(first: "Peter", last: "Griffin")
=> #<Person id: nil, first: "Peter", middle: nil, last: "Griffin", created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :004 > p.emergency_contacts << ec
=> [#<EmergencyContact person_id: nil, emergency_contact_id: nil, created_at: nil, updated_at: nil>]
ruby-1.9.2-p180 :005 > p.save!
=> true
However, I don't feel the EmergencyContact model should have to be there, since I am really just referencing a Person model anyway.
Is there a way to remove this "middle-man" model, so that I can just do something like:
p = Person.new(first: "John", last: "Doe")
p.emergency_contacts << Person.new(first: "Peter", last: "Griffin")
I'd really use self referential associations for this kind of purpose.
Se tutorial here: http://railscasts.com/episodes/163-self-referential-association
This is probably basic stuff, but I really made my homework looking it up. I still can´t find the solution.
I have a users db, a movies db, and a ratings db. Ratings has a movie_id, user_id and grade.
class Rating < ActiveRecord::Base
attr_accessible :grade
belongs_to :user
belongs_to :movie
end
class User < ActiveRecord::Base
has_many :ratings, :dependent => :destroy
has_many :movies, :through => :ratings
...
class Movie < ActiveRecord::Base
has_many :ratings, :dependent => :destroy
has_many :users, :through => :ratings
...
But I can´t create a rating with both movie_id and user_id
rails c test
Loading test environment (Rails 3.0.3)
#movie=Factory(:movie)
#<Movie id: 1, title: "Rocky 20", link: "#", created_at: "2011-01-22 21:04:42", updated_at: "2011-01-22 21:04:42">
#user=Factory(:user)
#<User id: 1, name: "lola", email: "lola#gmail.com", created_at: "2011-01-22 21:04:48", updated_at: "2011-01-22 21:04:48", encrypted_password: "c306a696138fa08c543ada3a3b4fd92067e9941743b7558e891...", salt: "f82c6abaccec17e2866d50150ad200181eb4bc8e0204249f171...", admin: false>
>> #user.ratings.create(:grade=> 4, :movie_id =>#movie.id)
=> #<Rating id: 1, grade: 4, user_id: nil, movie_id: nil, created_at: "2011-01-22 21:04:55", updated_at: "2011-01-22 21:04:55">
that "movie_id: nil" is what is killing me...
then of course me tests are not passing:
#user.rating.create(grade => 5, :movie_id => #movie.id) has no movie_id
#movie.rating.create(grade => 5, :user_id => #user.id) has no user_id
any hints?
Thanks!!
The code you're showing here should absolutely work IMO. Which leaves me guessing that it's something you aren't showing here. A couple of things I'd try:
Use the regular console for a change
Don't use Factories (maybe it's them?), create actual entries instead
I've tried your example and here's what I got:
> m = Movie.create :title => "Foo"
=> #<Movie id: 1, title: "Foo", created_at: …>
> u = User.create :name => "Marc"
=> #<User id: 1, name: "Marc", created_at: …>
> u.ratings.create :grade => 4, :movie_id => m.id
=> #<Rating id: 1, grade: 4, user_id: 1, movie_id: 1, created_at: …>
> m.ratings.create :grade => 3, :user_id => u.id
=> #<Rating id: 2, grade: 3, user_id: 1, movie_id: 1, created_at: …>
Which you can also use like this, as you might know:
> m.ratings.create :grade => 3, :user => u
=> #<Rating id: 3, grade: 3, user_id: 1, movie_id: 1, created_at: …>
Try defining your factories like this:
Factory.define :movie, :class => Movie do |f|
f.title "Rocky 20"
f.link "#"
end
Factory.define :user, :class => User do |f|
f.name "Lola"
f.email "lola#gmail.com"
end
Factory.define :rating, :class => Rating do |f|
f.movie { |m| m.association :movie}
f.user { |u| u.association :user}
f.grade 4
end
Then to test a rating, use the rating factory by itself:
it "creates a rating" do
rating = Factory.create(:rating)
rating.user.name.should == "Lola"
rating.movie.title.should == "Rocky 20"
end