Access the join-model attribute stored in the join table created with #create_join_table

In the Rails application (4.1.5 / ruby ​​2.0.0p481 / win64), I have a many-to-many relationship between Student and Course, and the StudentCourse join model, which is an association that has the additional β€œstart” attribute, which set to false by default.

I also added an index to the connection table made from student_id and course_id, and set a unique check for it, like this

t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course' 

Now I see that associations are created either:

 Student.first.courses.create(:name => "english") 

or

 Course.first.students << Student.first 

This is good, and I guess this is the expected behavior.

What I care for is the right way to get and set the "start" attribute. I see strange behavior when accessing this attribute from other models, and not directly from the join model.

 s = Student.create c = Course.create(:name => "english") s.student_courses.first 

=> | "English" | false | # (presented as a table for practicality)

 s.student_courses.first.started = true 

=> | "English" | true |

 s.save 

=> true

Ok, it looks like it was saved, but when I mine ak:

 StudentCourse.first 

=> | 1 | 1 | false |

Thus, it is set to true if I look at the student’s nested attributes, but it is still erroneous in the connection model. I also tried doing a "reboot"! but it doesn’t matter, and they will mantle their own value.

If something goes so bad that the values ​​are not really saved, I need to say, instead of getting the β€œtruth” when saving, because otherwise how bad can the consequences be? What am I missing here?

In any case, if I try to change the "start" attribute in the connection model directly, I run into another problem:

 StudentCourse.first.started = true 

Student download (1.0ms) SELECT "student_courses". * FROM "student_courses" LIMIT 1 => true

 StudentCourse.first.started 

=> false

He has not changed!

 StudentCourse.find_by(:student_id => "10", :course_id => "1").started = true 

=> true

 StudentCourse.find_by(:student_id => "10", :course_id => "1").started 

=> false

Same as before .. I try:

 StudentCourse.find(1).started = true 

ActiveRecord :: UnknownPrimaryKey: unknown primary key for student_courses table in StudentCourse model.

Then with:

 sc = StudentCourse.first sc.started = true 

=> true

 sc 

=> | 1 | 1 | true |

seems great, but when saved:

 sc.save 

(0.0ms) start a transaction

SQL (1.0ms) UPDATE "student_courses" SET "start" =? WHERE "student_courses". "IS NULL [[" start "," true "]] SQLite3 :: SQLException: there is no such column: student_courses .: UPDATE" student_courses "SET" start "=? WHERE" student_courses "." "NULL (1.0 ms) rollback transaction ActiveRecord :: StatementInvalid: SQLite3 :: SQLException: there is no such column: student_courses .: UPDATE "student_courses" SET "start" =? WHERE is "student_cursses". " "NULL from C: /Ruby200-x64/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.9-x64-mingw32/lib/sqlite3/database.rb: 91: in` Initialization '


  • So, I think all this is due to the lack of a primary key in the join table?

  • But I'm not sure how to use it, and if this is a good practice for the case I'm trying to solve?

  • Also, if this is a problem, why then I do not get the same warning here when I save the student after I do s.student_courses.first.started = true , as shown in the examples above?


Code

student.rb

 class Student < ActiveRecord::Base has_many :student_courses has_many :courses, :through => :student_courses end 

course.rb

 class Course < ActiveRecord::Base has_many :student_courses has_many :students, :through => :student_courses end 

student_course.rb

 class StudentCourse < ActiveRecord::Base belongs_to :course belongs_to :student end 

schema.rb

 ActiveRecord::Schema.define(version: 20141020135702) do create_table "student_courses", id: false, force: true do |t| t.integer "course_id", null: false t.integer "student_id", null: false t.string "started", limit: 8, default: "pending", null: false end add_index "student_courses", ["course_id", "student_id"], name: "by_course_and_student", unique: true create_table "courses", force: true do |t| t.string "name", limit: 50, null: false t.datetime "created_at" t.datetime "updated_at" end create_table "students", force: true do |t| t.string "name", limit: 50, null: false t.datetime "created_at" t.datetime "updated_at" end end 

