Rails has_and_belongs_to_many only works one way - ruby-on-rails

I am trying to create a many to many relationship between my groups table and my keywords table.
When I am in my controller I cant do Keyword.groups or Group.keywords as I get a no method error. I have checked Group.methods and I only have these related methods
"before_add_for_groups_keywords",
"before_add_for_groups_keywords?",
"before_add_for_groups_keywords=",
"after_add_for_groups_keywords",
"after_add_for_groups_keywords?",
"after_add_for_groups_keywords=",
"before_remove_for_groups_keywords",
"before_remove_for_groups_keywords?",
"before_remove_for_groups_keywords=",
"after_remove_for_groups_keywords",
"after_remove_for_groups_keywords?",
"after_remove_for_groups_keywords=",
"before_add_for_keywords",
"before_add_for_keywords?",
"before_add_for_keywords=",
"after_add_for_keywords",
"after_add_for_keywords?",
"after_add_for_keywords=",
"before_remove_for_keywords",
"before_remove_for_keywords?",
"before_remove_for_keywords=",
"after_remove_for_keywords",
"after_remove_for_keywords?",
"after_remove_for_keywords=",
Where as Keyword.methods gives me these
"before_add_for_keywords_groups",
"before_add_for_keywords_groups?",
"before_add_for_keywords_groups=",
"after_add_for_keywords_groups",
"after_add_for_keywords_groups?",
"after_add_for_keywords_groups=",
"before_remove_for_keywords_groups",
"before_remove_for_keywords_groups?",
"before_remove_for_keywords_groups=",
"after_remove_for_keywords_groups",
"after_remove_for_keywords_groups?",
"after_remove_for_keywords_groups=",
"before_add_for_groups",
"before_add_for_groups?",
"before_add_for_groups=",
"after_add_for_groups",
"after_add_for_groups?",
"after_add_for_groups=",
"before_remove_for_groups",
"before_remove_for_groups?",
"before_remove_for_groups=",
"after_remove_for_groups",
"after_remove_for_groups?",
"after_remove_for_groups=",
My models
has_and_belongs_to_many :keywords
has_and_belongs_to_many :groups
My db schema is the following
create_table "groups", force: :cascade do |t|
t.integer "member_id"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "groups", ["member_id"], name: "index_groups_on_member_id", using: :btree
create_table "groups_keywords", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "keyword_id"
end
add_index "groups_keywords", ["group_id", "keyword_id"], name: "index_groups_keywords_on_group_id_and_keyword_id", unique: true, using: :btree
create_table "keywords", force: :cascade do |t|
t.string "keyword"
t.string "keyword_hash"
t.datetime "checked_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

Associations are defined for instances.
Keyword.groups indeed does not exist (as a class method).
However, Keyword#groups, as in Keyword.first.groups, does work. The reason for that is that Keyword.first returns an instance of Keyword class, which Keyword class itself is not.

A better way to implement a many to many relationship is with the has_many :through idiom:
class Keyword
has_many :keyword_groups
has_many :groups, :through => :keyword_groups
class Group
has_many :keyword_groups
has_many :keywords, :through => :keyword_groups
class KeywordGroup
belongs_to :keyword
belongs_to :group
This will give you flexibility in the future if you need to extend your join model.

When you do Keyword.group(1), it doesn't do what you think it is doing. It just finds all the keywords and groups them by 1.
SELECT * "keywords".* FROM "keywords" GROUP BY 1
Indeed, you can't actually call these kind of method on Class level, associations are accessed through instance level. And as you have defined them has_and_belongs_to_many, you will have to be plural about them, not single.
keyword = Keyword.first
keyword.groups # Not 'group', and on instance level

Related

Rails: Self-referential associations have my head spinning

