【问题标题】:Rendering a JSON object of a join-model and its associated models呈现连接模型及其关联模型的 JSON 对象
【发布时间】:2015-01-03 23:08:37
【问题描述】:

在 Rails ( 4.1.5 / ruby​​ 2.0.0p481 / win64 ) 应用程序中,StudentCourse 之间存在多对多关系以及连接模型StudentCourse 表示关联,并且有一个附加属性,称为started(默认设置为“false”)。

我还在由 student_idcourse_id 组成的连接表中添加了一个索引,并对其设置了唯一检查,如下所示

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

我希望它是一个复合主键,但由于在 rails 中没有复合主键(不使用 gem),我还添加了一个名为 id 的主键:

t.column :id, :primary_key

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

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

Course.first.students << Student.first

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


也就是说,我正在努力围绕 ActiveRecord 查询中的关联解决方案。让我更好地解释一下:

作为参考,Student.allCourse.allStudentCourses.all 将返回如下表格:

学生.all

+----+-----------+
| id | name      |
+----+-----------+
| 1  | Aidan     |
| 2  | Alison    |
| 3  | Elizabeth |
+----+-----------+

课程.all

+----+----------+------------------+
| id | name     | description      |
+----+----------+------------------+
| 1  | english  | desc. here       |
| 2  | music    | desc. here       |
| 3  | dance    | desc. here       |
| 4  | science  | desc. here       |
| 5  | french   | desc. here       |
| 6  | arts     | desc. here       |
+----+----------+------------------+

StudentCourse.all

+-------------+------------+------------+----+
| course_id   | student_id | started    | id |
+-------------+------------+------------+----+
| 1           | 1          | false      | 1  |
| 2           | 1          | false      | 2  |
| 3           | 1          | false      | 3  |
| 1           | 2          | true       | 4  |
| 2           | 2          | true       | 5  |
| 4           | 3          | false      | 6  |
| 5           | 2          | false      | 7  |
| 5           | 1          | true       | 8  |
| 6           | 2          | false      | 9  |
+-------------+------------+------------+----+


到目前为止,我可以愉快地渲染所有课程的 json 对象,以及每门课程的所有学生的姓名,如下所示:

render json: Course.all.to_json(:include => {:students => {:only => :name}})

我还可以轻松呈现学生正在或即将参加的所有课程

render json: @student.courses.to_json(:include => {:students => {:only => :name}})

其中还包括参加这些课程的其他学生。

但假设我想渲染一个学生尚未开始的课程,以及正在学习该课程的所有其他学生(已开始或未开始该课程) [请阅读下面的更新部分,实际上我正在寻找相反的东西!]

我认为最简单的方法是查询连接表,例如:

StudentCourse.all.where(student_id: @student.id, started: false)

+-------------+------------+------------+----+
| course_id   | student_id | started    | id |
+-------------+------------+------------+----+
| 5           | 2          | false      | 7  |
| 6           | 2          | false      | 9  |
+-------------+------------+------------+----+

但是我如何从这个结果表(关联对象)中继续获得一个带有课程名称(和所有其他属性)以及 包括学生的打包好的 json 对象,就像我对:Course.all.to_json(:include =&gt; {:students =&gt; {:only =&gt; :name}}) 所做的那样?

我认为我在这里缺少一些重要关键概念的基本知识,但此时我什至无法识别它们,非常感谢一些帮助......谢谢。



更新:

我刚刚意识到以下部分是我最初想要做的。这是相反的事情。在所有这些细节中,我迷路了。我希望我在这里添加它就可以了。

因此,给定一个学生(我们称他为 Aiden),我需要返回一个 JSON 对象,其中仅包含他所参加的课程以及 他已经开始 当此类课程中有其他学生尚未开始他们时,它还必须包括每门课程的学生姓名。

所以...

我现在有:

aiden_started_courses = Student(1).courses.where(:student_courses => {:started => true } )

