Rails self-referential has_many association - ruby-on-rails

Using Ruby on Rails, I think I need to create a self-referential has_many association to model words in Chinese.
Background:
Each word is a composite of multiple component words.
For example, if I have three words, 'ni', 'hao', and 'nihao', I want to be able to do:
nihao.components = ['ni', 'hao']
and
'ni'.composites = ['nihao']
'hao'.composites =['nihao']
I don't believe this should be a hierarchical association (I've seen several gems... ) because a word doesn't have 1 or 2 "parents", it has 0, 1, or hundreds of "composites". Likewise a word has 0, 1, or several "components".
I've tried:
class Word < ActiveRecord::Base
has_many :relationships
has_many :components, through: :relationships, foreign_key: :component_id
has_many :composites, through: :relationships, foreign_key: :composite_id
end
class Relationship < ActiveRecord::Base
belongs_to :component, class_name: "Word"
belongs_to :composite, class_name: "Word"
end
This isn't quite correct as I am unable to add components:
nihao.components << ni
(0.2ms) BEGIN
(0.2ms) ROLLBACK
ActiveModel::UnknownAttributeError: unknown attribute 'word_id' for Relationship.
from (irb):5
Database schema:
create_table "relationships", force: :cascade do |t|
t.integer "component_id"
t.integer "composite_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "words", force: :cascade do |t|
t.string "characters"
t.string "pinyin"
t.string "opinyin"
t.string "tpinyin"
t.string "english"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

Try this, you were not associating your models properly for this kind of use case.
class Word < ActiveRecord::Base
has_many :component_relationships, class_name: 'Relationship', foreign_key: :composite_id
has_many :composite_relationships, class_name: 'Relationship', foreign_key: :component_id
has_many :components, through: :component_relationships
has_many :composites, through: :composite_relationships
end
class Relationship < ActiveRecord::Base
belongs_to :component, class_name: "Word", foreign_key: :component_id
belongs_to :composite, class_name: "Word", foreign_key: :composite_id
end
I have not tried this, but this should work.

Related

Rails : double association with same Model

I created a double "Synonym" association with the same Word model as following:
class Synonym < ApplicationRecord
belongs_to :word_origin, :class_name => "Word"
belongs_to :word_synonym, :class_name => "Word"
end
Here is my Word model:
class Word < ApplicationRecord
has_many :word_synonyms, :class_name => 'Word', :foreign_key => 'word_synonym_id', dependent: :destroy
has_many :word_origins, :class_name => 'Word', :foreign_key => 'word_origin_id', dependent: :destroy
end
However, when I try to delete a word, I get the following error:
ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column
words.word_synonym_id does not exist)
Any idea what's wrong with my double association? Thanks.
EDIT: here is the schema of Word and Synonym
create_table "words", force: :cascade do |t|
t.string "content"
t.string "slug"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["content"], name: "index_words_on_content", opclass: :gin_trgm_ops, using: :gin
end
create_table "synonyms", force: :cascade do |t|
t.bigint "word_origin_id"
t.bigint "word_synonym_id"
t.index ["word_origin_id"], name: "index_synonyms_on_word_origin_id"
t.index ["word_synonym_id"], name: "index_synonyms_on_word_synonym_id"
end
Word is referencing itself without the needed columns. You have to change the line to be
has_many :word_synonyms, class_name: 'Synonym',
foreign_key: 'word_synonym_id', dependent: :destroy
has_many :word_origins, class_name: 'Synonym',
foreign_key: 'word_origin_id', dependent: :destroy

How to implement multiple associations between models?

