Rails Inheritance with relationships problem - ruby-on-rails

I am using single table inheritance in my project. Instead of explaining more, I'll give the code:
# person_profile.rb
class PersonProfile < ActiveRecord::Base
belongs_to :Person
end
# company_profile.rb
class CompanyProfile < ActiveRecord::Base
belongs_to :Company
end
# person.rb
class Person < User
has_one :PersonProfile
end
# company.rb
class Company < User
has_one :CompanyProfile
end
This seems to me like it should work fine. In one of my views I try if #person.PersonProfile == nil which makes perfect sense to me. But Rails doesn't like it:
Mysql::Error: Unknown column 'person_profiles.person_id' in 'where clause': SELECT * FROM `person_profiles` WHERE (`person_profiles`.person_id = 41) LIMIT 1
Rails is looking for person_id in the table person_profiles, but there is only a user_id in that table. What is the best way to fix this bug?

You can use the :foreign_key option of has_one to specify the key.
For example:
has_one :person, :foreign_key => "user_id"
See this reference.

The models specified in your model associations should be in lowercase with each word being separated by an underscore. So:
class PersonProfile < ActiveRecord::Base
belongs_to :person
end
class CompanyProfile < ActiveRecord::Base
belongs_to :company
end
class Person < User
has_one :person_profile
end
class Company < User
has_one :company_profile
end

i had to specify the foreign_key as 'user_id' because it thinks its 'person_id' by default.
class Person < User
has_one :person_profile, :foreign_key => 'user_id'
end
class Company < User
has_one :company_profile, :foreign_key => 'user_id'
end

Related

Shopping cart Active Record Associations

