I'm creating an app to represent the pedigree of livestock. Each child has one dam (e.g. ewe) and one sire (e.g. ram). A dam/sire pairing can have multiple children (e.g. lambs) and a dam and sire may have many more children independent of the other. I am trying to represent this relationship so that I could do something like ewe.children and get a listing of her offspring. Similarly, I'd like to be able to do something like lamb.ewe to get her mother or lamb.ewe.ewe to get her maternal grandmother.
from schema.rb...
create_table "parent_child_relationships", force: :cascade do |t|
t.integer "parent_id"
t.integer "child_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["child_id"], name: "index_parent_child_relationships_on_child_id"
t.index ["parent_id", "child_id"], name: "index_parent_child_relationships_on_parent_id_and_child_id", unique: true
t.index ["parent_id"], name: "index_parent_child_relationships_on_parent_id"
end
from parent_child_relationship.rb...
class ParentChildRelationship < ApplicationRecord
belongs_to :sire, class_name: "Animal"
belongs_to :dam, class_name: "Animal"
belongs_to :children, class_name: "Animal"
end
from animal.rb...
has_one :sire_relationship, class_name: "ParentChildRelationship",
foreign_key: "child_id",
dependent: :destroy
has_one :dam_relationship, class_name: "ParentChildRelationship",
foreign_key: "child_id",
dependent: :destroy
has_many :child_relationships, class_name: "ParentChildRelationship",
foreign_key: "parent_id",
dependent: :destroy
has_one :sire, through: :sire_relationship, source: :child
has_one :dam, through: :dam_relationship, source: :child
has_many :children, through: :child_relationships, source: :parent
In the console, I run the following commands to grab the animals I want to relate to each other...
s = Shepherd.first
ewe = s.animals.find_by(id: 37)
ram = s.animals.find_by(id: 133)
lamb = s.animals.find_by(id: 61)
Now, when I try to create the sire_relationship and dam_relationship I get an error since it doesn't seem to see the relationship as being unique. The sire_relationship is replaced by the dam_relationship...
>> lamb.create_sire_relationship(parent_id: ram.id)
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 133], ["child_id", 61], ["created_at", "2018-01-15 15:33:06.649936"], ["updated_at", "2018-01-15 15:33:06.649936"]]
(2.5ms) commit transaction
ParentChildRelationship Load (0.2ms) SELECT "parent_child_relationships".* FROM "parent_child_relationships" WHERE "parent_child_relationships"."child_id" = ? LIMIT ? [["child_id", 61], ["LIMIT", 1]]
=> #<ParentChildRelationship id: 1, parent_id: 133, child_id: 61, created_at: "2018-01-15 15:33:06", updated_at: "2018-01-15 15:33:06">
>> lamb.create_dam_relationship(parent_id: ewe.id)
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 37], ["child_id", 61], ["created_at", "2018-01-15 15:33:35.045703"], ["updated_at", "2018-01-15 15:33:35.045703"]]
(1.0ms) commit transaction
ParentChildRelationship Load (0.1ms) SELECT "parent_child_relationships".* FROM "parent_child_relationships" WHERE "parent_child_relationships"."child_id" = ? LIMIT ? [["child_id", 61], ["LIMIT", 1]]
(0.0ms) begin transaction
SQL (0.5ms) DELETE FROM "parent_child_relationships" WHERE "parent_child_relationships"."id" = ? [["id", 1]]
(1.2ms) commit transaction
=> #<ParentChildRelationship id: 2, parent_id: 37, child_id: 61, created_at: "2018-01-15 15:33:35", updated_at: "2018-01-15 15:33:35">
Creating the children_relationships, I get these errors...
>> ewe.child_relationships.create(child_id: lamb.id)
(0.1ms) begin transaction
SQL (0.9ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 37], ["child_id", 61], ["created_at", "2018-01-15 15:37:11.436086"], ["updated_at", "2018-01-15 15:37:11.436086"]]
(0.1ms) rollback transaction
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: parent_child_relationships.parent_id, parent_child_relationships.child_id: INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)
from (irb):13
>> ram.child_relationships.create(child_id: lamb.id)
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "parent_child_relationships" ("parent_id", "child_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["parent_id", 133], ["child_id", 61], ["created_at", "2018-01-15 15:37:25.264947"], ["updated_at", "2018-01-15 15:37:25.264947"]]
(2.5ms) commit transaction
Finally, if I check to see whether I can access the sire of lamb, I get another error...
>> lamb.dam
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :child in model ParentChildRelationship. Try 'has_many :dam, :through => :dam_relationship, :source => <name>'. Is it one of sire, dam, or children?
from (irb):21
>> lamb.sire
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :child in model ParentChildRelationship. Try 'has_many :sire, :through => :sire_relationship, :source => <name>'. Is it one of sire, dam, or children?
from (irb):22
I get similar errors if I do ewe.children or ram.children.
I'm looking for an extra pair of eyes to tell me what I'm doing wrong or whether there's an easier way to achieve what I'm after.
The problem is that you only have one parent_id in your animals table which can only store the ID of a single parent. This works for bacteria but not for animals which have two parents. Your parent_id is getting written to when you set the dam and the sire.
There are multiple ways to do this but I think the simplest is to have a dam_id and sire_id in your animals table.
This is the migration to create the table:
class CreateAnimals < ActiveRecord::Migration[5.1]
def change
create_table :animals do |t|
t.integer :dam_id, index: true
t.integer :sire_id, index: true
t.timestamps
end
end
end
This is what your model will look like this. Notice that you need two belongs_to/has_many relationships:
class Animal < ApplicationRecord
belongs_to :dam, class_name: 'Animal'
belongs_to :sire, class_name: 'Animal'
has_many :children_as_sire, class_name: 'Animal', foreign_key: :sire_id
has_many :children_as_dam, class_name: 'Animal', foreign_key: :dam_id
def children
children_as_dam + children_as_sire
end
end
Notice the getter method children that grabs both children_as_dam and children_as_sire. This will result in two SQL queries which is not ideal. If you're tracking the sec of the Animal, you could do something like:
def children?
case sex
when 'male'
children_as_sire
when 'female'
children_as_dam
end
end
I wrote some specs to demonstrate:
require 'rails_helper'
RSpec.describe Animal, type: :model, focus: true do
it 'can be created' do
expect { Animal.create }.to_not raise_error
end
it 'can have a dam' do
animal = Animal.new
animal.update! dam: Animal.create
expect(animal.dam).to be_a(Animal)
expect(animal.sire).to be_nil
end
it 'can have a sire' do
animal = Animal.new
animal.update! sire: Animal.create
expect(animal.sire).to be_a(Animal)
expect(animal.dam).to be_nil
end
it 'can have both a dam and a sire and tell the difference' do
dam = Animal.create
sire = Animal.create
child = Animal.create dam: dam, sire: sire
expect(child.reload.dam).to eq(dam)
expect(child.reload.sire).to eq(sire)
end
it 'grandma' do
grandma = Animal.create
dam = Animal.create dam: grandma
child = Animal.create dam: dam
expect(child.reload.dam.dam).to eq(grandma)
end
it 'has children' do
sire = Animal.create
animal = Animal.create sire: sire
expect(sire.reload.children).to include(animal)
end
end
Notice that you can't add children to a model:
animal = Animal.create
animal.children << Animal.create # will raise an error
Instead, you have to manually set the sire and dam (which is probably what you want to do since you're keeping track).
Related
I'm trying to migrate my project from Rails 5.0 to 5.2
Project extensively uses creation of related models through .build_%related_model%, it was working on rails 5.0 and now it's broke.
Does this functionality removed, or should i use another syntax?
class User < ActiveRecord::Base
belongs_to :profile, inverse_of: :user
end
class Profile < ActiveRecord::Base
has_one :user, inverse_of: :profile
end
new_user = User.new
new_user.build_profile
new_user.save
Previously this code created both User and his Profile. Now this will create only User, without Profile.
Any ideas how to fix this?
irb(main):001:0> new_user = User.new
=> #<User id: nil, profile_id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> new_user.build_profile
=> #<Profile id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> new_user.save
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "profiles" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2018-04-21 13:16:23.669286"], ["updated_at", "2018-04-21 13:16:23.669286"]]
Profile Load (0.2ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
SQL (0.2ms) INSERT INTO "users" ("profile_id", "created_at", "updated_at") VALUES (?, ?, ?) [["profile_id", 1], ["created_at", "2018-04-21 13:16:23.691292"], ["updated_at", "2018-04-21 13:16:23.691292"]]
(181.1ms) commit transaction
=> true
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.integer :profile_id
t.timestamps
end
end
end
class CreateProfiles < ActiveRecord::Migration[5.1]
def change
create_table :profiles do |t|
t.timestamps
end
end
end
class User < ApplicationRecord
belongs_to :profile, inverse_of: :user
end
class Profile < ApplicationRecord
has_one :user, inverse_of: :profile
end
tested it out all worked. Copied code to answer since its not readable in comment. Your problem must be somewhere else do u get any errors or paste migration files to under question
accepts_nested_attributes_for :profile fixed this exact issue, but many other have popped up.
I had to stop this update and rollback everything.
As far as I know, assign_attributes (unlike update_attributes) is not supposed to save the record or for that matter, any record.
So it quite startled me when I discovered that this is not true when supplying _ids for a has_many through: relation.
Consider the following example:
class GroupUser < ApplicationRecord
belongs_to :group
belongs_to :user
end
class Group < ApplicationRecord
has_many :group_users
has_many :users, through: :group_users
end
class User < ApplicationRecord
has_many :group_users
has_many :groups, through: :group_users
validates :username, presence: true
end
So we have users and groups in an m-to-m relationship.
Group.create # Create group with ID 1
Group.create # Create group with ID 2
u = User.create(username: 'Johny')
# The following line inserts two `GroupUser` join objects, despite the fact
# that we have called `assign_attributes` instead of `update_attributes`
# and, equally disturbing, the user object is not even valid as we've
# supplied an empty `username` attribute.
u.assign_attributes(username: '', group_ids: [1, 26])
The log as requested by a commenter:
irb(main):013:0> u.assign_attributes(username: '', group_ids: [1, 2])
Group Load (0.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" IN (1, 2)
Group Load (0.1ms) SELECT "groups".* FROM "groups" INNER JOIN "group_users" ON "groups"."id" = "group_users"."group_id" WHERE "group_users"."user_id" = ? [["user_id", 1]]
(0.0ms) begin transaction
SQL (0.3ms) INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 1], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.691941"], ["updated_at", "2017-06-29 08:15:11.691941"]]
SQL (0.1ms) INSERT INTO "group_users" ("group_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["group_id", 2], ["user_id", 1], ["created_at", "2017-06-29 08:15:11.693984"], ["updated_at", "2017-06-29 08:15:11.693984"]]
(2.5ms) commit transaction
=> nil
I daresay that update_attributes and the _ids construct are mostly used for processing web forms - in this case a form that updates the user itself as well as its group association. So I think it is quite safe to say that the general assumption here is all or nothing, and not a partial save.
Am I using it wrong in some way?
#gokul-m suggests reading about the issue at https://github.com/rails/rails/issues/17368. One of the comments in there points to a temporary workaround: https://gist.github.com/remofritzsche/4204e399e547ff7e3afdd0d89a5aaf3e
an example of my solution to this problem:
ruby:
def assign_parameters(attributes, options = {})
with_transaction_returning_status {self.assign_attributes(attributes, options)}
end
You can handle validation with assign_attributes like so
#item.assign_attributes{ year: "2021", type: "bad" }.valid?
I've been working on implementing a new model that belongs to one of our web apps existing models. Eventually, I want to build out a nested form. I understand that the form, and strong params can have it's own suite of issues, but I am currently struggling to get the models to behave as I would expect in the rails console.
Rails 4.2.7, Postgres DB
UPDATE - 10/3/16 - Still trying to find the right solution, but have made some changes
Our work is with Schools and Districts, and this particular case deals with surveys and how a survey is assigned to a school and district. Until now, a survey has been assigned to a district with a SurveyAssignment model, and some down the line logic assumed that all schools in a district were also "assigned" to the survey. Now, we want to be able to add more granularity to the SurveyAssignment and allow some specificity at the school level.
So I created a SchoolSurveyAssignment model and started to get the bits in place.
Here is the relevant model info:
class District < ActiveRecord::Base
...
has_many :schools, dependent: :destroy
has_many :survey_assignments, dependent: :destroy
...
end
class School
...
belongs_to :district
has_many :school_survey_assignments
has_many :survey_assignments, :through => :school_survey_assignments
...
end
class SurveyAssignment
belongs_to :district
belongs_to :survey
has_one :survey_version, through: :survey
has_many :school_survey_assignments, inverse_of: survey_assignment
has_many :schools, :through => :school_survey_assignments
accepts_nested_attributes_for :school_survey_assignments
attr_accessor :survey_group, :survey_version_type, :survey_version_id, :school_survey_assignments_attributes
validates :survey_id, presence: true
end
class SchoolSurveyAssignment
belongs_to :survey_assignment, inverse_of: :school_survey_assignments
belongs_to :school
attr_accessor :school_id, :survey_assignment_id, :grades_affected, :ulc_affected
validates_presence_of :survey_assignment
validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
Relevant Controller code:
class SurveyAssignmentsController < ApplicationController
before_action :set_district
before_action :set_survey_assignment, only: [:show, :edit, :update, :destroy]
respond_to :html, :json, :js
def new
#new_survey_assignment = SurveyAssignment.new()
#district.schools.each do |school|
#new_survey_assignment.school_survey_assignments.build(school_id: school.id)
end
end
def create
#survey_assignment = SurveyAssignment.new(survey_assignment_params)
if #survey_assignment.save
flash[:notice] = "Survey successfully assigned to #{#district.name}"
else
flash[:alert] = "There was a problem assigning this survey to #{#district.name}"
end
redirect_to district_survey_assignments_path(#district)
end
def survey_assignment_params
params.require(:survey_assignment).permit(:survey_id, :status, :survey_version_id, school_survey_assignments_attributes: [:id, :survey_assignment_id, :school_id, grades_affected: [], ulc_affected: []]).tap do |p|
p[:district_id] = #district.id
p[:school_year] = session[:selected_year]
end
end
def set_district
#district = District.find(params[:district_id])
end
Here is the relevant schema info:
create_table "school_survey_assignments", force: :cascade do |t|
t.integer "survey_assignment_id"
t.integer "school_id"
t.integer "grades_affected", default: [], array: true
t.string "ulc_affected", default: [], array: true
end
add_index "school_survey_assignments", ["school_id"], name: "index_school_survey_assignments_on_school_id", using: :btree
add_index "school_survey_assignments", ["survey_assignment_id"], name: "index_school_survey_assignments_on_survey_assignment_id", using: :btree
create_table "survey_assignments", force: :cascade do |t|
t.integer "district_id"
t.integer "survey_id"
t.integer "status"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "school_year"
t.integer "last_response_status_id"
end
add_index "survey_assignments", ["district_id"], name: "index_survey_assignments_on_district_id", using: :btree
Once these were in place, I stepped into my rails console and attempted the following:
2.3.1 :002 > sa1 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
(0.2ms) BEGIN
SQL (0.7ms) INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:20.205144"], ["updated_at", "2016-09-30 21:30:20.205144"]]
(7.2ms) COMMIT
=> #<SurveyAssignment id: 369, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:20", updated_at: "2016-09-30 21:30:20", school_year: 2017, last_response_status_id: nil>
2.3.1 :003 > sa2 = SurveyAssignment.create(district_id: 3, survey_id: 508, school_year: 2017)
(0.3ms) BEGIN
SQL (0.4ms) INSERT INTO "survey_assignments" ("district_id", "survey_id", "school_year", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["district_id", 3], ["survey_id", 508], ["school_year", 2017], ["created_at", "2016-09-30 21:30:30.701197"], ["updated_at", "2016-09-30 21:30:30.701197"]]
(0.5ms) COMMIT
=> #<SurveyAssignment id: 370, district_id: 3, survey_id: 508, status: nil, created_at: "2016-09-30 21:30:30", updated_at: "2016-09-30 21:30:30", school_year: 2017, last_response_status_id: nil>
So now, I've successfully created two Survey Assignments. I'm now going to create two School Survey Assignments off of sa1:
2.3.1 :004 > [{school_id: 5}, {school_id: 6}].each do |ssa|
2.3.1 :005 > sa1.school_survey_assignments.create(ssa)
2.3.1 :006?> end
(0.2ms) BEGIN
SchoolSurveyAssignment Exists (2.4ms) SELECT 1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 5 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
SQL (0.4ms) INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id" [["survey_assignment_id", 369]]
(6.4ms) COMMIT
(0.6ms) BEGIN
SchoolSurveyAssignment Exists (0.4ms) SELECT 1 AS one FROM "school_survey_assignments" WHERE ("school_survey_assignments"."school_id" = 6 AND "school_survey_assignments"."survey_assignment_id" = 369) LIMIT 1
SQL (0.3ms) INSERT INTO "school_survey_assignments" ("survey_assignment_id") VALUES ($1) RETURNING "id" [["survey_assignment_id", 369]]
(0.4ms) COMMIT
=> [{:school_id=>5}, {:school_id=>6}]
2.3.1 :007 > sa1.save
(0.3ms) BEGIN
(0.4ms) COMMIT
=> true
Now, it looks like I've successfully created two SchoolSurveyAssignments with survey_assignment_id = 369 and school_ids = 5 and 6
2.3.1 :008 > sa1.school_survey_assignments
SchoolSurveyAssignment Load (0.3ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."survey_assignment_id" = $1 [["survey_assignment_id", 369]]
=> #<ActiveRecord::Associations::CollectionProxy [#<SchoolSurveyAssignment id: 5, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>, #<SchoolSurveyAssignment id: 6, survey_assignment_id: 369, school_id: nil, grades_affected: [], ulc_affected: []>]>
As you can see from the ActivRecord::Associations::CollectionProxy, both of the SchoolSurveyAssignments were created, with survey_assignment_id: 369, but with a nil school_id. This is troubling as it seems to be
Ignoring the parameters being passed into the create function, and
ignoring the validation of school_id
Another item that I don't understand is the following:
2.3.1 :009 > SchoolSurveyAssignment.find(5).survey_assignment_id
SchoolSurveyAssignment Load (0.6ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1 [["id", 5]]
=> nil
2.3.1 :011 > SchoolSurveyAssignment.find(5).survey_assignment.id
SchoolSurveyAssignment Load (0.3ms) SELECT "school_survey_assignments".* FROM "school_survey_assignments" WHERE "school_survey_assignments"."id" = $1 LIMIT 1 [["id", 5]]
SurveyAssignment Load (0.4ms) SELECT "survey_assignments".* FROM "survey_assignments" WHERE "survey_assignments"."id" = $1 LIMIT 1 [["id", 369]]
=> 369
Calling .survey_assignment_id should return the attribute on the SchoolSurveyAssignment and give 369. .survey_assignment.id is simply just grabbing the parent object's ID. I would expect both to return the same value, but one returns nil.
The end use case is making a SurveyAssignment form that lets the user set the attributes for a new SurveyAssignment and also set the attributes for X number of SchoolSurveyAssignments (based on # of schools in a district; varies from 2 to 15). Once I get a better grasp on how these models are interacting, I feel confident in executing this goal, but the behavior I'm seeing doesn't make sense to me, and I was hoping to find some clarity on implementing these related models. I feel like I'm bouncing around the answer, but am missing a key detail.
Thanks,
Alex
Try removing your attr_accessor lines of code. attr_accessor shouldn't be used for attributes that are persisted in the database and it's probably messing up the methods that ActiveRecord already provides by default, causing those attributes to not be saved properly
class SurveyAssignment
belongs_to :district
belongs_to :survey
has_one :survey_version, through: :survey
has_many :school_survey_assignments, inverse_of: survey_assignment
has_many :schools, :through => :school_survey_assignments
accepts_nested_attributes_for :school_survey_assignments
validates :survey_id, presence: true
end
class SchoolSurveyAssignment
belongs_to :survey_assignment, inverse_of: :school_survey_assignments
belongs_to :school
validates_presence_of :survey_assignment
validates :school_id, presence: true, uniqueness: {scope: :survey_assignment_id}
end
For the first question, School and SurveyAssignment don't know each other, school_id becomes nil. In your app, these models have m-to-n association indirectly, so how about using has_many through association between models?
In School model, add below:
has_many :survey_assignments, :through => :school_survey_assignments
In SurveyAssignments model, add below:
has_many :schools, :through => :school_survey_assignments
For the last question, both codes seem to be same..
Whenever I instantiate a new ActiveRecord model (one that has not been persisted to the database) and attempt to access some various associations on the built model, the Rails query builder will sometimes:
Add a (1=0) predicate to the where clause of the query.
Add a 'distinct` clause to the select statement.
I think this only occurs when the has_many :through association is joining two or more tables.
I want to know why it adds the (1=0) predicate as well as the distinct clause. For the (1=0) predicate, it shouldn't matter if the new model has been saved to the database or not (right?). I have no idea why the distinct clause is being added.
I have a simple example below.
class Assignment < ActiveRecord::Base
has_many :assignment_attachments
has_many :attachments, through: :assignment_attachments
end
class AssignmentAttachment < ActiveRecord::Base
belongs_to :assignment
belongs_to :attachment
end
class Attachment < ActiveRecord::Base
has_many :assignment_attachments
has_many :assignments, through: :assignment_attachments
end
class Submission < ActiveRecord::Base
belongs_to :assignment
has_many :assignment_attachments, through: :assignment
has_many :attachments, through: :assignment
end
s = Submission.new(assignment: Assignment.first)
s.assignment #=> #<Assignment ...>
s.assignment_attachments #=> [#AssignmentAttachment id: '1'>, #AssignmentAttachment assignment_id: '1', attachment_id: '1' ...>]
s.attachments #=> []
Here's the sql query for s.attachments:
SELECT DISTINCT attachments.*
FROM attachments
INNER JOIN assignment_attachments ON attachments.id = assignment_attachments.attachment_id
INNER JOIN assignments ON assignment_attachments.assignment_id = assignments.id
WHERE assignments.id = 'a0dbfdc7-0d67-4aad-ad06-6a7a5a91d2d0' AND (1=0)
Located somewhere deep in one of the subtrees of the abstract syntax tree that arel builds:
# the 'distinct' select clause
#<Arel::Nodes::SelectCore:0x007ffe43d45be0
#groups=[],
#having=nil,
#projections=
[#<struct Arel::Attributes::Attribute
relation=
#<Arel::Table:0x007ffe45a7be58
#aliases=[],
#columns=nil,
#engine=
Attachment(name: string, description: text, created_at: datetime, updated_at: datetime, required: boolean, id: uuid, slug: string, file_types: string),
#name="attachments",
#primary_key=nil,
#table_alias=nil>,
name="*">],
#set_quantifier=#<Arel::Nodes::Distinct:0x007ffe43d44dd0>,
...
# the (1=0) predicate
#wheres=
[#<Arel::Nodes::And:0x007ffe43d45028
#children=
[#<Arel::Nodes::Equality:0x007ffe45958788
#left=
#<struct Arel::Attributes::Attribute
relation=
#<Arel::Table:0x007ffe45958e68
#aliases=[],
#columns=nil,
#engine=ActiveRecord::Base,
#name="assignments",
#primary_key=nil,
#table_alias=nil>,
name="id">,
#right=#<Arel::Nodes::BindParam:0x007ffe45958878>>,
#<Arel::Nodes::Grouping:0x007ffe43d45050 #expr="1=0">]>]
Do you know why arel is building the distinct clause and the (1=0) predicate? I can use some workarounds to get what I want - however, I would love to be able to investigate and find out why and how this tree is built.
Thanks for any/all advice.
Note this is an edit to reflect new info from OP. With the new info I have recreated the scenario in a fresh project by copy and pasting OP's code plus the following migration.
Migration:
class CreateAttachments < ActiveRecord::Migration
def change
create_table :attachments do |t|
t.integer :assignment_attachment_id
t.timestamps null: false
end
create_table :assignments do |t|
t.integer :assignment_attachment_id
t.timestamps null: false
end
create_table :assignment_attachments do |t|
t.integer :assignment_id
t.integer :attachment_id
t.timestamps null: false
end
create_table :submissions do |t|
t.integer :assignment_id
t.timestamps null: false
end
end
end
Works fine when associations exist in DB
When I save the Attachment and Assignment models to which I will associate my Submission model s, even though never save s, it works fine.
irb(main):001:0> attachment = Attachment.new
=> #<Attachment id: nil, assignment_attachment_id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> assignment = Assignment.new
=> #<Assignment id: nil, assignment_attachment_id: nil, created_at: nil, updated_at: nil>
irb(main):005:0> assignment.attachments << attachment
=> #<ActiveRecord::Associations::CollectionProxy [#<Attachment id: nil, assignment_attachment_id: nil, created_at: nil, updated_at: nil>]>
irb(main):006:0> assignment.save
(0.2ms) begin transaction
SQL (1.5ms) INSERT INTO "assignments" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2015-10-14 17:10:34.936929"], ["updated_at", "2015-10-14 17:10:34.936929"]]
SQL (0.6ms) INSERT INTO "attachments" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2015-10-14 17:10:34.944453"], ["updated_at", "2015-10-14 17:10:34.944453"]]
SQL (0.3ms) INSERT INTO "assignment_attachments" ("assignment_id", "attachment_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["assignment_id", 1], ["attachment_id", 1], ["created_at", "2015-10-14 17:10:34.947481"], ["updated_at", "2015-10-14 17:10:34.947481"]]
(0.8ms) commit transaction
=> true
irb(main):007:0> s = Submission.new(assignment: Assignment.first)
Assignment Load (0.2ms) SELECT "assignments".* FROM "assignments" ORDER BY "assignments"."id" ASC LIMIT 1
=> #<Submission id: nil, assignment_id: 1, created_at: nil, updated_at: nil>
irb(main):008:0> s.assignment
=> #<Assignment id: 1, assignment_attachment_id: nil, created_at: "2015-10-14 17:10:34", updated_at: "2015-10-14 17:10:34">
irb(main):009:0> s.assignment_attachments
AssignmentAttachment Load (0.2ms) SELECT "assignment_attachments".* FROM "assignment_attachments" INNER JOIN "assignments" ON "assignment_attachments"."assignment_id" = "assignments"."id" WHERE "assignments"."id" = ? [["id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<AssignmentAttachment id: 1, assignment_id: 1, attachment_id: 1, created_at: "2015-10-14 17:10:34", updated_at: "2015-10-14 17:10:34">]>
irb(main):010:0> s.attachments
Attachment Load (0.2ms) SELECT "attachments".* FROM "attachments" INNER JOIN "assignment_attachments" ON "attachments"."id" = "assignment_attachments"."attachment_id" INNER JOIN "assignments" ON "assignment_attachments"."assignment_id" = "assignments"."id" WHERE "assignments"."id" = ? [["id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Attachment id: 1, assignment_attachment_id: nil, created_at: "2015-10-14 17:10:34", updated_at: "2015-10-14 17:10:34">]>
irb(main):011:0> s.attachments.to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"assignment_attachments\" ON \"attachments\".\"id\" = \"assignment_attachments\".\"attachment_id\" INNER JOIN \"assignments\" ON \"assignment_attachments\".\"assignment_id\" = \"assignments\".\"id\" WHERE \"assignments\".\"id\" = 1"
Generates "1=0" predicate when there are no associations
If I do not specify any associations to the Submission instance, however, when I ask for its attachments, the query does get the 1=0 predicate. As this post describes, the 1=0 predicate is added when you are trying to retrieve records that join on an array of ids that is actually empty. This is true here, since we made sure that there are no assignment ids we can use for joining to attachment.
irb(main):007:0> Submission.new.attachments.to_sql
=> "SELECT \"attachments\".* FROM \"attachments\" INNER JOIN \"assignment_attachments\" ON \"attachments\".\"id\" = \"assignment_attachments\".\"attachment_id\" INNER JOIN \"assignments\" ON \"assignment_attachments\".\"assignment_id\" = \"assignments\".\"id\" WHERE \"assignments\".\"id\" = NULL AND (1=0)"
irb(main):008:0>
Notice how it says WHERE \"assignments\".\"id\" = NULL. It can't leave it at that because it doesn't want to assume that there aren't null ids in the assignments table that would cause it to return false positives. With the additional 1=0 predicate, you are ensured to get the correct answer: an empty result.
DISTINCT
I am unable to reproduce a scenario where DISTINCT appears.
According to the Rails' guide, the has_many :through asssociation is used for the classic many-to-many connection.
In this context, I the (1=0) predicate makes sense. Let me explain with an example.
Consider the Assignment, AssignmentAttachment, and Attachment models from the question above:
Assignment.new.attachments.to_sql
SELECT attachments.*
FROM attachments INNER JOIN assignment_attachments ON attachments.id = assignment_attachments.attachment_id
WHERE assignment_attachments.assignment_id = NULL AND (1=0)
In code above, a new Assignment instance, not yet persisted to the database, tries to access attachments associated with it. Obviously, since the model has not been saved, there are no attachments associated with it and the query builder adds a (1=0) predicate.
I still don't know why the DISTINCT clause doesn't get added to the query for a classic many-to-many relationship but does when the has_many :through relationship goes through >= 2 tables.
The Issue
The rails console will not save my record to the database if it has a belongs_to association.
2.1.1 :002 > Track.create name: 'asdfasdf'
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "records" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2014-06-12 15:58:20.868095"], ["updated_at", "2014-06-12 15:58:20.868095"]]
(45.9ms) commit transaction
=> #<Track id: nil, record_id: 1, name: "asdfasdf", created_at: nil, updated_at: nil>
class Record < ActiveRecord::Base
has_many :tracks
accepts_nested_attributes_for :tracks, allow_destroy: true
end
class Track < ActiveRecord::Base
belongs_to :record
end
class CreateRecords < ActiveRecord::Migration
def change
create_table :records do |t|
t.string :title
t.timestamps
end
end
end
class CreateTracks < ActiveRecord::Migration
def change
create_table :tracks do |t|
t.belongs_to :record
t.string :name
t.timestamps
end
end
end
As you can see the :id field is nil. The :record_id field gets incremented instead.
I have tried resetting the primary key and the issue still persists. This is however not my main beef because the record is not even saved after the insert.
2.1.1 :003 > Track.last
Track Load (0.3ms) SELECT "tracks".* FROM "tracks" ORDER BY "tracks"."id" DESC LIMIT 1
=> nil
This is an issue because it prevents me from using nested forms like this one. Indecently when I download and run that rails app it works perfectly.
2.1.1 :002 > Answer.create content: 'swwaaagggggs'
(0.2ms) begin transaction
SQL (0.8ms) INSERT INTO "answers" ("content", "created_at", "updated_at") VALUES (?, ?, ?) [["content", "swwaaagggggs"], ["created_at", "2014-06-12 16:46:04.989529"], ["updated_at", "2014-06-12 16:46:04.989529"]]
(58.9ms) commit transaction
=> #<Answer id: 3, question_id: nil, content: "swwaaagggggs", created_at: "2014-06-12 16:46:04", updated_at: "2014-06-12 16:46:04">
Yet when I recreate it exactly myself I run into the same problem.
Strangely
If I rollback migrations and remove the association I get this.
2.1.1 :003 > Track.create name: 'swagger'
(0.2ms) begin transaction
SQL (0.7ms) INSERT INTO "records" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2014-06-12 16:06:12.443845"], ["updated_at", "2014-06-12 16:06:12.443845"]]
(0.2ms) rollback transaction
ActiveModel::MissingAttributeError: can't write unknown attribute `record_id'
class Record < ActiveRecord::Base
end
class Track < ActiveRecord::Base
end
class CreateRecords < ActiveRecord::Migration
def change
create_table :records do |t|
t.string :title
t.timestamps
end
end
end
class CreateTracks < ActiveRecord::Migration
def change
create_table :tracks do |t|
t.string :name
t.timestamps
end
end
end
Note the reference to :record_id when the column no longer exists
I am at a loss.
Google has run out of pages and nobody else seem to have this issue which had led me to think that there has been something wrong with my code for the last 2 days; "something this well documented shouldn't be this hard," I thought. If anyone can help me figure this out I would be grateful.
Please let me know if you require more information this is the first time I have felt the need to post to StackOverflow.
Record is probably a reserved key word in rails ?
http://reservedwords.herokuapp.com/words/record?q[word_or_notes_cont]=record