I have 4 models.
UserModel
SkillModel
UserSkillModel
PreferenceSkillModel
Association I used:
UserModel
has_many :skills, through: :user_skills
has_many :user_skills
has_many :skills, :through: :preference_skills
has_many :preference_skills
SkillModel
has_many :users, through: :user_skills
has_many :users_skills
has_many :users, :through: :preference_skills
has_many :preference_skills
UserSkillModel
belongs_to :user
belongs_to :skill
PreferenceSkillModel
belongs_to :user
belongs_to :skill
Schema:
create_table "preference_skills", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.integer "user_id", default: 0
t.integer "skill_id", default: 0
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "user_skills", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.integer "user_id"
t.integer "skill_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "skills", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.string "name", default: ""
t.integer "skill_count", default: 0
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
How can I implement this association in a correct way?
How can I implement this association in a correct way?
What is correct may depend on what you are trying to do. The following is correct, by naming separate associations for users-skills and users-preference_skills.
class User
has_many :user_skills
has_many :my_skills, through: :user_skills, class_name: 'Skill'
has_many :preference_skills
has_many :my_preference_skills, :through: :preference_skills, class_name: 'Skill'
...
end
class SkillModel
has_many :users_skills
has_many :direct_users, through: :user_skills, class_name: 'User'
has_many :preference_skills
has_many :preference_users, :through: :preference_skills, class_name: 'User'
...
end
If you want one collection with all the skills you need to use STI or polymorphism. Read the Rails Guides for more information.
I don't think there's a need for separate user_skills & preference_skills models.
You can use STI here like -
skill.rb
Skill < ApplicationRecord
end
user_skill.rb
UserSkill < Skill
end
preference_skill.rb
PreferenceSkill < Skill
end
Note: The Skill model that I've used is different from what you are assuming.
You might want to rename your current Skills model to more explainatory name like SkillDetail.
After this you can have your associations like -
class User < AR
has_many :skills
has_many :skill_details, through: :skills
end
class Skill < AR
belongs_to :user
belongs_to :skill_detail
end
class SkillDetail < AR
has_many :skills
has_many :users, through: :skills
end
P.S. Do some research on Single Table Inheritance (STI)
Hope this helps.
Also note that you got a downvote because this is a very vague question & you are directly asking to implement a business logic.

Error creating DB entry in model with nested has_many association

