Rails 3.2.3
Here's the example that's confusing me:
class Person < ActiveRecord::Base
has_many :toys
end
class Man < Person
end
class Woman < Person
has_many :cookies, foreign_key: person_id
end
Then this happens:
Person.reflections.keys # [:toys]
Man.reflections.keys # [:toys]
Woman.reflections.keys # [:cookies]
Please note that I expected Woman.reflections.keys to be [:toys, :cookies]
Further, if I do this:
Person.new.keys # []
Man.new.keys # []
Woman.new.keys # NoMethodError: undefined method `association_class' for nil:NilClass
So I figured out what was going on for me. I'm not exactly sure why the app loaded and ran without issue but then gave me this problem. Here's more info on my models:
class Person < ActiveRecord::Base
DEFAULT_SUBCLASS = Woman # <------
has_many :toys
end
class Man < Person
end
class Woman < Person
has_many :cookies, foreign_key: person_id
end
Rails wasn't throwing an error, it was just not fully loading the Person class prior to the Woman class I think. There's an issue here with both classes needed the other to load before they can load but I'm not sure why it resulted in my issue. I'd be curious to learn more. But maybe this will help someone experiencing something similar.
Related
I'm having some trouble querying between models in Rails. I have a class Message that belongs_to: booking. My goal is to add an active scope to Message that depends on a Booking scope.
class Booking < ActiveRecord::Base
has_one :event
has_many :messages
def self.active
includes(:event).
where('events.endtime >= ? AND status IS NOT ?'
Time.current.beginning_of_week,
statuses['canceled'])
end
end
class Message < ActiveRecord::Base
belongs_to :booking
belongs_to :person
self.active(person_id)
where(person_id: person_id).merge(Booking.active)
end
end
I want to find the Messages directed to a specific Person where the associated Booking is active. I therefore wish to use the Booking.active when creating Message.active.
Calling Message.active(1) with above implementation returns the following error:
Association named 'event' was not found on Message; perhaps you misspelled it?
Is there any way I can use Booking.active in the implementation of Message.active and get Messages returned?
If you are adding conditions on associations, you also need to join them, not only merge or include them, i.e. the following should work:
class Booking < ActiveRecord::Base
# ...
def self.active
joins(:event).
where('events.endtime >= ? AND status IS NOT ?'
Time.current.beginning_of_week,
statuses['canceled'])
end
end
class Message < ActiveRecord::Base
# ...
self.active(person_id)
where(person_id: person_id).joins(:booking).merge(Booking.active)
end
end
There is not much documentation on this, see this for more info.
I've seen a lot of many-to-many associations, but it seems that the common trend is that they end up using has_many :through relationships.
Let's say you have the following:
class User < ActiveRecord::Base
has_many :user_relationships
has_many :relations, :through => :user_relationships
end
class UserRelationship < ActiveRecord::Base
belongs_to :user
belongs_to :related_user, class_name: "User"
end
Given this type of relationship you end up setting the relationship up as follows:
user1.relations << user2
user2.relations << user1
Or this:
user1.user_relationships.build(related_user_id: 2)
user2.user_relationships.build(related_user_id: 1)
Resulting in rows in the join table looking like this:
user_id | related_user_id
1 | 2
2 | 1
So that when setting up the relation such as the above, you can see that the following can be accomplished
user1.relations.include? user2 = true
user2.relations.include? user1 = true
My question is: Is there a way to accomplish the above, or at least something similar to the above in speed, in Rails WITHOUT having to create 2 rows for every single two-way relationship and maintain the ability to see the relationship from both ends in an efficient manner, reducing the space complexity of creating this relationship by half...
Apologies if this is a noobie question, but I'm new to Rails, just starting to get the hang of things. It's easy to find out how to set these up, but I find it much harder to find out how to actually implement them in an efficient manner
Here's (roughly) what I've done. I'm going to skip a ton of the detail, but am happy to share more if it's helpful. This may be heinous, so I'm curious about others' thoughts.
Essentially, I have a Relationship model something like:
module ActsAsRelatingTo
class Relationship < ActiveRecord::Base
validates :owner_id, presence: true
validates :owner_type, presence: true
validates :in_relation_to_id, presence: true
validates :in_relation_to_type, presence: true
belongs_to :owner, polymorphic: true
belongs_to :in_relation_to, polymorphic: true
acts_as_taggable
acts_as_taggable_on :roles
end
end
Then, I've created a acts_as_relating_to module (again, glossing over the details) that includes stuff like:
module ActsAsRelatingTo
def acts_as_relating_to(*classes_array)
# re-opens the class at run time so I can do things like add
# new instance methods on-the-fly
class_eval do
before_destroy :tell_to_unrelate
has_many :owned_relationships,
as: :owner,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
has_many :referencing_relationships,
as: :in_relation_to,
class_name: "ActsAsRelatingTo::Relationship",
dependent: :destroy
# iterates through arguments passed in 'acts_as_relating_to' call
# in the Person model, below.
classes_array.each do |class_sym|
# This is a method created on-the-fly. So, when I call
# (in the Person model, below) 'acts_as_relating_to :people',
# then I get a method on each instance of 'Person' called
# 'people_that_relate_to_me. You can also create class methods
# using `define_singleton_method`.
#
# The reason for doing all of this via the define_method
# is that it lets me create any number of 'things_i_relate_to'
# methods on any class descending from ActiveRecord::Base
# (more on this in a bit). Which, if I understand it
# correctly, is how a lot of the ActiveRecord functionality gets
# into a model in the first place.
define_method(class_sym.to_s + "_that_relate_to_me") do |options={}|
... some stuff
end
# Same as above, but know I'm defining a method called
# 'people_i_relate_to'
define_method(class_sym.to_s+"_i_relate_to") do |options={}|
... some more stuff
end
# I can also create static methods and incorporate them in the
# 'Person' class. I just define them in modules (such as
# ActsAsRelatingTo::InstanceMethods and ActsAsRelatingTo::ClassMethods)
# and then either 'include' (for instance methods) or 'extend'
# (for class methods) them.
include InstanceMethods
extend ClassMethods
end
end
end
end
# Here, I'm telling ActiveRecord::Base to 'extend' this module.
# That makes the 'acts_as_relating_to' method available in
# any class that descends from ActiveRecord::Base.
ActiveRecord::Base.extend ActsAsRelatingTo
Then, I can do something like:
class Person < ActiveRecord::Base
# Here, I am calling the method that I defined above, passing in
# :people, :organizations, and :programs. This is exactly the
# sort of thing you do all the time when you say something like
# 'has_one :foo', or 'belongs_to :bar'.
acts_as_relating_to :people, :organizations, :programs
# Here, I am calling a method I have that builds on acts_as_relating_to,
# but which I did not show, that creates administrative methods on
# the person so that I can say stuff like 'person.administrate organization'.
# Or, 'organization.administrators'.
acts_as_administering :organizations, :programs
...
end
So, if I have person_1 and person_2 and I have one relationship record where the owner is person_1, and the in_relation_to is person_2, then I can say person_1.people_i_relate_to and get back person_2. Or, I can say, person_2.people_that_relate_to_me and get back person_1.
I've also go an acts_as_administering module which builds on the acts_as_relating_to module and lets me do stuff like person_1.administered_organizations and person_1.administered_programs.
I've probably rattled on too long. Anyway, if it's interesting, I can say more.
Cheers!
I'm getting some strange behaviour when fetching collections from a has_many association with rails 3 when using STI. I have:
class Branch < ActiveRecord::Base
has_many :employees, class_name: 'User::Employee'
has_many :admins, class_name: 'User::BranchAdmin'
end
class User < ActiveRecord::Base
end
class User::Employee < User
belongs_to :branch
end
class User::BranchAdmin < User::Employee
end
The desired behaviour is that branch.employees returns all employees including branch admins. The branch admins only seem to be 'loaded' under this collection when they have been accessed by branch.admins, this is output from the console:
Branch.first.employees.count
=> 2
Branch.first.admins.count
=> 1
Branch.first.employees.count
=> 3
This can be seen in the generated SQL, the first time:
SELECT COUNT(*) FROM "users" WHERE "users"."type" IN ('User::Employee') AND "users"."branch_id" = 1
and the second time:
SELECT COUNT(*) FROM "users" WHERE "users"."type" IN ('User::Employee', 'User::BranchAdmin') AND "users"."branch_id" = 1
I could solve this problem by just specifying:
class Branch < ActiveRecord::Base
has_many :employees, class_name: 'User'
has_many :admins, class_name: 'User::BranchAdmin'
end
since they all be found from their branch_id but this creates problems in the controller if I want to do branch.employees.build then the class will default to User and I have to hack at the type column somewhere. I have got around this for now with:
has_many :employees, class_name: 'User::Employee',
finder_sql: Proc.new{
%Q(SELECT users.* FROM users WHERE users.type IN ('User::Employee','User::BranchAdmin') AND users.branch_id = #{id})
},
counter_sql: Proc.new{
%Q(SELECT COUNT(*) FROM "users" WHERE "users"."type" IN ('User::Employee', 'User::BranchAdmin') AND "users"."branch_id" = #{id})
}
but I would really like to avoid this if possible. Anyone, any ideas?
EDIT:
The finder_sql and counter_sql haven't really solved it for me because it seems that parent associations don't use this and so organisation.employees that has_many :employees, through: :branches will again only include the User::Employee class in the selection.
Basically, the problem only exists in the development environment where classes are loaded as needed. (In production, classes are loaded and kept available.)
The problem comes in due to the interpreter not having seen yet that Admins are a type of Employee when you first run the Employee.find, etc. call.
(Notice that it later uses IN ('User::Employee', 'User::BranchAdmin'))
This happens with every use of model classes that are more than one level deep, but only in dev-mode.
Subclasses always autoload their parent hierarchy. Base classes don't autoload their child hierachies.
Hack-fix:
You can force the correct behaviour in dev-mode by explicitly requiring all your child classes from the base class rb file.
Can you use :conditions?
class Branch < ActiveRecord::Base
has_many :employees, class_name: 'User::Employee', :conditions => {:type => "User::Employee"}
has_many :admins, class_name: 'User::BranchAdmin', :conditions => {:type => "User::BranchAdmin"}
end
This would be my preferred method. One other way to do it might be to add a default scope to the polymorphic models.
class User::BranchAdmin < User::Employee
default_scope where("type = ?", name)
end
A similar problem continues to exist in Rails 6.
This link outlines the issue and workaround. It contains the following explanation and code snippet:
Active Record needs to have STI hierarchies fully loaded in order to generate correct SQL. Preloading in Zeitwerk was designed for this use case:
By preloading the leaves of the tree, autoloading will take care of the entire hierarchy upwards following superclasses.
These files are going to be preloaded on boot, and on each reload.
# config/initializers/preload_vehicle_sti.rb
autoloader = Rails.autoloaders.main
sti_leaves = %w(car motorbike truck)
sti_leaves.each do |leaf|
autoloader.preload("#{Rails.root}/app/models/#{leaf}.rb")
end
You may require a spring stop for the configuration changes to take.
Indeed, that was the plan in the early days of the gem, but it was abandoned soon (in 2019, before Rails 6 was out). Preloading has been deprecated for a long time, and has been deleted in the forthcoming Zeitwerk 2.5.
In a Rails application you can do it this way:
# config/initializers/preload_vehicle_sti.rb
Rails.application.config.to_prepare do
Car
Motorbike
Truck
end
That is, you "preload" just by using the constants in a to_prepare block.
I got these three models:
Projects, Organisms and Sequences
Each sequence is unique and belongs to an organism. An organism can have many sequences. So far, it works fine.
The problem I am struggling with is the association in my projects model:
I need to select one organism and one of the to the organism associated sequences to the project. But also, an organism can have many projects.
I was following these instructions : has_and_belongs_to_many-associations-in-ruby-on-rails. But it fails, when I try save my form data. When I try to "build" it in the console, it even fails:
ree-1.8.7-2011.03 :001 > project = Project.new
=> #<Project id: nil, name: nil, organism_id: nil, sequence_id: nil, created_at: nil, updated_at: nil>
ree-1.8.7-2011.03 :002 > project.organism.build
NoMethodError: undefined method `organism' for #<Project:0x56117c8>
What's wrong with what I'm doing?
Maybe i am following the wrong path here. im not very good in database design, so i need some help obviously :) What i'd like to have is something like project.organism and project.sequence as well as sequence.organism and origanism.sequences. You see, the three models are close connected to each other. The background is, that a rake task will update the list of the organisms and the associated sequences regulari form NCBI database. Therefore i cant just "save" the data in the projects model, i have to link them to other tables.
EDIT: my Project Model looks like this:
class Project < ActiveRecord::Base
has_and_belongs_to_many :organisms
#has_one :sequence
end
note: i still have no idea, how to make the sequence available to the project.
and the migration:
class CreateProjects < ActiveRecord::Migration
def change
create_table :projects do |t|
t.string :name
t.integer :organism_id
t.integer :sequence_id
t.timestamps
end
end
end
thank you four your help,
Adrian
EDIT 2:
i think i have solved it. I just rethought about that, and this is the more simple solution:
class Project < ActiveRecord::Base
belongs_to :organism
belongs_to :sequence
end
class Sequence < ActiveRecord::Base
belongs_to :organism
end
class Organism < ActiveRecord::Base
has_many :projects
has_many :sequences
end
The problem with this is only that i cant go back from sequences to projects like sequence.projects
try
project.organisms.build
instead of
project.organism.build
You did called the association organisms after all :)
I print in my view a number that tell me, how many people read my article. It looks something like a:
<%=article.hits.count%>
As is possible to see, I created a simple association.
Now I am trying to get the information, if the user who is log in on my page, so if he is already had read this article. In my table that contains hits is column user_id.
But I can't still find the way, how to get...
I tried something like:
<% if session[:login_user_id].hits.user_id == session[:login_user_id]%>
Have you read it already.
<% end %>
But the example above doesn't work me... Could anyone help me please, how to do?
EDIT: The models:
class Article < ActiveRecord::Base
has_many :hits
end
class Hits < ActiveRecord::Base
belongs_to :article, :class_name => "DataHit", :foreign_key => "article_id"
has_many :users
end
class User < ActiveRecord::Base
belongs_to :hit
end
Thanks in advance
Let's first talk about the model you like to receive. For me, it sounds like:
Every article can be visited / read by many users.
Every user can read / visit many articles.
This is a classical n:m-association which is normally implemented by a has-many-through association.
If this is the intention, it should be implemented like:
class Article < ActiveRecord::Base
has_many :hits
has_many :users, :through => :hits
end
class Hits < ActiveRecord::Base
belongs_to :article, :class_name => "DataHit", :foreign_key => "article_id"
belongs_to :user
end
class User < ActiveRecord::Base
has_many :hits
has_many :articles, :through => :hits
end
Of course, you have to add migrations that ensure that the final DB model is like that:
Hit has article_id and user_id to ensure that users may find the articles they have read
If you have that model implemented, it should be more easy. Then you have operations available like: #article.users.contains(User.find(user_id)). Have a look at the tutorial at Ruby on Rails Guides which explain what the has-many-through relation is and which advantages they have.
It would be helpful if you try the things first in the console of Rails. To do that, start with:
Start the rails console in the root directory of your application: rails c
Enter there e.g.: art = Article.find(1) to get the article with the id.
Try which methods are available: art.methods.sort to see all methods that could be used. If there is no method users, you have did something wrong with the assocication.
Try the call: us = art.users and look at the result. It should be a rails specific object, an object that behaves like a collection and understands how to add and remove users to that collection (with the whole life cycle of rails). The error your currently have could mean different things:
Your database model does not match your associations defined in Rails (I suspect that).
Some minor tweak (misspelling somewhere) which hinders Rails.
I hope this gives you some clues what to do next, I don't think that we can fix the problem here once and for all times.