I'm trying to create a model that has a collection, of which there may be 0 or 1 "primary" (or default) item in the collection, I'd like to create an attribute that directly references this primary item.
An example model set might look like:
User < ActiveRecord::Base
belongs_to :company
Company < ActiveRecord::Base
has_many :users
has_one :primary_user, class: 'User'
The "primary" user should be the only result returned by Company.primary_user, but should also be a part of the "users" collection
What are some ways to accomplish this? Reminder, there is only 0 or 1 "primary" allowed, and it must be a member of the general collection.
You could do the following:
In your company table add a field called primary_user_id: integer with a migration (only if you don't have it, I believe you do). In this field your are going to store the id of the primary user for that company. You should set the primary user in the create method of the companies_controller.rb.
In your Company model add the method that returns you the primary user.
Company < ActiveRecord::Base
has_many :users
def primary_user
return User.find(primary_user_id)
end
end
Then whenever you want to use this method you should be careful cause you said there could be no primary_user for some companies. So after calling primary_user method verify if it is not nil.
In any view you got to use this or controller
#bunch of code you got wherever you are
#i want to get the primary user of my first company
company = Company.first
primary_user = company.primary_user
#in order to access field from primary_user, firstly do this
#if primary_user exists
if !primary_user.nil?
#do whatever you want with your primary_user
puts primary_user.name
primary_user.destroy #don't think you want to destroy him
end
You can pass a lambda to has_one and use it to scope the relationship:
has_one :primary_user, -> { where primary: 1 }, class_name: 'User'
This does not enforce that there can be atmost one primary user. That is something you will have to handle elsewhere - eg. through a custom validation.
Related
In a Rails 4 application, I have an STI model that stores metadata in a jsonb column.
Base Class:
class Post < ActiveRecord::Base
...
end
Subclass:
class JobPost < Post
# has a jsonb column for metadata
end
One of the data attributes in the metadata column of a JobPost is a foreign_key reference to another table (company_id). I'd like to add a belongs_to :company reference in the JobPost model. It seems like this should be possible by doing something like
class JobPost < Post
belongs_to :company do
Company.find_by_id self.metadata['company_id']
end
end
but that doesn't appear to work. Help?
Note: I am not necessarily intent on using belongs_to rather than writing needed methods like def company by hand, but I do need a way to eager load companies when listing job posts. If there's a way to do that eager loading without a belongs_to relation I'm all ears.
Update1
I have also tried the following, which doesn't appear to work either:
class JobPost < Post
belongs_to :company, foreign_key: "(posts.metadata->>'company_id')::integer".to_sym
end
Update2
To be more clear about my intentions and need:
1) A JobPost belongs_to a Company, but a Post (and other subclasses of Post) does not. I'd prefer not to jankily add the company_id column to the posts table when it won't be used by the other subclasses.
2) A JobPost could justify having it's own table (perhaps the relationship with a Company is enough to justify it). There are reasons why this wouldn't be ideal, but if that's the only answer I'm open to it. I'd, however, like a more definitive "what you're trying to do can't be done" response before going down this road, though.
The primary question is whether you can customize belongs_to so that it uses the metadata column rather than expecting the foreign key to be a column in the table.
The secondary question is whether you can eager load companies alongside job posts without having that belongs_to relation set up.
EDIT
UPD 2
You need to add "company_id" column to the base class of your STI table. If JobPost inherits from Post, and it should have "company_id" then add the "company_id" column to Post (base table).
Remember STI stands for "Single Table Inheritance" so there is only one table on database schema level. Imagine a column of a Post table, where few data records are the foreign key entries for Companies with company_id and what about the other records of this column with non JobPost subclass types, are they null/empty? Hence the relationship is defined with parent STI table and subclass inherits these relations. Additional type column in STI defines the subclass type.
Check here
You may need to dig further on Polymorphic classes instead of STI if both JobPost and Post have relationship with Company, else create two separate model, as they tend do have some unique relationships and column fields.
UPD
Based on updated ask
app/model/company.rb
class Company < ActiveRecord::Base
has_many :posts
delegate :jobposts, to: :posts
end
app/model/post.rb
class Post < ActiveRecord::Base
belongs_to :company
self.inheritance_column = :ptype
scope :job_posts, -> { where(ptype: 'JobPost') }
def self.ptype
%w(JobPost)
end
end
app/models/jobpost.rb
class JobPost < Post; end
Create a company
company = Company.create!(company_params)
Create some posts and add them to the company
company.posts << JobPost.new(jobpost_params)
To fetch jobpost by company relationship
company.job_posts
In case you are storing company_id in jsonb in any which column, just format your jobpost_params hash input accordingly and it should do the deed for you
OLD ASK
To find by primary key
Company.find(id)
In your case, id is self.metadata['company_id']
To find by other keys
Company.find_by(key: value)
Company.find_by_id is no more recommended
Please remove do and end after belongs_to in your model, instead in your controller you can write:
Jobpost.all.each do |x|
# your do
end
regarding foreign key, as rails is convention over configuration, it by default includes company_id reference to Jobpost which you can change in your Company.rb model
Rails 4 lets you scope a has_many relationship like so:
class Customer < ActiveRecord::Base
has_many :orders, -> { where processed: true }
end
So anytime you do customer.orders you only get processed orders.
But what if I need to make the where condition dynamic? How can I pass an argument to the scope lambda?
For instance, I only want orders to show up for the account the customer is currently logged into in a multi-tenant environment.
Here's what I've got:
class Customer < ActiveRecord::Base
has_many :orders, (account) { where(:account_id => account.id) }
end
But how, in my controller or view, do I pass the right account? With the code above in place when I do:
customers.orders
I get all orders for account with an id of 1, seemingly arbitrarily.
The way is to define additional extending selector to has_many scope:
class Customer < ActiveRecord::Base
has_many :orders do
def by_account(account)
# use `self` here to access to current `Customer` record
where(:account_id => account.id)
end
end
end
customers.orders.by_account(account)
The approach is described in Association Extension head in Rails Association page.
To access the Customer record in the nested method you just can access self object, it should have the value of current Customer record.
Sinse of rails (about 5.1) you are able to merge models scope with the othe model has_many scope of the same type, for example, you are able to write the same code as follows in the two models:
class Customer < ApplicationRecord
has_many :orders
end
class Order < ApplicationRecord
scope :by_account, ->(account) { where(account_id: account.id) }
end
customers.orders.by_account(account)
You pass in an instance of the class you have defined. In your case, you would pass in a customer and then get the account.
From the API http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Accessing the owner object
Sometimes it is useful to have access to the owner object when building the query. The owner is passed as a parameter to the block. For example, the following association would find all events that occur on the user's birthday:
class User < ActiveRecord::Base
has_many :birthday_events, ->(user) { where starts_on: user.birthday },
class_name: 'Event'
end
In your example it would be:
class Customer < ActiveRecord::Base
has_many :orders, ->(customer) { where(account_id: customer.account.id) }
end
I know this is old, but since no answer was accepted yet, I thought adding my views on the point would harm no one.
The problem is that whenever you pass a scope to a has_many relationship, passing the instance of the owner class as an argument is not only a possibility but it is the only possibility to pass an argument. I mean, you are not allowed to pass more arguments, and this one will always be the instance of the owner class.
So #RobSobers, when you
"get all orders for account with an id of 1, seemingly arbitrarily."
it is not arbitrary, you get all orders with th id of the customer you called the relation on. I guess your code was something like
Customer.first.orders(#some_account_which_is_ignored_anyway)
Seems like has_many relation was not meant to accept arguments.
Personally, I prefer the solution of #МалъСкрылевъ.
So I have a DB relationship where a User has a PreferenceList. I have set up a preference_list_id in the database table, and I have set up a belongs_to :user relationship with PreferenceList and a has_one :preference_list relationship with User.
Here's the weird part. If I do user.preference_list = preference_list, I can access user.preference_list and it will give me the correct instance of preference_list. However, if I do user.preference_list_id (a valid, column, I've checked), it gives me nothing even after I have added preference_list to user.preference_list. I need the id to be updated for various database operations. What am I doing wrong?
The foreign key goes on the table for the class declaring the belongs_to association. So if you have
class PreferenceList < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :preference_list
end
table preferences_lists should have user_id columns. Rails will handle this columns properly. You can add preference_list_id column to users table as well. But this column has no special meaning, Rails will do nothing with it by default. Try to change belongs_to/has_one sides if you really need to operate preference_list_id. Or use user.preference_list.id
I have a rails application that has three different types of users and I need them all to share the same common profile information. However, each different user also has unique attributes themselves. I'm not sure how to separate out the different fields.
Admin (site wide admin)
Owner (of a store/etc)
Member (such as a member of a co-op)
I'm using devise for authentication and cancan for authorization. Therefore I have a User model with a set of roles that can be applied to the user. This class looks this:
class User < ActiveRecord::Base
# ... devise stuff omitted for brevity ...
# Roles association
has_many :assignments
has_many :roles, :through => :assignments
# For cancan: https://github.com/ryanb/cancan/wiki/Separate-Role-Model
def has_role?(role_sym)
roles.any? { |r| r.name.underscore.to_sym == role_sym }
end
end
Each user has a profile that includes:
First & Last Name
Address Info (city/st/zip/etc)
Phone
I do not want to pollute the User model with this info so I'm throwing it into a Profile model. This part is fairly simple. This turns the User model into something like this:
class User < ActiveRecord::Base
# ... devise stuff omitted for brevity ...
# ... cancan stuff omitted for brevity ...
has_one :profile
end
The additional fields is where I have some uneasy feelings about how to model are ...
If a user is an admin, they'll have unique fields such as:
admin_field_a:string
admin_field_b:string
etc
If a user is a Owner they'll have unique fields ...
stripe_api_key:string
stripe_test_api_key:string
stripe_account_number:string
has_one :store # AR Refence to another model that Admin and Member do not have.
If a user is a member they'll have a few additional fields as such:
stripe_account_number:string
belongs_to :store # the store that they are a member of
has_many :note
...
and a Store model will contain a has_many on the members so we work the the members of the store.
The issue is around the additional fields. Do I set these up as different classes? Put them into a different
I've currently tried a few different ways to set this up:
One way is to set up the User Model as aggregate root
class User < ActiveRecord::Base
# ...
# Roles association
has_many :assignments
has_many :roles, :through => :assignments
# Profile and other object types
has_one :profile
has_one :admin
has_one :owner
has_one :member
# ...
end
The benefit of this approach is the User model is the root and can access everything. The downfall is that if the user is a "owner" then the "admin" and "member" references will be nil (and the cartesian of the other possibilities - admin but not owner or member, etc).
The other option I was thinking of was to have each type of user inherit from the User model as such:
class User < ActiveRecord::Base
# ... other code removed for brevity
has_one :profile
end
class Admin < User
# admin fields
end
class Owner < User
# owner fields
end
class Member < User
# member fields
end
Problem with this is that I'm polluting the User object with all kinds of nil's in the table where one type doesn't need the values from another type/etc. Just seems kind of messy, but I'm not sure.
The other option was to create each account type as the root, but have the user as a child object as shown below.
class Admin
has_one :user
# admin fields go here.
end
class Owner
has_one :user
# owner fields go here.
end
class Member
has_one :user
# member fields go here.
end
The problem with the above is I'm not sure how to load up the proper class once the user logs in. I'll have their user_id and I'll be able to tell which role they are (because of the role association on the user model), but I'm not sure how to go from user UP to a root object. Methods? other?
Conclusion
I have a few different ways to do this, but I'm not sure what the correct "rails" approach is. What is the correct way to model this in rails AR? (MySQL backend). If there is not a "right" approach, whats the best of the above (I'm also open to other ideas).
Thanks!
My answer assumes that a given user can only be one type of user - e.g. ONLY an Admin or ONLY a Member. If so, this seems like a perfect job for ActiveRecord's Polymorphic association.
class User < ActiveRecord::Base
# ...
belongs_to :privilege, :polymorphic => true
end
This association gives User an accessor called 'privilege' (for lack of a better term and to avoid naming confusion which will become apparent later). Because it is polymorphic, it can return a variety of classes. The polymorphic relationship requires two columns on the corresponding table - one (accessor)_type and (accessor)_id. In my example, the User table would gain two fields: privilege_type and privilege_id which ActiveRecord combines to find the associated entry during lookups.
Your Admin, Owner and Member classes look like this:
class Admin
has_one :user, :as => :privilege
# admin fields go here.
end
class Owner
has_one :user, :as => :privilege
# owner fields go here.
end
class Member
has_one :user, :as => :privilege
# member fields go here.
end
Now you can do things like this:
u = User.new(:attribute1 => user_val1, ...)
u.privilege = Admin.new(:admin_att1 => :admin_val1, ...)
u.save!
# Saves a new user (#3, for example) and a new
# admin entry (#2 in my pretend world).
u.privilege_type # Returns 'Admin'
u.privilege_id # Returns 2
u.privilege # returns the Admin#2 instance.
# ActiveRecord's SQL behind the scenes:
# SELECT * FROM admin WHERE id=2
u.privilege.is_a? Admin # returns true
u.privilege.is_a? Member # returns false
Admin.find(2).user # returns User#3
# ActiveRecord's SQL behind the scenes:
# SELECT * FROM user WHERE privilege_type='Admin'
# AND privilege_id=2
I would recommend you make the (accessor)_type field on the database an ENUM if you expect it to be a known set of values. An ENUM, IMHO, is a better choice than a VARCHAR255 which Rails would normally default to, is easier/faster/smaller to index but makes changes down the road more difficult/timeconsuming when you've got millions of users. Also, index the association properly:
add_column :privilege_type, "ENUM('Admin','Owner','Member')", :null => false
add_column :privilege_id, :integer, :null => false
add_index :user, [:privilege_type, :privilege_id], :unique => true
add_index :user, :privilege_type
The first index allows ActiveRecord to rapidly find the reverse association (e.g. find the user that has a privilege of Admin#2) and the second index allows you to find all Admins or all Members.
This RailsCast is a bit dated but a good tutorial on polymorphic relationships nonetheless.
One last note - in your question, you indicated Admin, Owner or Member was the user's type which is appropriate enough, but as you probably see, I'd have to explain that your user table would then have a user_type_type field.
I'm probably not going to give you a rails approved suggestion, but ..
Separating your profile is a good call. Consider using the Decorator pattern for the a role. You can have an AdminUserDecorator, OwnerUserDecorator, or MemberOwnerDecorator. You could also dynamically add the additional fields directly on the instance (it is Ruby after all), but I think that would get ugly and complicated. (If you really want to do bad things, use a visitor object to give you an instance of your decorator from a method on the user class.)
Also, why put the stripe or payment config on the Owner instead of being part of the store information? Unless perhaps an owner can have multiple stores and use the same payment info for each store?
UPDATE: I should also suggest using TDD to flush out what works.
You've already accepted an answer, but for what it's worth, I have a similar situation and chose to go with your "User Model as aggregate root" approach. My User model contains all the "profile" info, and the User, to use a fictitious example, has_one :buyer and has_one :seller. I use a simple tinyint field as bitflags for which roles the user holds, since users could be both buyers and sellers (or TBD other roles I need in the future). If a bit is set, you can assume the corresponding association is not nil (which hasn't been an issue for me since I always check the bitflags before using the association reference). I don't actually have too many unique fields in my real subordinate models, but it's very useful to keep things decluttered when each subordinate model has additional associations, like if seller has_one :merchant_account" and a buyer has_one :purchase_history, etc. I haven't gone live yet, but when I do, I'll follow this post up with any issues I encounter.
I have the following:
class User < ActiveRecord::Base
has_one :subscription
end
class Subscription < ActiveRecord::Base
belongs_to :user
end
The User has a subscription_id and thus can have only one subscription (which is what I want).
Which works fine, but now I do:
#users = User.find(:all)
and I want all the subscriptions to be included.
I tried:
#users = User.find(:all, :include=>[:subscription]) # include subscription
But that would like the subscription table to have a user_id (SQLite3::SQLException: no such column: subscriptions.user_id: SELECT "subscriptions".* FROM "subscriptions" WHERE ("subscriptions".user_id = 2)).
Which is (ofcourse) not what I want.
I am new at RoR and I couldn't find a good example of this case in the books I have nor on the web.
I think you have your associations the wrong way round on the model objects. You should have
class User < ActiveRecord::Base
belongs_to :subscription
end
class Subscription < ActiveRecord::Base
has_one :user
end
belongs_to should be used on the side of the association that defines the foreign key (in this case subscription_id). Semantically this probably looks a bit odd, but that's because in this case rails would kind of expect a user_id to be on the subscriptions table instead of the other way round as you have it.
After that
User.find(:all, :include=>[:subscription])
Should work fine
First of all if the user has foreign key (subscription_id) then it should have belongs_to not the other way around. As the Rails docs says for has_one method:
"This method should only be used if the other class contains the foreign key. If the current class contains the foreign key, then you should use belongs_to instead"
(taken from: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001834)
Second, in your example you tried to find User and include user. You need to do this:
#users = User.find(:all, :include=>[:subscription])