对于 student 而言,它采用所有在连接表“started”列中具有“true”值的 courses。 (同样,在连接表中,每个 student-course 记录都是“复合”唯一的,因此给定的 student_id 和 course_id 只能有一个唯一记录。

通过下一个查询,对于 one 的“aiden_started_courses”,我可以取消所有在“started”上具有错误值的相关 student-courses 关联

aiden_started_courses[0].student_courses.where(:started => false).includes(:student).to_json(:include => :student)

+-------------+------------+------------+----+
| course_id   | student_id | started    | id |
+-------------+------------+------------+----+
| 1           | 2          | false      | 4  |
+-------------+------------+------------+----+
| 1           | 9          | false      | 5  |
+-------------+------------+------------+----+

所以问题就在这里:我已经设法为 aiden_started_courses 数组中的一门课程获得了这个,但是我如何能够构建一个查询来返回此数据em>所有艾登开始的课程?

是否有可能在一行中做到这一点?我知道我可能可以使用 Ruby 枚举器循环,但我有点觉得我会在 Rails 编码约定级别和性能级别上打破一些模式? (再次遇到 N+1 问题?) ...


到目前为止我能做到的:

我想出了这个方法,我可以在其中找到所有尚未开始给定用户已经开始的课程的学生:

Student.includes(:student_courses).
where(:student_courses => { :started => false, :course_id => aiden.courses.where
           (:student_courses => {started: true}).ids  } ) 

或者这个:

Course.includes(:students).where(:student_courses => {:started => false, 
   :course_id => aiden.courses.where(:student_courses => {:started =>true}).ids })

如果这些课程包括尚未开始的学生,则查找给定学生已开始的所有课程

但我真正需要的是获得这样的 JSON 对象:

[
    {
        "id": 1,
        "name": "english",
        "students": [
            {"name": "ALison"},
            {"name": "Robert"}]
    },
    {
        "id": 2,
        "name": "music",
        "description": null,
        "students": [
            {"name": "Robert"},
            {"name": "Kate"}]
    }
]


我可以在其中看到给定学生正在上和已经开始的课程,但只有那些还有其他学生尚未开始的课程,以及这些学生的名字......

我在想,我可能无法通过常规的 AR 查询来获得它,所以也许应该手动构建 JSON?但我怎么能这样做呢?


谢谢你的建议。我为冗长道歉..但希望它会有所帮助..

【问题讨论】:

  • 在末尾追加.to_jsonStudentCourse.all.where(student_id: @student.id, started: false).to_json
  • 这不能回答我的问题。我已经到了这个(确切的)点。我知道添加“to_json”会呈现 JSON ......但我想要呈现的不仅仅是那个!请仔细阅读问题的最后部分。
  • 是的,对不起。要自动填充学生/课程,您应该在各自的 ActiveRecords 上指定 :belongs_to:has_many 属性,如下所示:api.rubyonrails.org/classes/ActiveRecord/Associations/…
  • 这已经完成了!我在第一行说这个! “我在学生和课程之间有多对多的关系”......这意味着模型中的 has_many ... :through ...s 和 belongs_tos。如果您真的必须发表评论,请您更加注意阅读问题吗?谢谢。

标签: ruby-on-rails ruby json activerecord custom-object


【解决方案1】:

使用 JBuilder,Rails 默认自带。忽略以“#”开头的行:

Jbuilder.new do |j|

    # courses: [{
    j.courses <student.courses - replace this with whatever variable> do |course|

      # id: <course.id>
      j.id course.id 

      # name: <course.name>
      j.name course.name

      # students: [{
      j.students <course.students - replace with whatever variable> do |student|

          # name: <student.name>
          j.name student.name

      end
      # }]

   end
   # }]

end

代码不多。删除 cmets 并简化一些功能,它看起来像:

student_courses = <...blah...>
json = Jbuilder.new do |j|
  j.courses student_courses do |course|
    j.(course, :id, :name)
    j.students <course.students - whatever method here>, :name
  end
end.target!

查看他们的guide,在纯 Ruby DSL 中生成 JSON 非常棒。因此,继续使用您想要获取学生/课程/学生课程的任何 ruby​​ 代码。

【讨论】:

  • 谢谢,点赞。我会研究JBuilder,我认为这是一个很好的建议,但由于我只是在学习,我也想知道如何首先做我想做的“基本”方式,以便更深入地了解涉及的部分和相关的编码模式。
  • 好的,谢谢。如果您只是想调用.to_json 并获得您正在寻找的结构,那么它不是最佳的,因为您首先必须构建具有所有嵌套结构的完整对象。复杂 JSON 的推荐方法是即时构建它,而无需生成数十个嵌套对象。
  • 那么你是说我试图通过单个 AR 查询而不是通过 Ruby 块循环多次迭代来获取关于该对象的尽可能多的信息不是一件好事吗?
  • 差不多。 AR 或任何 ORM 并不意味着是 100% 数据库抽象层。您的用例非常复杂,您必须与 AR 进行战斗才能在一个查询中完成。尝试在 Java Hibernate 中做同样的事情,你会遇到同样的问题。使用嵌套关系和范围来实现您想要的 (course.each { |course| course.students.not_started.each { |student| ...)。 Rails 还为您提供了足够的缓存机制(包括俄罗斯娃娃缓存),所以如果性能是一个问题,缓存结果,或者调用 ActiveRecord::Base.connection.execute ... 并使用普通 SQL。
【解决方案2】:

使用scope 来发挥你的优势:

class Course < ActiveRecord::Base
  # ...
  scope :not_started, -> { joins(:student_courses) \
                                      .where(student_courses: {started: false}) }

  scope :with_classmates, -> { includes(:students) } # use eager loading
end

然后调用:

@student.courses.not_started.with_classmates \
                                     .to_json(include: {students: {only: :name}})

输出:

[
    {
        "id": 1,
        "name": "english",
        "description": null,
        "students": [
            {"name": "Aiden"},
            {"name": "Alison"}]},
    {
        "id": 2,
        "name": "music",
        "description": null,
        "students": [
            {"name": "Aiden"},
            {"name": "Alison"}]},
    {
        "id": 3,
        "name": "dance",
        "description": null,
        "students": [
            {"name": "Aiden"}]}]

【讨论】:

  • 谢谢!你还给了我关于范围的信息,我非常感激!但是有些东西我不明白:为什么我需要调用 #includes(:students) 一次,然后我需要在 #to_json 方法上再次调用它?我注意到,如果我不在 AR 查询本身上调用它,但我在 to_json 调用中保留 :include => students,它仍然会包含学生。这是因为第一种方法是查询学生的数据库,然后这些数据可用于 to_json,而在第二种方法中 to_json 将为学生发出查询?这两种方式在查询和过滤学生方面有何不同?
  • to_json 访问each course 的数据库时,它避免了N+1 次查询。使用includes 将急切地加载所有关联的学生at onceto_json 将简单地从该结果集中提取。例如,从 20 门课程中收集学生现在需要 3 个查询而不是 21 个查询。
  • 我很抱歉不得不再次提问,但您能看一下问题更新吗?我终于明白了我最初想要做什么..谢谢..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-24
  • 1970-01-01
  • 1970-01-01
  • 2013-07-23
  • 2015-01-21
  • 2016-11-02
相关资源
最近更新 更多