Unable to define Active Record Associations properly - ruby-on-rails

I have got the following models,
class City < ActiveRecord::Base
belongs_to :state
belongs_to :country
end
class State < ActiveRecord::Base
belongs_to :country
has_many :cities
end
class Country < ActiveRecord::Base
has_many :states
has_many :cities, :through => :state
end
This is my schema.rb,
create_table "cities", :force => true do |t|
t.string "name"
t.string "state_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "countries", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "states", :force => true do |t|
t.string "name"
t.string "country_id"
t.datetime "created_at"
t.datetime "updated_at"
end
This is my seed data,
country_in = Country.create(name: 'India')
state_ap = country_in.states.create(name: 'Andhra Pradesh')
state_mh = country_in.states.create(name: 'Maharashtra')
city_hyd = state_ap.cities.create(name: 'Hyderabad')
state_ap.cities.create([{name: 'Tirupathi'}, {name: 'Visakhapatnam'}])
state_mh.cities.create([{name: 'Mumbai'}, {name: 'Pune'}, {name: 'Thane'}])
Problem
When I try to find all cities under "India" using
country_in.cities
I get this error : ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association :state in model Country
When I try to find which country the city "Hyderabad" is, using
city_hyd.country , I get nil
Why are the links between cities and countries not present?
Are my associations wrong is there something else I missed out on?

The missing link here is the following (see the "Rails Guides for Associations"):
class Country < ActiveRecord::Base
has_many :states
has_many :cities, :through => :states
end
class City < ActiveRecord::Base
belongs_to :state
def country
state ? state.country : nil
end
end
My changes were the following:
has_many :through needs the plural form, not the singular.
If you use belongs_to in City, there needs to be a state_id defined in the table (which would be redundant). So I have replaced it through a getter method. A corresponding setter method is not possible.
I hope that now works for you.

Related

How to set an instance of a model as an attribute of another model in Rails?

I am fairly new to Rails and working on an app that will allow a user to make a List containing their top 5 Items of a certain category. The main issue I'm having is how to keep track of the List order (which should be allowed to change and will be different for each User)?
My Items can belong to many Lists and my Lists should have many Items so, as of now, I am using a has_and_belongs_to_many association for both my Item and List models.
My idea to keep track of the list order right now is to have my #list have 5 attributes: one for each ranking on the list (ie. :first, :second, :third, :fourth, :fifth) and I am attempting to associate the #item instance to the #list attribute (ie. #list.first = #item1, #list.second = #item2 , etc...). Right now I am saving the #list attribute to the #item ID (#list.first = 1), but I would prefer to be able to call the method .first or .second etc and have that point directly at the specific Item instance.
Here is my current schema for lists, items, and the join table list_nominations required for the has_and_belongs_to_many association-which I'm pretty sure I am not utilizing correctly (the :points attribute in items will be a way of keeping track of popularity of an item:
create_table "lists", force: :cascade do |t|
t.integer "user_id"
t.integer "category_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "first"
t.string "second"
t.string "third"
t.string "fourth"
t.string "fifth"
end
create_table "items", force: :cascade do |t|
t.string "name"
t.integer "category_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "points", default: 0
end
and here is the code currently in my List and Item models:
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_and_belongs_to_many :items
end
class Item < ApplicationRecord
belongs_to :category
has_and_belongs_to_many :lists
end
Is there a way to do this or any suggestions on a better way to keep track of the List order without creating multiple instances of the same Item?
I'm afraid your tables don't fit any known approach, you can achieve what you want but this is not a perfect nor a recommended solution, you could specify the primary key on many has_one associations inside lists but in items it's not very possible to have all lists in one association but you can have an instance method which query lists and returns the matched ones
the hacky solution:
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_one :first_item, primary_key: :first, class_name: "Item"
has_one :second_item, primary_key: :second, class_name: "Item"
has_one :third_item, primary_key: :third, class_name: "Item"
has_one :fourth_item, primary_key: :fourth, class_name: "Item"
has_one :fifth_item, primary_key: :fifth, class_name: "Item"
end
class Item < ApplicationRecord
belongs_to :category
def lists
List.where(
"first = ? OR second = ? OR third = ? OR fourth = ? OR fifth = ?", self.id, self.id, self.id, self.id, self.id
)
end
end
you can read about how to create a many-to-many relationship via has_and_belongs_to_many associations here: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association (your tables will need a field to properly point to each other)
What I recommend doing is following a many-to-many through relationship guide (mono-transitive association) :
you will need 1 extra table because you want to track the order(first,second, etc)
DB:
create_table "lists", force: :cascade do |t|
.. all your other fields without first,second, etc..
end
create_table "items", force: :cascade do |t|
.. all your other fields
end
create_table "lists_items", force: :cascade do |t|
t.integer "list_id"
t.integer "item_id"
t.integer "rank" there is where you will store your order (first, second ..) bas as an integer
end
Models:
class ListsItem < ApplicationRecord
belongs_to :list
belongs_to :item
end
class List < ApplicationRecord
belongs_to :user
belongs_to :category
has_many :lists_items, -> { order(:rank) }, limit: 5
has_many :items, through: :lists_items
end
class Item < ApplicationRecord
belongs_to :category
has_many :lists_items
has_many :lists, through: :lists_items
end
you can read more about many-to-many via has_many through here https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
and the difference between the 2 approaches here https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many

Active Record how to add records to has_many :through association in rails