I'm currently working on a small school project that utilizes Ruby on Rails and I'm having some trouble getting my self-referential associations working correctly.
Context
The intended functionality of my web app is for users to post houses/apartments for other users to search through and rent. Since I'm having issues with a specific association, I'm working with a completely stripped down version that only has two models, User and Lease.
What I'm Trying to Accomplish
Ideally, when a person first registers on the site, a User object is created to hold their information such as email and password. A User can then either post a listing or search through listings.
Once a post has been created and another user decides to rent the posted house, a Lease object is created, which holds the ID of the posting User as well as the ID of the renting user, aliased as "landlord_id" and "tenant_id" respectively.
A User should now be identified as either a User, Landlord or a Tenant (or both Landlord and Tenant) based on whether there are any Lease objects with their ID as either a Landlord or a Tenant. This identification will be used to determine whether the User can access other areas of the site.
userFoo.leases
This should give me a list of all Lease objects with which the User's ID is associated, regardless of whether it's as a Landlord or Tenant.
userFoo.tenants
This should give me a list of any User object whose ID is associated with the ID of userFoo as a Tenant through Lease, and the inverse if I ask for landlords.
The Code
User Class
class User < ApplicationRecord
has_many :tenants, class_name: "Lease", foreign_key: "landlord_id"
has_many :landlords, class_name: "Lease", foreign_key: "tenant_id"
end
Lease Class
class Lease < ApplicationRecord
belongs_to :landlord, class_name: "User"
belongs_to :tenant, class_name: "User"
end
Users Table Migration
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.timestamps
end
end
end
Leases Table Migration
class CreateLeases < ActiveRecord::Migration[6.0]
def change
create_table :leases do |t|
t.references :landlord, null: false, foreign_key: {to_table: :users}
t.references :tenant, null: false, foreign_key: {to_table: :users}
t.timestamps
end
end
end
Database Schema
ActiveRecord::Schema.define(version: 2020_10_18_005954) do
create_table "leases", force: :cascade do |t|
t.integer "landlord_id", null: false
t.integer "tenant_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["landlord_id"], name: "index_leases_on_landlord_id"
t.index ["tenant_id"], name: "index_leases_on_tenant_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "leases", "users", column: "landlord_id"
add_foreign_key "leases", "users", column: "tenant_id"
end
What's Wrong?
userFoo.leases
Normally a User would have_many leases by having their ID associated with a lease as "user_id." However, since I'm using "tenant_id" and "landlord_id", this command fails because it can't find "user_id" in the Leases table.
userFoo.tenants
This command gives me a list of all Lease objects where userFoo's ID is associated as "landlord_id" instead of all User objects associated with userFoo's ID as tenants. To retrieve a tenant as is, I have to use the command:
userFoo.tenants.first.tenant
Conclusion
I am having a bit of a hard time understanding these deeper, more complex associations, and I've spent some time trying to find a detailed reference on has_many that covers all the arguments, but all I can really find are small blog posts that reference the "Employees" and "Managers" example on guides.rubyonrails.com . I think one problem is that I'm not sure I'm correctly reflecting my model associations in my table schema.
I'm more than happy to teach myself if someone can point me in the right direction. I'm also open to alternative solutions but only if I can't get the functionality I want out of this setup, because my instructor specifically asked me to try it this way
Thanks in advance for any help! It's much appreciated.
as per your requirement you can try like this:
# app/models/user.rb
class User < ApplicationRecord
has_many :owned_properties, class_name: "Property", foreign_key: "landlord_id"
has_many :rented_properties, class_name: "Property", foreign_key: "tenant_id"
end
Here I have declared two associations with same table but different foreign keys.
# app/models/property.rb
class Property < ApplicationRecord
belongs_to :landlord, class_name: "User"
belongs_to :tenant, class_name: "User"
end
Here I have taken one table by using this user can post one property where landlord is the owner of a house and later you can add tenant who is taking rent to one property.
# db/migrations/20201018054951_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false, index: true
t.string :password_digest, null: false
t.timestamps
end
end
end
Above is your users table migration.
# db/migrations/20201018055351_create_properties.rb
class CreateProperties < ActiveRecord::Migration[6.0]
def change
create_table :properties do |t|
t.references :landlord, foreign_key: {to_table: :users}, null: false
t.references :tenant, foreign_key: {to_table: :users}
t.timestamps
end
end
end
Above is your properties table migration.
# db/schema.rb
ActiveRecord::Schema.define(version: 2020_10_18_055351) do
create_table "properties", force: :cascade do |t|
t.bigint "landlord_id", null: false
t.bigint "tenant_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["landlord_id"], name: "index_properties_on_landlord_id"
t.index ["tenant_id"], name: "index_properties_on_tenant_id"
end
create_table "users", force: :cascade do |t|
t.string "name", null: false
t.string "email", null: false
t.string "password_digest", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email"
end
add_foreign_key "properties", "users", column: "landlord_id"
add_foreign_key "properties", "users", column: "tenant_id"
end
If you want to fetch all the owned properties of a user, use user.owned_properties.
If you want to fetch all rented properties of a user, use user.rented_properties.
^^ Here both the cases you'll get objects of Property class.
If you want to get landlord of a property, use property.landlord.
If you want to get tenant of a property, use property.tenant.
^^ Here both the cases you'll get objects of User class.
If you want you can add other attributes like: name, price, etc to properties table.
I think, this will help you. Thanks :) Happy Coding :)

Why is my Rails activerecord many-to-many through relationship not working

