【问题标题】:Accessing a join-model attribute stored in a join table created with #create_join_table访问存储在使用 #create_join_table 创建的连接表中的连接模型属性
【发布时间】:2014-11-05 18:00:12
【问题描述】:

在 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'

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

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

Course.first.students << Student.first

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

我所关注的是获取和设置“started”属性的正确方法。 从其他模型而不是直接从连接模型访问该属性时,我看到了一种奇怪的行为。

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

s.student_courses.first

=> | "英文" |假 | # (表示为实用表)

s.student_courses.first.started = true

=> | "英文" |真的 |

s.save

=> 是的

好吧,看起来它已经被保存了,但是当我抢劫 ak 时:

StudentCourse.first

=> | 1 | 1 |假 |

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

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

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

StudentCourse.first.started = true

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

StudentCourse.first.started

=> 错误

它没有改变!

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

=> 是的

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

=> 错误

和以前一样..我尝试:

StudentCourse.find(1).started = true

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

然后用:

sc = StudentCourse.first
sc.started = true

=> 是的

sc

=> | 1 | 1 |真的 |

看起来不错,但保存时:

sc.save

(0.0ms) 开始交易

SQL (1.0ms) UPDATE "student_courses" SET "started" = ?在哪里 "student_courses"."" 为 NULL [["started", "true"]] SQLite3::SQLException:没有这样的列:student_courses。:更新 "student_courses" SET "开始" = ? WHERE "student_courses"."" 为空 (1.0ms) 回滚事务 ActiveRecord::StatementInvalid: SQLite3::SQLException:没有这样的列:student_courses。:更新 "student_courses" SET "开始" = ? WHERE "student_courses"."" 是 NULL 来自 C:/Ruby200-x64/lib/ruby/gems/2.0.0/gems/sqlite3-1.3.9-x64-mingw32/lib/sqlite3/database.rb:91:in `初始化'


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

  • 但我不太确定如何使用它以及这是否代表 我试图解决的案例的良好做法?

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


代码

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(连接表的迁移)

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

【问题讨论】:

  • 请列出您对 Student、Class 和 StudentClass 的模型定义。
  • @nikkon226 用代码更新问题
  • StudentCourse 架构是id, course_id, student_id, started, created_at, updated_at
  • 添加到问题 schema.rb 和用于连接表创建的迁移代码。

标签: ruby-on-rails ruby activerecord many-to-many nested-attributes


【解决方案1】:

好的,我终于知道这里发生了什么:

如果您使用#create_join_table 在迁移中创建连接表,此方法将创建名为“id”的默认主键(并且不为其添加索引),这就是 rails使用#create_table时默认会这样做。

ActiveRecord 需要一个主键来构建它的查询,因为它是在执行Model.find(3) 之类的操作时默认使用的列。

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

StudentCourse.find_by(:course_id =&gt; "1", :student_id =&gt; "2").started = true 也将返回 true,但它当然不会保存,直到您调用 #save。如果将其分配给 var relationship,然后调用 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 创建连接表,而是只使用#create_table(这会自动创建一个“id”)。

希望这对其他人有所帮助。

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

【讨论】:

  • 这确实帮助了某人……我!谢谢:)
【解决方案2】:

好的,您的连接表中似乎没有主键(我们很快就会得到确认)。尝试访问连接表时确实需要有一个主键。

我建议您的迁移是:

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

模型定义看起来不错,所以这是我认为需要进行的唯一更改。

在那之后,做你一直在做的事情应该可以正常工作。您将创建连接,然后在创建后访问它。如果您想在创建时将布尔值分配为 true,则需要通过 StudentCourse 模型使用您需要的信息(student_id、course_id 和started = true)而不是通过任一关联来创建记录。

StudentCourse.create(course_id: course.id, student_id: student.id, started: true)

【讨论】:

    【解决方案3】:
    s = Student.create
    c = Course.create(:name => "english")
    s.student_courses.first.started = true
    s.save
    

    我认为这里的线索在您发布的第一行(如上所示)。 s 是学生的一个实例,当您调用 s.save 时,您要求学生保存对其属性的任何更改。但是,没有任何更改要保存,因为您对关联进行了更改。

    您有几个选择。如果您更喜欢代码 sn-p 中的直接访问方法,那么以下应该可以工作。

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

    另一种选择是使用accepts_nested_attributes_for 宏从学生的角度公开started 属性。

    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}])
    

    【讨论】:

    • > s.student_classes.first.update_attributes(:started => true) (0.0ms) begin transaction SQL (0.0ms) UPDATE "student_courses" SET "activated" = ? WHERE "student_courses"."" IS NULL [["started", "t"]] SQLite3::SQLException: no such column: student_courses.: UPDATE "student_courses" SET "activated" = ? WHERE "student_courses"."" IS NULL (0.0ms) rollback transaction ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: student_courses.: UPDATE "student_courses" SET "started" = ? WHERE "student_courses"."" IS NULL from .../lib/sqlite3/database.rb:91:in `initialize'
    • 这并不是我更喜欢一种或另一种方法(1 - 直接在连接模型实例上设置属性 vs 2 - 从模型实例嵌套属性访问它)。我试图了解这一切是如何工作的,以及我看到我看到的错误的原因。您确定需要“accept_nested_attributes_for”吗?模型嵌套不应该隐含在声明与“has_many ... :through”和“belongs_to”的关系吗?
    • 我认为这与“accept_nested_attributes_for”不允许嵌套属性访问无关。我在上面的评论中粘贴的错误表明 ActiveRecord 尝试构造一个查询来查找属性列名称,例如 WHERE "student_courses".""。最后两个双引号是空的,因为没有设置 rails 默认用来标识记录的主键“id”键,因为迁移中的 #create_join_table 方法不会创建它。
    • 抱歉,这里的细节变化太大了,很难跟上。
    • 这么多?我只是将模型名称从“Class”切换到“Course”以避免混淆。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多