[ TLDR: Check your fixtures! (see answer below) ]
gem 'rails', '5.2.3'
I have two models, People and Puppies that are in a has_many "through" relationship. The join model is Companion.
class Person < ApplicationRecord
has_many :companions, -> { order(created_at: :asc) }
has_many :puppies, through: :companions
class Puppy < ApplicationRecord
has_many :companions
has_many :people, through: :companions
class Companion < ApplicationRecord
self.table_name = 'people_puppies' #is the table name messing things up?
belongs_to :person
belongs_to :puppy
default_scope { order(created_at: :asc) }
My problem is that created_at and updated_at timestamps are not working on the Companion model. When I try to assign a new relationship between two records...
#person.puppies << some_puppy
# or
#person.companions << some_puppy
# or
Companion.create!(puppy: some_puppy, person: #person)
...I get an DB constraint violation message:
ActiveRecord::NotNullViolation: SQLite3::ConstraintException: NOT NULL constraint failed: people_puppies.created_at: INSERT INTO "people_puppies" ("person_id", "puppy_id") VALUES (1052040621, 904095534)
Why isn't Rails adding the timestamps?
Here's the schema:
create_table "people_puppies", force: :cascade do |t|
t.integer "person_id", null: false
t.integer "puppy_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["person_id", "puppy_id"], name: "index_people_puppies_on_person_id_and_puppy_id"
t.index ["puppy_id", "person_id"], name: "index_people_puppies_on_puppy_id_and_person_id"
end
WOOPS!
The DB constraint violation wasn't actually coming from << or my test code at all. I had associations in my fixtures that were leftover from before I converted from has_and_belongs_to_many to has_many through:
Ex:
some_person:
email: foo#gmail.com
puppies:
- some_puppy
^ THIS is what was causing the DB error before my test code even started. :-/
My original question was based on incorrect assumptions, but this seems like an easy mistake to make if you refactor from HABTM to has_many through (and you have preexisting fixtures). So even though it's embarrassing, I will leave this question in case it helps someone in the future.
Related
I have to two models in the same namespace which have a habtm relation.
class Resource::Item < ApplicationRecord
has_and_belongs_to_many :resource_sets, foreign_key: 'resource_item_id', class_name: 'Resource::Set', table_name: 'resource_items_sets'
end
class Resource::Set < ApplicationRecord
has_and_belongs_to_many :resource_items, foreign_key: 'resource_set_id', class_name: 'Resource::Item', table_name: 'resource_items_sets'
end
The migration has been generated with rails g migration CreateJoinTableResourceItemsResourceSets resource_item resource_set
class CreateJoinTableResourceItemsResourceSets < ActiveRecord::Migration[5.2]
def change
create_join_table :resource_items, :resource_sets do |t|
# t.index [:resource_item_id, :resource_set_id]
# t.index [:resource_set_id, :resource_item_id]
end
end
end
So far everything looks great. The table resource_items_sets is being created with the two columns resource_item_id and resource_set_id.
This is the schema
create_table "resource_items_sets", id: false, force: :cascade do |t|
t.bigint "resource_item_id", null: false
t.bigint "resource_set_id", null: false
end
After creating an resource_item I get the following which is as expected.
pry(main)> Resource::Item.first.resource_sets
=> #<Resource::Set::ActiveRecord_Associations_CollectionProxy:0x3fdd08748004>
But doing the following throws an error. I was expecting 0.
pry(main)> Resource::Item.first.resource_sets.count
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column resource_items_sets.set_id does not exist
LINE 1: ...N "resource_items_sets" ON "resource_sets"."id" = "resource_...
^
: SELECT "resource_sets"."id" FROM "resource_sets" INNER JOIN "resource_items_sets" ON "resource_sets"."id" = "resource_items_sets"."set_id" WHERE "resource_items_sets"."resource_item_id" = $1 ORDER BY "resource_sets"."name" ASC
from /Users/username/.rvm/gems/ruby-2.6.0/gems/activerecord-5.2.2/lib/active_record/connection_adapters/postgresql_adapter.rb:677:in `async_prepare'
Caused by PG::UndefinedColumn: ERROR: column resource_items_sets.set_id does not exist
LINE 1: ...N "resource_items_sets" ON "resource_sets"."id" = "resource_...
Where does set_id come from when resource_set_id has been declare everywhere? How can I fix this issue? I want to keep the namespaces for both since I might end up creating items and set for more namespaces.
Thanks so much, guys!
You need to set the foreign keys on both sides of the join table because they can't be inferred in your case.
In this case the correct has_and_belongs_to_many calls should look like:
has_and_belongs_to_many :resource_items,
foreign_key: 'resource_set_id',
association_foreign_key: 'resource_item_id',
class_name: 'Resource::Item',
join_table: 'resource_items_sets'
end
and
class Resource::Item < ApplicationRecord
has_and_belongs_to_many :resource_sets,
foreign_key: 'resource_item_id',
association_foreign_key: 'resource_set_id',
class_name: 'Resource::Set',
join_table: 'resource_items_sets'
end
Note the added association_foreign_key option specifying the other foreign key in the join table.
ich would like to ask for some help with has_and_belongs_to_many association.
I have the following tables and models:
candidate_job_title_translations -> Candidate::JobTitleTranslation (in a subfolder with table_name_prefix )
create_table "candidate_job_title_translations", force: :cascade do |t|
end
profile_experiences, ProfileExperience
create_table "profile_experiences", id: :serial, force: :cascade do |t|
end
candidate_job_title_translations_profile_experiences, No model
create_table "candidate_job_title_translations_profile_experiences", id: false, force: :cascade do |t|
t.bigint "candidate_job_title_translation_id", null: false
t.bigint "profile_experience_id", null: false
end
The two models are setuped for the association:
class ProfileExperience < ApplicationRecord
has_and_belongs_to_many :candidate_job_title_translations, class_name: 'Candidate::JobTitleTranslation'
end
class Candidate::JobTitleTranslation < ApplicationRecord
has_and_belongs_to_many :profile_experiences, class_name: 'ProfileExperience'
end
My Problem now is, I get a ActiveRecord error, saying job_title_translation_id does not exist, which is correct. It should look for candidate_job_title_translation_id
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column candidate_job_title_translations_profile_experiences.job_title_translati
on_id does not exist
LINE 1: ...ces" ON "candidate_job_title_translations"."id" = "candidate...
I have the feeling I can solve it by not having the table_name_prefix and model structure but, that is not good in terms of my structure.
Maybe you have an idea.
Thanks
Thats not really a good domain model to start with.
If you want a translations table you want to do it something like:
class Position
belongs_to :title
has_many :translated_titles,
through: :title,
source: :translations
end
class Title
has_many :positions
has_many :translations,
class_name: 'Titles::Translation'
end
class Titles::Translation
belongs_to :title
end
You should be more concerned about creating meaningful relations and duplication than "I don't want to have another class, waaah" which is the most common reason for choosing HABTM.
Also when "namespacing" models in Rails the module should be plural:
Good: Titles::Translation
Bad: Title::Translation
This convention is due to the way that ActiveRecord maps tables to tables to classes and the fact that nesting your model inside another model class is not really a good idea.
Context:
I have two tables, challenges and challenge_steps. Both tables need to have relation between them, I need to be able to reference a Step with a Challenge and the inverse relationship.
A challenge can have multiple steps but ONLY ONE current_step.
Schema:
Challenge:
t.string "name"
t.string "subtitle"
t.text "brief", null: false
t.integer "min_team_size", default: 2, null: false
t.integer "max_team_size", default: 5, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
Challenge::Step:
t.integer "challenge_id"
t.string "name"
t.text "description"
t.datetime "start_at"
t.datetime "end_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
To do this I can think of three solutions, but none of them are satisfying:
Solution One:
Challenge Model:
has_many :steps, inverse_of: :challenge, dependent: :destroy
belongs_to :current_step, class_name: Challenge::Step
Challenge::Step:
belongs_to :challenge
has_one :challenge_relation, class_name: Challenge,
foreign_key: :current_step_id, dependent: :restrict_with_error
As you can see in my Challenge::Step model I have a belongs_to(:challenge) and the Rails documentation reads:
For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier.
So the behavior is OK, but the code looks odd.
Solution Two:
Create a table which contains challenge_id and step_id. Which will reference each challenge and its current_step
This one is good but it mean we need the read another table to get the needed info.
Solution Three:
add in the Challenge model:
has_many :steps, inverse_of: :challenge, dependent: :destroy do
def current_step
proxy_association.owner.steps.where(current_step: true).first
end
end
It returns a collection and the schema doesn't respect the real relation between a Challenge and his step.
What would most efficient and elegant? Could you think of a solution which would have none of these drawbacks ?
First of all, why is Challenge::Step a subclass of Challenge?
Surely you'd want it to be Step on its own? For the purposes of clarity, I will refer to it just as Step.
--
Here's what I'd do:
#app/models/challenge.rb
class Challenge < ActiveRecord::Base
has_many :steps
def current
steps.where(current: true).order(current: :desc).first
end
end
#app/models/step.rb
class Step < ActiveRecord::Base
# columns id | challenge_id | current (datetime) | etc...
belongs_to :challenge
end
This will give you the ability to call:
#challenge = Challenge.find params[:id]
# #challenge.steps = collection of steps
# #challenge.current_step = latest current step
The idea being that you could save your current_step attribute as a date in the Step model. This will have the added benefit of giving you the ability to see the historical record of when each step was "current".
--
An alternative would be to make a current column in the Challenge model:
#app/models/challenge.rb
class Challenge < ActiveRecord::Base
# columns id | name | current | etc
has_many :steps
def current_step
steps.find current
end
end
#app/models/step.rb
class Step < ActiveRecord::Base
#columns id | challenge_id | name | etc
belongs_to :challenge
end
This will allow you to call the following:
#challenge = Challenge.find params[:id]
# #challenge.steps = collection of steps
# #challenge.current_step = single instance of step
--
Your third solution is by far most elegant, but it assumes the structure you have implemented being correct.
I think you don't have the correct setup to handle the current_step attribute; you either need a way to distinguish it in the Step model or the Challenge model.
I think the first solution is 'The Rails Way' of doing what you need.
Maybe the only drawback there is the code readability, in the sense that a Challenge doesn't belong to a current step in literal terms, but I think a comment on the line should be enough, as then, the interface to access it is really meaningful: challenge.current_step
I have been trying and failing for 2 days now :) to get a list of ideas (posts basically) with likes. Order Desc preferably.
I have scaffolded ideas and users which work fine.
Likes (socialization gem) gives me the headache.
I can add likes and retrieve them. And I can also find out how many likes a specific idea has: idea.likers(User).count
and find out whether a user likes a specific idea: user.likes?(idea)
But I can't do agregates because of the non-standard field names which prohibit me from making a JOIN.
create_table "likes", force: :cascade do |t|
t.string "liker_type"
t.integer "liker_id" (this is/should be user_id)
t.string "likeable_type"
t.integer "likeable_id" (this is/should be idea_id)
t.datetime "created_at"
end
add_index "likes", ["likeable_id", "likeable_type"], name: "fk_likeables"
add_index "likes", ["liker_id", "liker_type"], name: "fk_likes"
Models:
like.rb - empty
user.rb - acts_as_liker
idea.rb - acts_as_likeable
Is there a way to join likes and ideas eg somehow matching liker_id to user_id? Or shall I rename the fields in the table (liker_id to user_id and likeable_id to idea_id)...? And also add these:
like.rb
belongs_to :user
belongs_to :idea
idea.rb
has_many :likes, dependent: :destroy
user.rb
has_many :likes, dependent: :destroy
Thanks in advance!
To specify a different column as foreign key which gets used in joins, you could add foreign_key: ... option to belongs_to as follows:
# app/models/like.rb
belongs_to :user, foreign_key: :liker_id
belongs_to :idea, foreign_key: :likeable_id
See referenced documentation on belongs_to.
You can also specify join conditions yourself as follows:
Idea.joins('inner join likes on ideas.id = likes.likeable_id').where(...)
I have a VideoCollection model that will contain many records from another model (called VideoWork), using the has_many relationship. The VideoCollection model inherits from the Collection model using single table inheritance, while the VideoWork model inherits from the Work model.
I'm having a problem when I try to call up the video_works that belong to a video_collection.
In my video_collection#show action, I use the following to try to display a collection's works:
def show
#video_collection = VideoCollection.find(params[:id])
#collections = #video_collection.children
#works = #video_collection.video_works
end
But when I try to use #works in the show view, I get the following:
PG::Error: ERROR: column works.video_collection_id does not exist
SELECT "works".* FROM "works" WHERE "works"."type" IN ('VideoWork') AND "works"."video_collection_id" = $1
##(Error occurs in the line that contains <% #works.each do |work| %>)
My model files:
#----app/models/video_collection.rb----
class VideoCollection < Collection
has_many :video_works
end
#----app/models/video_work.rb----
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
The "parent" models:
#----app/models/collection.rb - (VideoCollection inherits from this)
class Collection < ActiveRecord::Base
end
#----app/models/work.rb - (VideoWork inherits from this)
class Work < ActiveRecord::Base
end
The Schema file:
#----db/schema.rb----
create_table "works", force: true do |t|
t.string "header"
t.string "description"
t.string "type"
t.string "folder_id"
end
create_table "collections", force: true do |t|
t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
t.text "ancestry"
t.string "name"
t.string "tile_image_link"
end
My Question
I assume that since I have a folder_id column in the works table that I should be able to set up the belongs_to relationship properly, but it seems that Rails still wants me to have a video_collection_id column instead. I would prefer not use something specific like video_collection_id as a foreign key in the works table since I need to set up other relationships (e.g.: photo_collection has_many photo_works, etc).
What am I doing wrong here?
I don't really use has_many and belongs_to with different foreign keys than the standard, but according to the docs I would do this:
class VideoCollection < Collection
has_many :video_works, foreign_key: "folder_id"
end
class VideoWork < Work
belongs_to :folder, class_name: "VideoCollection", foreign_key: "folder_id"
end
Your Pg error says that the association is looking for 'video_collection_id' instead of 'folder_id'
Guides (chapter 4.3.2.5)