I am trying to implement a shopping cart within a Rails application. For this, I am trying to glue together these models:
user.rb
class User < ApplicationRecord
has_many :cart_contents
end
cart_content.rb:
class CartContent < ApplicationRecord
belongs_to :user
has_one :product
end
product.rb
class Product < ApplicationRecord
end
To test if this arrangement is working, I tried this in the console:
irb(main):006:0> cc = CartContent.new
=>
#<CartContent:0x000055679d802a28
...
irb(main):008:0> cc.user = User.find(1)
User Load (0.2ms) SELECT "users".* FROM [...]
=> #<User id: 1, email: "mail#example.com", ...
irb(main):010:0> cc.product = Product.find(1)
Product Load (0.1ms) SELECT "products".* FROM [...]
/[...]/activemodel-7.0.0/lib/active_model/attribute.rb:211:
in `with_value_from_database': can't write unknown attribute `cart_content_id` (ActiveModel::MissingAttributeError)
What am I missing here? Do I need to indicate a relationship to cart_content in product.rb?
One possible solution: you may want your CartContent to belongs_to :product.
class CartContent < ApplicationRecord
belongs_to :user
has_one :product # change this to belongs_to
end
cc.product = Product.find(1)
# .. can't write unknown attribute `cart_content_id` (ActiveModel::MissingAttributeError)
has_one will expect your products table to have a cart_content_id column.
See guide section 2.7 Choosing Between belongs_to and has_one
For this, you need to build relations between your models. As mentioned by Jared in this link for the foreign key to exist, you need to specify a belongs_to constraint.
The distinction is in where you place the foreign key (it goes on the table for the class declaring the belongs_to association)
class Product < ApplicationRecord
belongs_to :cart_content
end
If you also want to access all Products for User you can go for below, but you probably don't need a through relation.
class User < ApplicationRecord
has_many :cart_contents
has_many :products, through: :cart_contents
end
Then your Product model would look like this
class Product < ApplicationRecord
belongs_to :cart_content
belongs_to :user
end
I found a solution that works for me.
product.rb
class Product < ApplicationRecord
has_one :cart_content
end
cart_content.rb
class CartContent < ApplicationRecord
belongs_to :user
belongs_to :product
end
user.rb
class User < ApplicationRecord
has_many :cart_contents
has_many :products, through: :cart_contents # for convenience
end
Rails no longer expects a cart_content_id column on Product and the following commands all work in the console:
user = User.first
product = Product.first
cc = CartContent.new
cc.user = user
cc.product = product
cc.save
User.first.products # returns products currently in cart

Should I use the foreign_key clause both in has_many as in belongs_to?

I have two models. Office and Employee. Employee has office_id as foreign_key. The tables was generated under a namespace. So, which is the correct?
class MyNamespace::Office < ActiveRecord::Base
has_many :employees, foreign_key: 'office_id'
end
class MyNamespace::Employee < ActiveRecord::Base
belongs_to :office, foreign_key: 'office_id'
end
Or
class MyNamespace::Office < ActiveRecord::Base
has_many :employees
end
class MyNamespace::Employee < ActiveRecord::Base
belongs_to :office, foreign_key: 'office_id'
end
I think the second example is the correct, because to me, doesn't makes sense declare the foreign_key in a has_many relation. A coworker thinks the first example is the correct. But I haven't found too many references to this subject. So, does Anybody know which is the correct example and why?
You can specify prefixes for proper mapping to table names in DB and remove foreign_keys and MyNamespace at all.
class Office < ActiveRecord::Base
self.table_name_prefix = 'namespace_'
has_many :employees
end
class Employee < ActiveRecord::Base
self.table_name_prefix = 'namespace_'
belongs_to :office
end

How do I specify the join table on a has_many through association?

Here is the schema information on my tables:
table_name: admin_users, primary_key: id
table_name: UserCompanies, primary_key: UserCompanyId, foreign_keys: [CompanyId, UserId]
table_name: Companies, primary_key: CompanyId'
I want to do something like the following:
AdminUser.first.companies
But, my attempts so far are not working, I'm assuming because I need to specify the table names, model names, or key names, but I don't know how that works with a has_many through relationship. Here is my best attempt at defining it so far:
class AdminUser < ActiveRecord::Base
has_many :user_companies, class_name:"TableModule::UserCompany", foreign_key:"UserId"
has_many :companies, through: :user_companies, class_name: "TableModule::Company"
end
# this code is from a rails engine separate from the app where AdminUser is defined
# the purpose of the engine is to provide access to this particular database
# the CustomDBConventions class adapts the models for this database to work with ActiveRecord so we can use snake case attributes, reference the primary key as 'id', and it specifies the correct tables names.
module TableModule
class UserCompany < CustomDBConventions
belongs_to :admin_user
belongs_to :company
end
class Company < CustomDBConventions
has_many :admin_users, through: :user_companies
end
class CustomDBConventions < ActiveRecord::Base
self.abstract_class = true
def self.inherited(subclass)
super
subclass.establish_connection "table_module_#{Rails.env}".to_sym
tb_name = subclass.table_name.to_s.gsub(/^table_module_/,"").classify.pluralize
subclass.table_name = tb_name
subclass.primary_key = tb_name.singularize + "Id"
subclass.alias_attribute :id, subclass.primary_key.to_sym
subclass.column_names.each do |pascal_name|
subclass.alias_attribute pascal_name.underscore.to_sym, pascal_name.to_sym
subclass.alias_attribute "#{pascal_name.underscore}=".to_sym, "#{pascal_name}=".to_sym
end
end
end
end
EDIT: So this setup is really close and I am missing only 1 foreign key specification. When I run AdminUser.first.companies I get a sql error:
TinyTds::Error: Invalid column name 'company_id'.: EXEC sp_executesql N'SELECT [Companies].* FROM [Companies] INNER JOIN [UserCompanies] ON [Companies].[CompanyId] = [UserCompanies].[company_id] WHERE [UserCompanies].[UserId] = #0', N'#0 int', #0 = 1
So I just need to specify to use UserCompanies.CompanyId on this join. How do I properly specify this foreign key?
Assuming the TableModule::UserCompany model has these associations...
class TableModule::UserCompany < ActiveRecord::Base
belongs_to :admin_user
belongs_to :company
end
...then I think this is what you're after:
class AdminUser < ActiveRecord::Base
has_many :companies, through: :user_company, class_name: "TableModule::UserCompany"
end
I'm uncertain what you're doing with the TableModule prefixes, but the following should work:
class AdminUser < ActiveRecord::Base
has_many :user_companies
has_many :companies, through: :user_companies
end
class Company < ActiveRecord::Base
has_many :user_companies
has_many :admin_users, through: :user_companies
end
class UserCompany < ActiveRecord::Base
belongs_to :admin_user
belongs_to :comany
end

Best way to do the following join in Rails 3

I have the following classes:
class Annotation < ActiveRecord::Base
has_many :annotation_tags
end
class Tag < ActiveRecord::Base
has_many :annotation_tags
end
class AnnotationTag < ActiveRecord::Base
belongs_to :annotation
belongs_to :tag
end
and the following join:
SELECT `annotations`.*, annotation_tags.* FROM `annotations` JOIN annotation_tags on
annotation_tags.annotation_id = annotations.id and annotation_tags.tag_id = 123
What is the best way to code this in Rails 3?
You have two options:
Use has_many, :xs, :through => :ys.
class Annotation < ActiveRecord::Base
has_many :annotation_tags
has_many :tags, :through => :annotation_tags
end
class Tag < ActiveRecord::Base
has_many :annotation_tags
has_many :annotations, :through => :annotation_tags
end
class AnnotationTag < ActiveRecord::Base
belongs_to :annotation
belongs_to :tag
end
Then you can type:
tag = Tag.find(123)
annotations = tag.annotations
Alternatively, if you don't need extra attributes on your AnnotationTag model, i.e. it's purely a join table, you can use has_and_belongs_to_many. Your join table must not have an id column, so in the migration, make sure you specify :id => false as described in the ActiveRecord Associations API docs
class Annotation < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :annotations
end
class AnnotationsTag < ActiveRecord::Base # First part of model name must be pluralized.
belongs_to :annotation
belongs_to :tag
end
In this case the syntax for getting all the annotations for a tag is the same.
tag = Tag.find(123)
annotations = tag.annotations
Here was my initial attempt:
tag_id = 123
Annotation.joins("JOIN #{AnnotationTag.table_name} on #{AnnotationTag.table_name}.annotation_id = #{Annotation.table_name}.id and #{AnnotationTag.table_name}.tag_id = #{tag_id}").scoped
#Cameron's is a much cleaner solution, but did require my join table class name to be changed to AnnotationsTags (note the plural).

Can a 3-way relationship be modeled this way in Rails?

A User can have many roles, but only one role per Brand.
Class User < AR::Base
has_and_belongs_to_many :roles, :join_table => "user_brand_roles"
has_and_belongs_to_many :brands, :join_table => "user_brand_roles"
end
The problem with this setup is, how do I check the brand and the role at the same time?
Or would I better off with a BrandRole model where different roles can be set up for each Brand, and then be able to assign a user to a BrandRole?
Class User < AR::Base
has_many :user_brand_roles
has_many :brand_roles, :through => :user_brand_roles
end
Class BrandRole < AR::Base
belongs_to :brand
belongs_to :role
end
Class UserBrandRole < AR::Base
belongs_to :brand_role
belongs_to :user
end
This way I could do a find on the brand for the user:
br = current_user.brand_roles.where(:brand_id => #brand.id).includes(:brand_role)
if br.blank? or br.role != ADMIN
# reject access, redirect
end
This is a new application and I'm trying to learn from past mistakes and stick to the Rails Way. Am I making any bad assumptions or design decisions here?
Assuming Roles,Brands are reference tables. You can have a single association table Responsibilities with columns user_id, role_id, brand_id.
Then you can define
Class User < AR::Base
has_many : responsibilities
has_many :roles, :through => responsibilities
has_many :brands,:through => responsibilities
end
Class Responsibility < AR::Base
belongs_to :user
has_one :role
has_one :brand
end
The you can define
Class User < AR::Base
def has_access?(brand)
responsibility = responsibilities.where(:brand => brand)
responsibility and responsibility.role == ADMIN
end
end
[Not sure if Responsibility is the term used in your domain, but use a domain term instead of calling it as user_brand_role]
This is a conceptual thing. If BrandRole is an entity for your application, then your approach should work. If BrandRole is not an entity by itself in your app, then maybe you can create a UserBrandRole model:
class User < AR::Base
has_many :user_brand_roles
end
class Brand < AR::Base
has_many :user_brand_roles
end
class Role < AR::Base
has_many :user_brand_roles
end
class UserBrandRole < AR::Base
belongs_to :user
belongs_to :brand
belongs_to :role
validates_uniqueness_of :role_id, :scope => [:user_id, :brand_id]
end

Resources