Рендеринг объекта JSON модели соединения и связанных с ней моделей

В приложении Rails ( 4.1.5 / ruby ​​2.0.0p481 / win64 ) у меня есть отношение «многие ко многим» между Студентом и Курсом и моделью соединения StudentCourse, который представляет ассоциацию и имеет дополнительный атрибут, называемый started (по умолчанию установлено значение "false").

Я также добавил индекс в таблицу соединений, состоящую из student_id и course_id, и установил для него уникальную проверку, например

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

Я хотел, чтобы это был составной первичный ключ, но поскольку в рельсах нет составных первичных ключей (без использования драгоценного камня), я также добавил первичный ключ с именем id:

t.column :id, :primary_key

Теперь я вижу, что ассоциации создаются либо путем выполнения:

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

or

Course.first.students << Student.first

Это нормально, и это ожидаемое поведение, я полагаю.


Тем не менее, я изо всех сил пытаюсь осмыслить разрешения ассоциаций в запросах ActiveRecord. Позвольте мне объяснить это лучше:

Для справки, Student.all, Course.all и StudentCourses.all будут возвращать такие таблицы:

Студент.все

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

Курс.все

+----+----------+------------------+
| 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 => {:students => {:only => :name}}) ?

Я думаю, что мне не хватает некоторых базовых знаний о некоторых важных ключевых понятиях, но на данный момент я даже не могу их идентифицировать и был бы очень признателен за помощь... спасибо.



Обновлять:

Я только что понял, что следующая часть — это то, что я изначально пытался сделать. Это противоположное. Среди всех этих подробностей я заблудился по пути. Я надеюсь, что это нормально, если я просто добавлю это здесь.

Итак, для данного студента (назовем его Эйден) мне нужно вернуть объект JSON, содержащий только те курсы, которые он посещает и которые начал, только когда в таких курсах есть другие студенты, которые не начинали их, и он также должен включать имена этих студентов для каждого курса.

So...

Теперь у меня есть:

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

который для студента принимает все курсы, которые имеют значение "true" в столбце "начало" таблицы соединения. (опять же, в таблице соединения каждая запись student-course является "составно" уникальной, поэтому может быть только одна уникальная запись для данных student_id и course_id).

С помощью следующего запроса для одного из "aiden_started_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, но как мне построить запрос, который возвращает эти данные для всех курсов? из начатых курсов Эйдена?

Можно ли это сделать в одну строку? Я знаю, что мог бы, вероятно, использовать циклы перечислителя 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 вручную? Но как я мог это сделать?


Спасибо в adv. и прошу прощения за многословие.. но, надеюсь, это поможет..


person jj_    schedule 07.11.2014    source источник
comment
добавить .to_json в конец: StudentCourse.all.where(student_id: @student.id, started: false).to_json.   -  person Aleksei Matiushkin    schedule 07.11.2014
comment
Это не отвечает на мой вопрос. Я уже добрался до этой (точной) точки. Я знаю, что добавление to_json рендерит JSON... Но я хочу рендерить не только это! Пожалуйста, прочитайте последнюю часть вопроса более внимательно.   -  person jj_    schedule 07.11.2014
comment
Да, извините. Чтобы студент/курсы заполнялись автоматически, вы должны указать атрибуты :belongs_to и :has_many для соответствующих ActiveRecord, как указано здесь: api.rubyonrails.org/classes/ActiveRecord/Associations/   -  person Aleksei Matiushkin    schedule 07.11.2014
comment
Это уже сделано! Я говорю это в первой строке! У меня есть отношение «многие ко многим» между студентом и курсом... что подразумевает has_many ... :through ...s и belongs_tos в моделях. Пожалуйста, не могли бы вы просто прочитать вопросы с большим вниманием, если вам действительно нужно это прокомментировать? Спасибо.   -  person jj_    schedule 07.11.2014


Ответы (2)


Используйте 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

Не много кода. Убрав комментарии и упростив некоторые функции, это будет выглядеть так:

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!

Ознакомьтесь с их руководством, очень здорово генерировать JSON в простом Ruby DSL. Так что продолжайте и используйте любой ruby-код, который вы хотите получить для студентов/курсов/студенческих курсов.

person Subhas    schedule 11.11.2014
comment
Спасибо, голосую. Я рассмотрю JBuilder, и я думаю, что это отличное предложение, но, поскольку я только учусь, я также хотел бы знать, как делать то, что я пытаюсь сделать в первую очередь, чтобы получить более глубокое понимание вовлеченного частей и связанных с ними шаблонов кодирования. - person jj_; 12.11.2014
comment
Хорошо спасибо. Если вы просто хотите вызвать .to_json и получить искомую структуру, это не оптимально, потому что вам сначала придется построить этот полный объект со всеми вложенными структурами. Рекомендуемый способ для сложного JSON — создавать его на лету, не создавая десятки вложенных объектов. - person Subhas; 12.11.2014
comment
То есть вы говорите, что моя попытка получить как можно больше информации об этом объекте с помощью одного запроса AR, а не многократных итераций через блочные циклы Ruby, не будет хорошей идеей? - person jj_; 13.11.2014
comment
Довольно много. AR или любой ORM в этом отношении не предназначены для 100% уровней абстракции БД. Ваш вариант использования достаточно сложен, поэтому вам придется бороться с AR, чтобы сделать это одним запросом. Попробуйте сделать то же самое в Java Hibernate, и у вас будет та же проблема. Используйте вложенные отношения и области для достижения желаемого (course.each { |course| course.students.not_started.each { |student| ...). Rails также предоставляет вам достаточно механизмов кэширования (включая кэширование матрешки), поэтому, если производительность является проблемой, кэшируйте результаты или вызовите ActiveRecord::Base.connection.execute ... и используйте простой SQL. - person Subhas; 15.11.2014

Используйте 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"}]}]
person Substantial    schedule 07.11.2014
comment
Спасибо! Вы также дали мне информацию о прицелах, которые я очень ценю! Но есть кое-что, чего я не понимаю: почему мне нужно вызвать #includes(:students) один раз, а затем снова вызвать метод #to_json? Я заметил, что если я не вызову его в самом запросе AR, но оставлю :include =› студентов в вызове to_json, он все равно будет включать студентов. Это потому, что при первом подходе БД запрашивается для студентов, а затем эти данные доступны для to_json, а во втором подходе to_json выдает запрос для студентов? Чем эти два способа отличаются при запросе и фильтрации учащихся? - person jj_; 08.11.2014
comment
Это позволяет избежать запросов N+1, когда to_json попадает в базу данных для каждого курса. Использование includes загрузит всех связанных студентов одновременно, а to_json просто выберет этот результат. задавать. Например, для сбора студентов с 20 курсов теперь потребуется 3 запроса вместо 21 запроса. - person Substantial; 08.11.2014
comment
Прошу прощения за то, что вынужден спросить еще раз, но не могли бы вы взглянуть на обновление вопроса? Наконец-то я понял, что я изначально пытался сделать.. спасибо.. - person jj_; 10.11.2014