In my chat app I have users and chats. The tables for each of these is connected by a join table:
class User < ApplicationRecord
has_and_belongs_to_many :chats_users
has_and_belongs_to_many :chats, :through => :chats_users
end
class Chat < ApplicationRecord
has_and_belongs_to_many :chats_users
has_and_belongs_to_many :users, :through => :chats_users
end
class ChatsUsers < ApplicationRecord
belongs_to :chat, class_name: 'Chat'
belongs_to :user, class_name: 'User'
validates :ad_id, presence: true
validates :tag_id, presence: true
end
And the inverse in chat.rb.
When creating a new chat with a list of participating list of user_ids, I want to first check a chat doesn't already exist with the exact same list of associated user_ids, but I can't work out a sane way to do this. How can this be done?
has_and_belongs_to_many is only used in the case where you do not need a join model (or where you initially think you don't need it) as its headless. Instead you want to use has_many through::
class User < ApplicationRecord
has_many :chat_users
has_many :chats, through: :chat_users
end
class Chat < ApplicationRecord
has_many :chat_users
has_many :users, through: :chat_users
end
class ChatUser < ApplicationRecord
belongs_to :chat
belongs_to :user
# not needed if these are belongs_to associations in Rails 5+
validates :ad_id, presence: true
validates :tag_id, presence: true
end
You may need to create a migration to change the name of your table to chat_users and make sure it has a primary key.
has_and_belongs_to_many uses an oddball plural_plural naming scheme that will cause rails to infer that the class is named Chats::User since plural words are treated as modules. While you can work around that by explicitly listing the class name its better to just align your schema with the conventions.
If your still just messing about in development roll back and delete the migration that created the join table and run rails g model ChatUser chat:belongs_to user:belongs_to to generate the correct table with a primary key and timestamps.
If you want to select chats connected to a given set of users:
users = [1,2,3]
Chat.joins(:users)
.where(users: { id: users })
.group(:id)
.having(User.arel_table[Arel.star].count.eq(users.length))
.exists?
Note that you don't really need to tell ActiveRecord which table its going through. Thats the beauty of indirect associations.
Related
I have a question on a platform I'm developing in Ruby on Rails 5.2.
I have an Owner model which is the owner of properties/property. The owner will post a property so that users (in this case roomates) can share the same property/house/department, etc.
I have Owners and I have Users (both tables are created using devise):
Owner.rb:
class Owner < ApplicationRecord
has_many :properties
end
User.rb:
class User < ApplicationRecord
#Theres nothing here (yet)
end
This is where the magic happens. Property.rb:
class Property < ApplicationRecord
belongs_to :owner
has_many :amenities
has_many :services
accepts_nested_attributes_for :amenities
accepts_nested_attributes_for :services
mount_uploaders :pictures, PropertypictureUploader
validates :amenities, :services, presence: true
scope :latest, -> { order created_at: :desc }
end
How can multiple users share a property? I'm aware that it will have a many-to-many association but I'm a bit confused how to connect these relationships so when the owner posts a property it will display something like:
Property available for: 3 users
And then begin to limit users until it completes the amount of users available.
This sounds like your average many to many assocation:
class User < ApplicationRecord
has_many :tenancies, foreign_key: :tenant_id
has_many :properties, through: :tenancies
end
class Tenancy < ApplicationRecord
belongs_to :tenant, class_name: 'User'
belongs_to :property
end
class Property < ApplicationRecord
has_many :tenancies
has_many :tenants, through: :tenancies
def availablity
# or whatever attribute you have that defines the maximum number
max_tenants - tenancies.count
end
end
You can restrict the number of tenants with a custom validation.
You can use a join table, called users_properties. This table will have a property_id and user_id. You'll then have the following in your properties model:
has_many :users_properties
has_many :users, through: :users_properties
Read more about it here https://guides.rubyonrails.org/association_basics.html
I am working on a Ruby on Rails API (version 4.0) to create and update invoices. The relationship between invoices and products is a has_many trough: relationship. Imagine I have product 1, 2, & 3. I am having trouble creating a new invoice that contains product 1 & 3.. When I run the code below I get the error:
Unknown primary key for table invoices_products in model InvoicesProduct.
This error doesn't really make sense to me since InvoicesProduct is a join table and shouldn't require a primary key.
One tricky part about the design is that it needs to track which employee added which products to the invoice, which is why invoices_product has employee_id. It does not seem to be the cause of the problem at the moment. Here is the DB design of the tables in questions:
InvoicesController
This is the code I currently have in the controller. The error message occurs on the first line of create:
def create
invoice = Invoice.new(create_invoice_params)
invoice.created_by = #current_user
# eventually need to set employee_id on each invoice_product,
# but just need to get it working first
# invoice.invoices_products.map!{|link| link.employee = #current_user }
invoice.save
respond_with invoice
end
def create_invoice_params
params.fetch(:invoice).permit(:customer_id, :status_code, :payment_method_code, product_ids: [])
end
Invoice
# /app/models/invoice.rb
class Invoice < ActiveRecord::Base
validates_presence_of :customer
validates_presence_of :created_by
belongs_to :customer, inverse_of: :invoices
belongs_to :created_by, inverse_of: :invoices, class_name: 'Employee'
has_many :invoices_products, inverse_of: :invoice
has_many :products, through: :invoices_products
end
InvoicesProduct
class InvoicesProduct < ActiveRecord::Base
validates_presence_of :invoice
validates_presence_of :product
validates_presence_of :employee
belongs_to :product, inverse_of: :invoices_products
belongs_to :invoice, inverse_of: :invoices_products
belongs_to :employee, inverse_of: :invoices_products
end
Product
# /app/models/product.rb
class Product < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 100 }
has_many :invoices_products, inverse_of: :product
has_many :invoices, through: :invoices_products
end
Request
This is what I've got in mind for a working request, the solution doesn't need to match this, but its what I've been trying
{
"invoice": {
"customer_id": "1",
"product_ids": ["1", "5", "8"]
}
}
I was able to fix the relationship by adding a primary key to the invoices_products. For some reason I was under the impression that join tables did not require a primary key for has_many :through relationships. However, looking at the example on the Rails guide site, the example join table does have a primary key.
That is because you are using has_many :through. If you don't want id (or any other additional field) in the join table, use has_many_and_belongs_to instead
I have two Models, Product and Category, and a join table, Categorizations, for the many-to-many relationship.
Let's say I have two objects, product and category, that are instances of the above.
products = Product.new(...)
category = Category.new(...)
product.categories << category
This successfully creates the relationship in both directions in the rails console, so that:
product.categories
category.products
are both nonempty. Next:
product.categories.delete category
will delete the value from the product object and the join table. HOWEVER it will not delete it from the category object, so that:
category.products
is nonempty, which means that the in-memory category.products object is out of sync with the actual database. It seems weird to me that creation would work symmetrically but deletion would not.
Here are the relevant models:
class Product < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations, :uniq => true
end
class Category < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :products, through: :categorizations, :uniq => true
end
class Categorization < ActiveRecord::Base
belongs_to :product, class_name: "Product"
belongs_to :category, class_name: "Category"
validates :product, presence: true
validates :category, presence: true
end
Any ideas? Thanks!
Answer: it's product.reload
This explanation is the first one I've found after hours searching:
https://stackoverflow.com/a/7449957/456280
The behavior you're observing is the way Rails is designed to behave. See the Rails Guide on associations
You might also want to look at the section on has_and_belongs_to_many (HABTM) associations. HABTM would let you get rid of your explicit Categorization model if renamed the join table categories_products.
If I have a has_and_belongs_to_many relationship between two models, let's say Users and Accounts, can I require that a User have at least one Account, and how?
Also, using the has_and_belongs_to_many relationship, is it possible for an Account not to have a User?
What I need is a relationship where Accounts can live on their own, and belong to Billers, but they can also belong to Users if a User signed up with one. Is this possible, and how?
I personally would drop the the HABTM. Instead I would use has_many :though=>
You would need to create two new models, account_users, and account_billers. You likely already have join tables for the HABTM, but this will expose them as models so they will need ID fields.
So you would end up with something like the following:
class Account < ActiveRecord::Base
has_many :account_billers
has_many :account_users
has_many :billers, :through=> :account_billers
has_many :users, :through=> :account_users
end
class User < ActiveRecord::Base
has_many :account_users
has_many :accounts, :through=>:account_users
validates :accounts, :length => { :minimum => 1}
end
class Biller < ActiveRecord::Base
has_many :account_billers
has_many :accounts, :through=>:account_billers
validates :accounts, :length => { :minimum => 1}
end
class AccountUser < ActiveRecord::Base
belongs_to :user
belongs_to :account
end
class AccountBiller < ActiveRecord::Base
belongs_to :biller
belongs_to :account
end
To validate presence of at least one association, you might want to use a custom validation method, something like
class User < ActiveRecord::Base
has_and_belongs_to_many :accounts
validate :require_at_least_one_account
private
def require_at_least_one_account
errors.add(:accounts, "must amount to at least one") if accounts.size < 1
end
end
(Although this brings a question of how an account is shared between users)
For your second question, looks like polymorphic associations are what you're looking for, but you can't do this straight with a HABTM relationship, you'll have to change it to a has_many :through and introduce a join model.
I just created new columns in my database for my micropost table and these columns were vote_count comment_count and I want to connect it to the Vote models vote_up count and the Comment models comment count. Since I just added these columns although there were votes and comments, how do I connect these other models to the micropost model to fill in the new columns. Any suggestions are much appreciated!
Micropost Model
class Micropost < ActiveRecord::Base
attr_accessible :title, :content, :view_count
acts_as_voteable
belongs_to :school
belongs_to :user
has_many :comments
has_many :views
accepts_nested_attributes_for :comments
end
It looks like what you're trying to do is use a counter_cache, which rails supports, but you've got the names of the columns wrong.
You want to add a comments_count and a votes_count column to your database instead of the ones that you have.
Then you can hook it up to your models as follows:
class Micropost< ActiveRecord::Base
attr_accessible :title, :content, :view_count
acts_as_voteable
belongs_to :school
belongs_to :user
has_many :comments, :counter_cache => true
has_many :views
accepts_nested_attributes_for :comments
end
The votes half of it is a bit more tricky since you're using some extra code with your acts_as_votable module, but counter caches are the way that you want to go if I understand you correctly.
Here is more info on them: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html