Setup:
Reservations can be assigned multiple Resources. A reservation-resource combo can have multiple SetUps.
I tried to set up the model like this:
class SetUp < ApplicationRecord
has_many :reservation_resource_set_ups, dependent: :destroy
has_many :reservations, through: :reservation_resource_set_ups
has_many :resources, through: :reservation_resource_set_ups
end
class Resource < ApplicationRecord
has_many :reservation_resources, dependent: :destroy
has_many :reservation_resource_set_ups, dependent: :destroy
has_many :reservations, through: :reservation_resources
has_many :set_ups, through: :reservation_resource_set_ups
end
class Reservation < ApplicationRecord
has_many :reservation_resources, dependent: :destroy
has_many :reservation_resource_set_ups, dependent: :destroy
has_many :resources, through: :reservation_resources
has_many :set_ups, through: :reservation_resource_set_ups
end
class ReservationResource < ApplicationRecord
belongs_to :reservation
belongs_to :resource
has_many :reservation_resource_set_ups
has_many :set_ups, through: :reservation_resource_set_ups
end
class ReservationResourceSetUp < ApplicationRecord
belongs_to :reservation
belongs_to :resource
belongs_to :set_up
end
Steps:
Create a reservation, assigning a resource, works:
res1 = Reservation.create(name:"res name")
res1.resources << Resource.find(1) # resource with id = 1 exists
The reservations and reservation_resources tables are updated correctly.
Assign a setup to the reservation_resource, fails:
res1.resources.first.set_ups << SetUp.find(1) # set_ups with id = 1 exists
This fails with error ActiveRecord::RecordInvalid (Validation failed: Reservation must exist)
Can you help point me in the right direction? Thanks!
(Here's the schema, if helpful...)
create_table "reservation_resource_set_ups", force: :cascade do |t|
t.integer "reservation_id"
t.integer "resource_id"
t.integer "set_up_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["reservation_id"], name: "index_reservation_resource_set_ups_on_reservation_id"
t.index ["resource_id"], name: "index_reservation_resource_set_ups_on_resource_id"
t.index ["set_up_id"], name: "index_reservation_resource_set_ups_on_set_up_id"
end
create_table "reservation_resources", force: :cascade do |t|
t.integer "reservation_id"
t.integer "resource_id"
t.text "comments"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["reservation_id"], name: "index_reservation_resources_on_reservation_id"
t.index ["resource_id"], name: "index_reservation_resources_on_resource_id"
end
create_table "reservations", force: :cascade do |t|
t.string "name"
...
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["end_date"], name: "index_reservations_on_end_date"
t.index ["repeat_end_date"], name: "index_reservations_on_repeat_end_date"
t.index ["start_date"], name: "index_reservations_on_start_date"
end
create_table "resources", force: :cascade do |t|
t.string "name"
t.text "description"
t.string "resource_type"
t.text "location"
t.integer "quantity", default: 1
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "set_ups", force: :cascade do |t|
t.string "name"
t.text "instructions"
t.string "image"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
<< method useless when you need to setup more than one foreign key.
The collection<< method adds one or more objects to the collection by setting their foreign keys to the primary key of the calling model.
So you cat treat the code
res1.resources.first.set_ups << SetUp.find(1) # set_ups with id = 1 exists
as
ReservationResourceSetUp.create(
set_up: SetUp.find(1), # from << SetUp.find(1)
resource: res1.resources.first, # from left side
reservation: nil # raises the error
)
To create the entry, just specify all the keys:
ReservationResourceSetUp.create(
set_up: SetUp.find(1),
resource: res1.resources.first,
reservation: res1
)
You have some problems with your models. For example:
ReservationResource has_many :reservation_resource_set_ups. ActiveRecord assumes a reservation_resource_id in ReservationResourceSetUp
but
ReservationResourceSetUp does not belongs_to :reservation_resource
I suggest some changes in your models:
class Resource < ApplicationRecord
has_many :reservation_resources, dependent: :destroy
has_many :reservations, through: :reservation_resources
has_many :reservation_resource_set_ups, through: :reservation_resources
has_many :set_ups, through: :reservation_resource_set_ups
end
class Reservation < ApplicationRecord
has_many :reservation_resources, dependent: :destroy
has_many :resources, through: :reservation_resources
has_many :reservation_resource_set_ups, through: :reservation_resources
has_many :set_ups, through: :reservation_resource_set_ups
end
class ReservationResource < ApplicationRecord
belongs_to :reservation
belongs_to :resource
has_many :reservation_resource_set_ups
has_many :set_ups, through: :reservation_resource_set_ups
end
class SetUp < ApplicationRecord
has_many :reservation_resource_set_ups, dependent: :destroy
has_many :reservations_resources, through: :reservation_resource_set_ups
has_many :resources, through: :reservation_resources
has_many :reservations, through: :reservation_resources
end
class ReservationResourceSetUp < ApplicationRecord
belongs_to :reservation_resource
belongs_to :set_up
end
The main change is that ReservationResourceSetUp now belongs to ReservationResource and SetUp (instead of Reservation, Resource and SetUp). In practice it's the same, but I think it handles your situation in a better way. You first create a Reservation for a Resource. This ReservationResource is then assigned one or more SetUps. I think your code would work if you make these changes. You must of course change your migrations. Now your ReservationResourceSetUp will have a reservation_resource_id and a set_up_id.

how to update schema after adding "accepts_nested_attributes_for" to a model

here is my model
class Lineup < ApplicationRecord
has_many :artists
accepts_nested_attributes_for :artists
belongs_to :event
end
class Artist < ApplicationRecord
has_many :events, :through => :lineups
has_many :lineups
end
when running this in the console
Lineup.new(artists_attributes: [{artist_id: 1}, {artist_id: 2}])
the error is ActiveModel::UnknownAttributeError: unknown attribute 'artists_attributes' for Lineup. Obviously, I can't just drop something like that into a model and expect any changes to be from that alone. Do I need to run a migration for this? If so, what needs to be in it?
schema:
create_table "artists", force: :cascade do |t|
t.string "name"
t.text "bio"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "lineup_id"
t.integer "event_id"
end
add_index "artists", ["event_id"], name: "index_artists_on_event_id", using: :btree
add_index "artists", ["lineup_id"], name: "index_artists_on_lineup_id", using: :btree
create_table "lineups", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "artist_id"
t.integer "event_id"
end
add_index "lineups", ["artist_id"], name: "index_lineups_on_artist_id", using: :btree
add_index "lineups", ["event_id"], name: "index_lineups_on_event_id", using: :btree
I would set it up like this
schema
table Lineup
...
table Artist
...
table LineupArtists
lineup_id: Integer
artist_id: Integer
models
class Artist < ApplicationRecord
has_many :lineup_artists, dependent: :destroy
has_many :lineups, through: :lineup_artists
end
class LineupArtist < ApplicationRecord
belongs_to :lineup
belongs_to :artist
accepts_nested_attributes_for :lineup
end
class Lineup < ApplicationRecord
has_many :lineup_artists, inverse_of: :lineup, dependent: :destroy
has_many :artists, through: lineup_artists
accepts_nested_attributes_for :lineup_artists
end
The way you currently have it (with Artists having a lineup_id and Lineups having an artist_id) each model can only have one of the other (i.e. an Artist can have one lineup and vise versa). A join table gives the ability to have a many-to-many relationship.
Setting up nested attributes is a little trickier with a many-to-many but I believe what I posted should cover it. If not got some of the weirder bits from this post (https://robots.thoughtbot.com/accepts-nested-attributes-for-with-has-many-through). Hope this helps.
Try to replace line:
has_many :artists
With this one:
has_many :artists, foreign_key: "lineup_id"

How do I write this has_many_to_many dependency?

I am trying to create a has many to many relationship between a Goal model. A goal can have dependent goals and a goal can have others goals that rely on it.
So far I have come up with the below, but it does not seem to be working.
class Goal < ActiveRecord::Base
belongs_to :goal_status
belongs_to :goal_type
has_many :users, through: :user_goals
has_many :user_goals
has_many :dependers, class_name: 'GoalDependency', foreign_key: :dependee_id
has_many :dependees, class_name: 'GoalDependency', foreign_key: :depender_id
has_many :dependencies, through: :dependees
has_many :depending, through: :dependers
validates_presence_of :goal_status_id, :goal_type_id
end
class GoalDependency < ActiveRecord::Base
belongs_to :dependee, class_name: 'Goal', foreign_key: 'dependee_id'
belongs_to :depender, class_name: 'Goal', foreign_key: 'depender_id'
end
Schema
create_table "goal_dependencies", force: :cascade do |t|
t.integer "dependee_id"
t.integer "depender_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "goals", force: :cascade do |t|
t.integer "goal_status_id"
t.integer "goal_type_id"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I am getting the error
Could not find the source association(s) "dependency" or :dependencies in model GoalDependency. Try 'has_many :dependencies, :through => :dependees, :source => <name>'. Is it one of dependee or depender?
I have tried putting in a couple of different values as the source, but nothing is working. I am not really familiar that much with using source.
I would guess this is possible in rails. Any ideas?
After taking #Pavan's advice I changed the language around and managed to get it working that way. See the code below.
class Goal < ActiveRecord::Base
belongs_to :goal_status
belongs_to :goal_type
has_many :users, through: :user_goals
has_many :user_goals
has_many :parent_goals, class_name: 'GoalDependency', foreign_key: :parent_id
has_many :child_goals, class_name: 'GoalDependency', foreign_key: :child_id
has_many :children, through: :child_goals
has_many :parents, through: :parent_goals
validates_presence_of :goal_status_id, :goal_type_id
end
class GoalDependency < ActiveRecord::Base
belongs_to :parent, class_name: 'Goal', foreign_key: 'parent_id'
belongs_to :child, class_name: 'Goal', foreign_key: 'child_id'
end
Schema
create_table "goals", force: :cascade do |t|
t.integer "goal_status_id"
t.integer "goal_type_id"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "goal_dependencies", force: :cascade do |t|
t.integer "parent_id"
t.integer "child_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

Resources