Three models Professor, Expertise & ExpertisesProfessor (the join table). I would like to use a has_many activerecord structure but when I call Expertise.professors.all I get an error
*NoMethodError (undefined method `professors' for Class:0x000000000a1ddda0) *
I want to be able to call Expertise.professors and Professor.expertise ???
I am comfortable with using HABTM instead of "has_many through" but for my project I prefer to use the the " has_many through " relationship so please if I could get solutions along those lines only if possible .
**professor.rb**
class Professor < ApplicationRecord
has_many :expertise_professors
has_many :expertises, through: :expertise_professors
end
**expertise.rb**
class Expertise < ApplicationRecord
has_many :expertise_professors
has_many :professors, through: :expertise_professors
end
**expertises_professor.rb**
class ExpertisesProfessor < ApplicationRecord
belongs_to :expertise
belongs_to :professor
end
My Schema File
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_12_18_191008) do
create_table "expertises", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "expertises_professors", id: false, force: :cascade do |t|
t.integer "expertise_id", null: false
t.integer "professor_id", null: false
end
create_table "professors", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
Any ideas what I have missed ?
You can not call Expertise.professors. You first need to load the single record or object of the Expertise like
expertise = Expertise.first
And then you can get all professors
expertise.professiors.all
Same way you can get all expertises for specific professor.

How to get parents of child objects of another specific parent object in a single query

EXPLANATION
In my Rails 5 application I have,
Parent model order.rb
class Order < ApplicationRecord
has_many :tasks
end
Child model task.rb
class Task < ApplicationRecord
belongs_to :order
belongs_to :rider
end
rider.rb
class Rider < ApplicationRecord
has_many :tasks
end
Parent and Task attributes schema.rb
create_table "orders", force: :cascade do |t|
t.string "status"
t.boolean "urgent"
t.date "schedule"
t.integer "customer_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "company_id"
t.index ["company_id"], name: "index_orders_on_company_id", using: :btree
t.index ["customer_id"], name: "index_orders_on_customer_id", using: :btree
end
create_table "tasks", force: :cascade do |t|
t.boolean "task_type"
t.string "details"
t.string "address"
t.string "nearby"
t.float "lat"
t.float "lng"
t.string "name"
t.string "contact"
t.time "time"
t.string "item_type"
t.integer "order_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "rider_id"
t.integer "status"
t.index ["order_id"], name: "index_tasks_on_order_id", using: :btree
end
Now let's assume,
I have 3 orders in my database, each order has 5 tasks.
1 out of 5 tasks of first 2 orders belongs to this rider my_rider = Rider.find(1)
So basically, no matter how many tasks are there in an order, if a rider has any of the task assigned, when we show #assigned_orders on my_rider dashboard there should be all the orders in which my_rider has a task assigned, which in my case is the first 2 out of 3 orders.
I don't know if I explained it right :/
PROBLEM
I want to show the #assigned_orders of my_rider on his dashboard.
I want my ActiveRecord to make a single query and get all orders along with the their tasks.
I tried this,
my_rider = Rider.find(1)
#assigned_orders = Task.joins(:order).where(rider: my_rider)
But it gives me the tasks objects in #assigned_orders instance variable not the orders.
I cannot query with the Parent (Order) model because it has no association with the Rider model.
Please help me find a solution for this problem. Thanks in advance!
It is as simple as ABC. Add has_many :through association, to set up a connection between riders and orders.
class Rider < ApplicationRecord
has_many :tasks
has_many :orders, -> { distinct }, through: :tasks
end
And then use it.
my_rider = Rider.find(1)
#assigned_orders = my_rider.orders
You can also do it without the association.
#assigned_orders = Order.joins(:tasks).where(tasks: {rider_id: my_rider.id}).distinct
You have rider_id column in your tasks table. Therefore I guess you can make the following query:
Order.joins(:tasks).where(tasks: { rider_id: my_rider.id }).distinct

How do I build a complex nested form to represent dynamic attributes

I've got a data model represented by the following image:
Basically, the idea is that there are many different Collections. All the Items in a single collection will have the same Attributes, but the list of attributes will be different per collection. The value of each Attribute per item will be stored in Item Attribute Values.
I'm trying to build a single page where a user can populate the attributes for an item. I'm assuming a nested form is the way to go but I'm at a loss as to how to represent this in the controller and on the page, considering the names of the attributes are in one table and the values in another.
If anyone has encountered or had to deal with a similar situation, any help would be appreciated.
Thanks
Here is one potential solution.
class Collection
has_and_belongs_to_many :items
has_and_belongs_to_many :attributes
end
class Item
has_and_belongs_to_many :collections
has_many :item_attributes
has_many :attributes, though: :item_attributes
end
class Attributes
has_and_belongs_to_many :collections
has_many :item_attributes
has_many :items, though: :item_attributes
end
class ItemAttribute
belongs_to :item
belongs_to :attribute
end
So lets look at the database layout to back these models:
ActiveRecord::Schema.define(version: 20151027173337) do
create_table "attributes_collections", id: false, force: :cascade do |t|
t.integer "attribute_id", null: false
t.integer "collection_id", null: false
end
create_table "collections", force: :cascade do |t|
t.string "title"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "collections", ["user_id"], name: "index_collections_on_user_id"
create_table "collections_items", id: false, force: :cascade do |t|
t.integer "collection_id", null: false
t.integer "item_id", null: false
end
create_table "item_attributes", force: :cascade do |t|
t.integer "item_id"
t.integer "attribute_id"
t.string "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "item_attributes", ["attribute_id"], name: "index_item_attributes_on_attribute_id"
add_index "item_attributes", ["item_id"], name: "index_item_attributes_on_item_id"
create_table "items", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
A Better way?
But of course performance will suffer due to the many joins. Plus each attribute will be stored as a VARCHAR which means you can't do any numeric comparisons in the database.
If you really need a flexible schema i would instead look into using HSTORE, JSON or another dynamic column type or a schemaless database such as MongoDB.
How do I create a form / controller for this?
First you should get very acquainted with what can be done with accepts_nested_attributes_for and fields_for and maybe consider using AJAX to delegate the actions out to CollectionController and a ItemController on the back end rather than cramming it all into a single monstrosity.

Rails 4 HABTM how to set multiple ids in console?

schema.rb:
ActiveRecord::Schema.define(version: 20150324012404) do
create_table "groups", force: :cascade do |t|
t.string "title"
t.integer "teacher_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "groups_students", id: false, force: :cascade do |t|
t.integer "group_id"
t.integer "student_id"
end
add_index "groups_students", ["group_id"], name: "index_groups_students_on_group_id"
add_index "groups_students", ["student_id"], name: "index_groups_students_on_student_id"
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "admin", default: false
t.string "type"
t.integer "group_id"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
group.rb:
class Group < ActiveRecord::Base
belongs_to :teacher
has_and_belongs_to_many :students
end
student.rb:
class Student < User
has_and_belongs_to_many :groups
end
I could have set a simple belongs_to and a has_many relationship between the student and group models, but I want students to be able to belong to more than one group, so I set up a HABTM association and corresponding join table.
I think I that right?
The question is, how do I, in the console, set a Student to belong to more than one group?
I have setup a User with 'type: Student' and I have two Groups. So...
In the console I do:
student = Student.first
Then, I want to set 'student' to belong to both Groups, but I don't know how to do this.
To set it to belong to one group I can do:
student.update_attributes(group_id: 1)
But how do make it belong to both groups? It would have two group_id's wouldn't it? I don't know how to set this.
If you need to see any of the other files, it's the 'handcode' branch here:
https://github.com/Yorkshireman/sebcoles/tree/handcode
The answers others have already provided are correct. But if you're working with id's you can also do something like this
student = Student.first
student.group_ids = 1,2,3,4
You don't need to set group_id for the User, the association is handled by the join table and the HABTM statement. You should remove group_id from the users table in the schema.
From memory you should be able to do something like this:
student = Student.first
groups = Group.all
student.groups << groups
student.save
See the active record guide on HABTM associations - specfically 4.4.1.3
Instead of habtm, just use the normal through and your life becomes easy. Make sure an id is generated for the association table (remove id:false)
create_table "group_students", force: :cascade do |t|
t.integer :group_id, nil:false
t.integer :student_id, nil:false
end
class Group < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :group
has_many :students, through :group_students
end
class Student < ActiveRecord::Base
has_many :group_students, dependent: :destroy, inverse_of :student
has_many :groups, through: :group_students
end
class GroupStudent < ActiveRecord::Base
belongs_to :group,
belongs_to :student
validates_presence_of :group, :student
end
Group.last.students << Student.last
or..
Student.last.groups << Group.last
Student.last.groups = [Group.find(1), Group.find(2)]
etc....
Ok, so it took me 3 days of all kinds of pain to work this out.
There was nothing wrong with my original code, except that I needed to remove the group_id from the user table.
roo's answer was correct, except that using 'group' as a variable name in the console confused Rails. This had led me to believe there was something wrong with my code, but there wasn't. You learn the hard way.
So, Students can be pushed into Groups like this:
To push a student into one group:
student = Student.first
OR
student = Student.find(1)
(or whatever number the id is)
group1 = Group.first
OR
group1 = Group.find(1)
student.groups << group1
To push into multiple groups (which was the original goal of this whole debacle:
student = Student.first
OR
student = Student.find(1)
allclasses = Group.all
student.groups << allclasses
To view your handywork:
student.groups
Works beautifully. The only problem I can see with my code is that it's possible to push the same student into a group twice, resulting in two duplicates of that student in one group. If anyone knows how to prevent this happening, I'm all ears.

Resources