create_join_table.rb (transfer for connection table)

 class CreateJoinTable < ActiveRecord::Migration def change create_join_table :courses, :students, table_name: :student_courses do |t| t.index [:course_id, :student_id], :unique => true, :name => 'by_course_and_student' t.boolean :started, :null => false, :default => false end end end 
+1
source share
3 answers

Well, I finally realized what was going on here:

If you create a connection table in hyphenation using #create_join_table , this method will not create a default primary key called "id" (and not add an index for it), which is the default rails when using #create_table .

ActiveRecord needs a primary key to build its queries, because it is the default column for operations such as Model.find(3) .

Also, if you think you can get around this by doing something like StudentCourse.find_by(:course_id => "1", :student_id => "2").update_attributes(:started => true) [0] , he it will still fail, because after the found record, AR will still try to update it by looking at the "identifier" of the found record.

Also StudentCourse.find_by(:course_id => "1", :student_id => "2").started = true will return true, but, of course, it does not persist until you name #save on it. If you assign it to var relationship , and then you call relationship.save , you will see that it cannot save for the above reasons.


[0] In the connection table, I did not want to duplicate entries for "student_id" and "course_id", so during the migration process I explicitly added a unique constraint for them (using a unique index).

This made me think that I no longer needed the primary key to uniquely identify the record, because I had these two values ​​... I thought adding an index to them was enough for them to work as a primary key ... but this not this way. You need to explicitly define the primary key if you are not using the default id.

It also turns out that Rails does not support composite primary keys , and even if I wanted to add a primary key assembly for these two values ​​(so they make them a primary key and a unique index, for example default rails "id" works), that would not be possible.

There is a gem for this: https://github.com/composite-primary-keys/composite_primary_keys


So, the end of the story, as I fixed it, just added t.column :id, :primary_key to the migration to create a connection table. In addition, I would not create a connection table with #create_join_table , but instead would only use #create_table (which would automatically create an "id").

Hope this helps someone else.

Also this answer to another question was very useful, thanks @Peter Alfvin!

+2
source

OK, it looks like you do not have a primary key (we will get confirmation soon) in your connection table. When trying to access the connection table, you need to have a primary key.

I would suggest that your migration would be:

 class CreateStudentCourses < ActiveRecord::Migration def change create_table :student_courses do |t| t.references :course t.references :student t.boolean :started, default: false t.timestamps t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course' end end end 

The model definitions look good, so this will be the only change I see that needs to be done.

After that, doing what you do should work correctly. You must create a connection and then access it after creation. If you want to set the logical value to true at creation, you will need to create a record using the StudentCourse model with the necessary information (student_id, course_id and start = true), and not through any association.

 StudentCourse.create(course_id: course.id, student_id: student.id, started: true) 
+1
source
 s = Student.create c = Course.create(:name => "english") s.student_courses.first.started = true s.save 

I think the key here is in the first line that you posted (presented above). s is an instance of the student, and when you call s.save, you ask the student to save any changes to his attributes. However, there are no changes to the save because you made changes to the association.

You have several options. If you prefer a direct access approach from your piece of code, then the following should work.

 s = Student.create c = Course.create(:name => 'english') s.courses << c s.student_courses.first.update_attributes(:started => true) 

Another alternative would be to use the accepts_nested_attributes_for macro to expose the initial attribute from the student's point of view.

 class Student has_many :student_courses, :inverse_of => :student has_many :courses, :through => :student_courses accepts_nested_attributes_for :student_courses end s = Student.create c = Course.create(:name => 'english') s.courses << c s.update_attributes(:student_courses_attributes=>[{:id => 1, :started => true}]) 
-1
source

Source: https://habr.com/ru/post/949370/


All Articles