访问存储在使用 #create_join_table 创建的连接表中的连接模型属性

jj_*_*jj_ 2 ruby activerecord many-to-many ruby-on-rails nested-attributes

在 Rails ( 4.1.5 / ruby​​ 2.0.0p481 / win64 ) 应用程序中,我有 Student 和 Course 之间的多对多关系和表示关联的连接模型 StudentCourse,它有一个名为“started”的附加属性,它默认设置为“false”。

我还在由 student_id 和 course_id 组成的连接表中添加了一个索引,并设置了一个唯一的检查,就像这样

t.index [:student_id, :course_id], :unique => true, :name => 'by_student_and_course'
Run Code Online (Sandbox Code Playgroud)

现在我看到关联是通过以下任一方式创建的:

Student.first.courses.create(:name => "english")
Run Code Online (Sandbox Code Playgroud)

或者

Course.first.students << Student.first
Run Code Online (Sandbox Code Playgroud)

这很好,我想这是预期的行为。

我正在寻找的是获取和设置“开始”属性的正确方法。 从其他模型而不是直接从连接模型访问该属性时,我看到了一个奇怪的行为。

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

s.student_courses.first
Run Code Online (Sandbox Code Playgroud)

=> | "英语" | 假| #(为了实用,以表格的形式表示)

s.student_courses.first.started = true
Run Code Online (Sandbox Code Playgroud)

=> | "英语" | 真实|

s.save
Run Code Online (Sandbox Code Playgroud)

=> 真

好的,这看起来已经保存了,但是当我抢劫 ak 时:

StudentCourse.first
Run Code Online (Sandbox Code Playgroud)

=> | 1 | 1 | 假|

因此,如果我通过学生嵌套属性,则将其设置为 true,但在连接模型中它仍然为 false。我也尝试过“重新加载!” 但这没有区别,它们将保持自己不同的价值。

如果事情变得如此糟糕以至于值实际上并没有被持久化,我应该被告知而不是在保存时得到“真实”,否则后果会有多糟糕?我在这里缺少什么?

无论如何,如果我尝试直接修改连接模型上的“started”属性,则会遇到另一种问题:

StudentCourse.first.started = true
Run Code Online (Sandbox Code Playgroud)

StudentCourse Load (1.0ms) SELECT "student_courses".* FROM "student_courses" LIMIT 1 => true

StudentCourse.first.started
Run Code Online (Sandbox Code Playgroud)

=> 假

它没有改变!

StudentCourse.find_by(:student_id => "10", :course_id => "1").started = true
Run Code Online (Sandbox Code Playgroud)

=> 真

StudentCourse.find_by(:student_id => "10", :course_id => "1").started
Run Code Online (Sandbox Code Playgroud)

=> 假

和以前一样..我尝试:

StudentCourse.find(1).started = true
Run Code Online (Sandbox Code Playgroud)

ActiveRecord::UnknownPrimaryKey:模型 StudentCourse 中表 student_courses 的未知主键。

然后:

sc = StudentCourse.first
sc.started = true
Run Code Online (Sandbox Code Playgroud)

=> 真

sc
Run Code Online (Sandbox Code Playgroud)

=> | 1 | 1 | 真实|

看起来不错,但保存时:

sc.save
Run Code Online (Sandbox Code Playgroud)

(0.0ms) 开始事务

SQL (1.0ms) UPDATE "student_courses" SET "started" = ? WHERE "student_courses"."" IS NULL [["started", "true"]] SQLite3::SQLException: 没有这样的列:student_courses.: UPDATE "student_courses" SET "started" = ? WHERE "student_courses"."" IS NULL (1.0ms) 回滚事务 ActiveRecord::StatementInvalid: SQLite3::SQLException: 没有这样的列: student_courses.: UPDATE "student_courses" SET "started" = ? WHERE "student_courses"."" 从 C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.9-x64-mingw32/lib/sqlite3/database.rb:91:in 中为 NULL ‘初始化’


  • 所以我认为这一切都与连接表中没有主键有关?

  • 但是我不确定如何使用它,如果这代表了我正在尝试解决的案例的好习惯?

  • 另外,如果这是问题所在,为什么我在保存学生之后没有收到相同的警告 s.student_courses.first.started = true,如上面的示例所示?


代码

学生.rb

class Student < ActiveRecord::Base

  has_many :student_courses
  has_many :courses, :through => :student_courses

end
Run Code Online (Sandbox Code Playgroud)

课程.rb

class Course < ActiveRecord::Base
  has_many :student_courses
  has_many :students, :through => :student_courses
end
Run Code Online (Sandbox Code Playgroud)

student_course.rb

class StudentCourse < ActiveRecord::Base
  belongs_to :course
  belongs_to :student
end
Run Code Online (Sandbox Code Playgroud)

模式文件

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
Run Code Online (Sandbox Code Playgroud)

create_join_table.rb(连接表的迁移)

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
Run Code Online (Sandbox Code Playgroud)

jj_*_*jj_ 5

好的,我终于明白了这里发生了什么:

如果您创建使用迁移一个连接表#create_join_table,此方法将不会创建一个名为“ID”的默认主键(而不是将它添加一个索引),它是用什么时,导轨(默认设置)#create_table

ActiveRecord 需要一个主键来构建其查询,因为在执行诸如Model.find(3).

此外,如果您认为可以通过执行类似StudentCourse.find_by(:course_id => "1", :student_id => "2").update_attributes(:started => true) [0]之类的操作来解决此问题,它仍然会失败,因为在找到记录后,AR 仍会尝试更新它,查看它找到的记录的“id”。

StudentCourse.find_by(:course_id => "1", :student_id => "2").started = true将返回 true 但当然它不会被保存,直到您对其调用 #save 。如果你将它分配给一个 varrelationship然后你调用relationship.save你会看到由于上述原因它无法保存。


[0] 在连接表中,我不想要“student_id”和“course_id”的重复记录,因此在迁移中我明确地为它们添加了唯一约束(使用唯一索引)。

这让我觉得我不再需要一个主键来唯一标识一条记录,因为我有这两个值......我认为在它们上添加一个索引就足以让它们作为主键工作......但事实并非如此。当您不使用默认的“id”时,您需要明确定义一个主键。

事实证明,Rails 不支持复合主键,因此即使我想在这两个值上添加主键构建(因此使它们成为主键和唯一索引,就像默认的 Rails“id”一样有效)它也会有不可能。

存在一个宝石:https : //github.com/composite-primary-keys/composite_primary_keys


所以,故事的结尾,我修复它的方式只是添加t.column :id, :primary_key到连接表创建的迁移中。我也可以不创建连接表,#create_join_table而是使用 just #create_table(这会自动创建一个“id”)。

希望这对其他人有帮助。

另外这个对另一个问题的回答非常有帮助,谢谢@Peter Alfvin!