I have quite the Problem with my has_many :through association.
The models look like this:
class User < ActiveRecord::Base
has_many :roles
has_many :datasets, through: :roles
has_secure_password
end
class Role < ActiveRecord::Base
belongs_to :user
belongs_to :dataset
end
class Dataset < ActiveRecord::Base
has_many :roles
has_many :users, through: :roles
end
I want to add a record to roles every time a new Dataset is created. It should
be created with the new Dataset, an existing User 'Admin' and the column name of Role should be set to 'Admin'.
I tried everything I found on Stackoverflow but nothing works for me.
My create method in the DatasetController looks this:
def create
#dataset = Dataset.new(dataset_params)
#dataset.save
#user = User.find_by(name: 'Admin')
#dataset.users << #user
##dataset.roles.create(user: #user, dataset: #dataset, name: 'Admin')
respond_with(#dataset)
end
I tried both the << operator and the create method.
the first results in:
ActiveRecord::UnknownAttributeError in DatasetsController#create
unknown attribute: dataset_id
the second in:
ActiveModel::MissingAttributeError in DatasetsController#create
can't write unknown attribute `user_id
Does anyone know why I get these errors?
my schema.rb:
create_table "datasets", force: true do |t|
t.string "name"
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "roles", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", force: true do |t|
t.string "name"
t.string "mail"
t.string "password_digest"
t.datetime "created_at"
t.datetime "updated_at"
end
Your Role model needs columns to refer to which User and Dataset it belongs. Without these it has no idea who belongs to who.
So you simply need to create a migration to add these columns:
class AddRefererColumnsToRole < ActiveRecord::Migration
def change
add_column :roles, :user_id, :integer
add_column :roles, :dataset_id, :integer
end
end

How to create an ActiveRecord object with a many-to-many relationship

I have created a blank rails app (rails new cheese_shop), with two models and a join table. I am trying to create a cheese shop and specifying which cheeses it contains, at creation time, like this:
cheeses = [
Cheese.create!(name: "Bree"),
Cheese.create!(name: "Kačkavalj"),
]
Shop.create! name: "The Whistling Cheese", cheeses: cheeses
However, I'm getting this error:
SQLite3::ConstraintException: NOT NULL constraint failed: stocks.shop_id: INSERT INTO "stocks" ("cheese_id", "created_at", "updated_at") VALUES (?, ?, ?)
Apparently, the shop ID is not inserted to the stocks table when I create the shop. Is it possible to fix this, without having to do it in two steps (i.e. without first creating the Shop, and then adding the cheeses?)
Here are my models:
class Cheese < ActiveRecord::Base
has_many :shops, through: :stocks
has_many :stocks
end
class Shop < ActiveRecord::Base
has_many :cheeses, through: :stocks
has_many :stocks
end
class Stock < ActiveRecord::Base
belongs_to :shop
belongs_to :cheese
end
My migrations look like this:
class CreateTables < ActiveRecord::Migration
def change
create_table :cheeses do |t|
t.string :name, null: false
t.timestamps null: false
end
create_table :shops do |t|
t.string :name, null: false
t.timestamps null: false
end
create_table :stocks do |t|
t.integer :shop_id, null: false
t.integer :cheese_id, null: false
t.integer :amount
t.float :price
end
end
end
maybe you should try to use nested attributes:
class Shop < ActiveRecord::Base
has_many :cheeses, through: :stocks
has_many :stocks
accepts_nested_attributes_for :stocks
end
and then you will be able to do something like:
cheese = Cheese.create!(name: "Bree")
params = { attrib: { name: "The Whistling Cheese", stocks_attributes: { cheese_id: cheese.id} } }
Shop.create params[:attrib]
here is doc: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
It turns out Rails creates the associations in two steps, first leaving out the Shop ID, then setting the Shop IDs with an UPDATE, all in one transaction. So The NOT NULL constraints are causing the problem.
Changing this:
t.integer :shop_id, null: false
t.integer :cheese_id, null: false
…to this:
t.integer :shop_id
t.integer :cheese_id, null: false
…solves the problem, although I'm unhappy with this since now I cannot rely on the database to ensure the integrity of my data.

Belongs To Association Table Schema Ruby on Rails

I want each student to be able to post multiple messages on my site.
therefore each student has_many :posts
and a post belongs_to :student (one student only)
The thing is I can create a record for a student in rails console but can't assign a post to the student ? I am a bit confused. The student model with the has many does not have the attributes from the belongs to model ?
I have a student.rb model
class Student < ActiveRecord::Base
attr_accessible :first_name, :last_name, :email, :gender, :number, :college, :password, :budget, :picture
mount_uploader :picture, PictureUploader
has_many :posts
end
I have a post.rb model
class Post < ActiveRecord::Base
attr_accessible :message
belongs_to :student
end
this is my schema
ActiveRecord::Schema.define(version: 20130827191617) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "posts", force: true do |t|
t.text "message"
end
create_table "students", force: true do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.string "number"
t.string "college"
t.string "password"
t.float "budget"
t.string "picture"
t.datetime "created_at"
t.datetime "updated_at"
end
You should add student_id integer column (it would be better if there was also index on this column) to posts table.
To do this, you can type in console:
bundle exec rails g migration add_student_id_to_posts student:references

How do I model this relationship in Rails

I'm a bit new to rails so if this is very basic please bear with me.
I am creating something like a chat application. My model looks like this (from schema.rb)
create_table "people", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "session_id"
end
create_table "sessions", :force => true do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.integer "leader_id"
end
I've set up the basic relationship I want -- which is session_id is a foreign key in the people table as such:
class Person < ActiveRecord::Base
belongs_to :session
end
class Session < ActiveRecord::Base
has_many :people
end
I want the leader_id to now be the person who is the "host" of the session (therefore its a foreign key to exactly one person). How do I setup an additional association so I can do:
session = Session.find_by_id(1)
host = Person.new(:name => 'joebob')
session.leader = host
You can tell Rails that the leader_id is really a Person id by specifying the class name in the belongs_to association:
class Session < ActiveRecord::Base
belongs_to :leader, :class_name => "Person"
end

Resources