При работе над проектами в Useful IO мы очень часто сталкивались с проблемой отсутствия какой-либо функциональности в Meteor. В последнее время мы столкнулись с проблемой сортировки и фильтрации документов в коллекции по полю, находящемуся в связанной коллекции. То, что может показаться тривиальным в реляционной базе данных, не так тривиально в MongoDB, как и в Meteor. Есть два рекомендуемых решения этой проблемы, но ни одно из них не является идеальным. Мы опишем оба. После этого мы представим наше собственное решение, которое представляет собой комбинацию обоих и пытается преодолеть трудности каждого из них.

Мы начнем с описания схемы примера, которая будет использоваться для лучшей иллюстрации решений. Мы будем использовать пакет Astronomy в качестве инструмента схемы, но вы можете использовать любой доступный пакет или просто не использовать его и работать с простыми объектами JavaScript, извлеченными из коллекции.

Как видите, у нас есть два связанных класса. У каждого сообщения (Сообщение) есть автор (Пользователь). Чтобы разрешить такую ​​связь, мы сохраняем ID автора в поле userId класса Posts.

Мы хотим отображать список всех сообщений и отсортируйте их по автору имени (поле имя класса Пользователь).

Решение №1 (Вложенные поля)

Наиболее распространенное (рекомендуемое?) Решение - сохранить связанный документ (Пользователь) в качестве вложенного документа в основном документе (Сообщение).

Конечно, мы не можем сделать это в Meteor с коллекцией Meteor.users. Однако для целей этого примера давайте просто предположим, что мы имеем дело не с коллекцией Meteor.users, и поэтому мы можем просто сохранить пользователей (User) в сообщении. (Опубликовать) документы. Определения наших классов нужно изменить на что-то вроде приведенного ниже листинга:

Как видите, у нас больше нет коллекции Пользователи. Вместо этого мы объявили вложенное свойство author, в котором может храниться экземпляр класса User. Предполагая, что у нас есть некоторые документы в коллекции Записи, давайте попробуем получить документы, отсортированные по полю author.name в возрастающем порядке.

Это было довольно просто. Это решение будет работать до тех пор, пока вам не придется хранить копии дополнительных документов (в нашем случае Пользователи) в отдельной коллекции.

Решение №2 (отдельные коллекции и ручная сортировка)

Второе решение требует большего количества шаблонов. Вам нужно получить документы из коллекции, которую вы хотите отсортировать (Сообщения), а затем для каждого из них найти соответствующий документ (Пользователь), получить его и закрепить. на основной документ. Имея массив объединенных документов, вы можете отсортировать его, как любой массив javascript. Вот одна реализация:

Давайте обсудим, что здесь происходит.

Сначала мы извлекаем все сообщения из коллекции и для каждого документа сообщения ищем связанный с ним пользовательский документ. Как упоминалось ранее, мы используем для этого поле postId.

Затем нам нужно выполнить сортировку вручную. Для этого мы используем метод sort () объекта Array, который принимает функцию компаратора в качестве аргумента. Из функции компаратора мы должны вернуть одно из трех значений: -1, 1 или 0, в зависимости от значений двух его аргументов. . Если первый документ (a) должен быть перед вторым (b), мы возвращаем -1. Если он должен быть после второго документа, мы возвращаем 1. Если они равны (равны с точки зрения сортировки), мы возвращаем 0. Благодаря Astronomy мы можем получить доступ к вложенному полю с помощью doc.get (‘author.name’), что немного упрощает наш код в отношении проверки на null здесь. Последнее, что мы делаем, это меняем массив сообщений в обратном порядке, если мы хотим, чтобы они располагались в порядке убывания.

Решение # 3 (Наше решение)

Теперь давайте посмотрим на наше решение. Это позволяет хранить документы в отдельных коллекциях, но сортировать их, как если бы они были в одной коллекции. Но как этого добиться? Чтобы ответить на этот вопрос, мы должны знать, как документы хранятся в коллекции (на клиенте). Каждая локальная коллекция имеет переменную _docs, которая представляет собой карту документов, где ключи являются идентификаторами документов. В нашем примере в коллекции Сообщения это будет Posts._collection._docs. При вызове метода find () Meteor берет документы с этой карты и применяет к каждому из них функцию преобразования (если она предусмотрена). Что, если бы мы могли изменить эти документы, добавляя к каждому связанный документ? Это предоставит нам те же возможности поиска, сортировки и фильтрации, что и при использовании только одной коллекции. Именно для этого и предназначено наше решение.

Мы создали функцию defineRelation, которая выполняет всю необходимую работу. Во-первых, давайте взглянем на код функции.

ПРИМЕЧАНИЕ. Если вы не хотите вдаваться в подробности реализации, вы можете просто перейти к следующему коду. пример использования. Необязательно понимать это, чтобы использовать его.

Как видите, для этого нужно четыре аргумента:

  • Коллекция - коллекция, в которой мы хотим хранить связанные документы (Записи).
  • RelatedCollection - связанная коллекция, из которой мы берем связанные документы (Пользователи).
  • RelationshipName - имя поля, в котором будут храниться связанные документы («автор»).
  • relatedFieldName - имя поля в основном документе, которое будет использоваться для извлечения документа из связанной коллекции (‘userId’).

Мы добавляем объект _computations в коллекцию, в которой будут храниться все реактивные вычисления для каждого документа в основной коллекции. Для каждого документа в основной коллекции мы прослушиваем три события: добавлено, изменено, удалено. Во-первых, мы должны убедиться, что все ранее созданные вычисления для данного документа остановлены. Нам необходимо реактивно обновлять связанный документ (Пользователь), находящийся в нашем основном документе (Сообщение), всякий раз, когда есть какие-либо изменения в связанном документе (Пользователь ). Вот почему мы регистрируем новое реактивное вычисление в хуках добавлено и изменено. Мы также гарантируем, что остановили вычисление для удаленных документов в обработчике remove.

Теперь давайте посмотрим, как будет выглядеть код, реализующий сортировку.

Вы, наверное, заметили, что это выглядит почти так же, как сортировка в первом решении. Единственное отличие состоит в том, что мы используем метод defineRelation () для реактивного обновления документов.

Преимущества

Благодаря нашему решению вы можете выполнять запросы на стороне клиента для нескольких связанных коллекций, как если бы они находились в одной коллекции. Вам не нужно выполнять сложные объединения и настраиваемую сортировку. В то же время вы можете хранить документы и изменять их отдельно.

Представление

А как насчет производительности? Мы всегда должны об этом заботиться. Мы будем использовать первое решение для справки.

Второй подход примерно в 13 раз медленнее первого. Это потому, что мы извлекаем документы перед их сортировкой, а это требует времени. Кроме того, он не использует преимущества мелкозернистой реактивности blaze и повторно отсортирует весь список, даже если был изменен только один элемент.

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

Ограничения

Наш подход можно использовать только на клиенте. Однако это не большая проблема, поскольку сортировка и фильтрация в основном выполняются на клиенте.

Демо

Вы можете протестировать каждое решение для сортировки, перейдя в пример приложения sorting-with-relations.meteor.com. Код приложения можно найти на GitHub https://github.com/usefulio/sorting-with-relations.

Астрономический пакет

Если вы используете Astronomy, вы можете просто использовать наш пакет, который сделает все за вас. Просто добавьте его в свой проект:

meteor add useful:astronomy-relations

Вы можете узнать больше о пакете на GitHub https://github.com/usefulio/meteor-astronomy-relations.