Google Firestore - как получить документ по нескольким идентификаторам за одну поездку туда и обратно?

105

Мне интересно, можно ли получить несколько документов по списку идентификаторов за одну поездку туда и обратно (сетевой вызов) в Firestore.

Джун
источник
4
Вы, кажется, предполагаете, что обратные обращения вызывают проблемы с производительностью вашего приложения. Я бы так не предполагал. Firebase отлично справлялась в таких случаях, поскольку она конвейерирует запросы . Хотя я не проверял, как Firestore ведет себя в этом сценарии, я хотел бы увидеть доказательство проблемы с производительностью, прежде чем предполагать, что она существует.
Франк ван Пуффелен,
1
Допустим, мне нужно документы a, b, cчтобы сделать что - то. Я запрашиваю все три параллельно в отдельных запросах. aзанимает 100 мс, bзанимает 150 мс и cзанимает 3000 мс. В результате мне нужно подождать 3000 мс, чтобы выполнить задачу. Это будет maxиз них. Это будет более рискованно, когда количество документов, которые нужно получить, велико. В зависимости от статуса сети, я думаю, это может стать проблемой.
Джун
1
Разве отправка их всех как одного не SELECT * FROM docs WHERE id IN (a,b,c)займет одинаковое количество времени? Я не вижу разницы, так как соединение устанавливается один раз, а остальное передается по конвейеру. Время (после первоначального установления соединения) - это время загрузки всех документов + 1 поездка туда и обратно, одинаковая для обоих подходов. Если он ведет себя по-другому, не могли бы вы поделиться образцом (как в моем связанном вопросе)?
Франк ван Пуффелен
Я думаю, что потерял тебя. Когда вы говорите, что он конвейерный, вы имеете в виду, что Firestore автоматически группирует и отправляет запросы на свой сервер за один проход к базе данных?
Джун
К вашему сведению, то, что я подразумеваю под циклом приема-передачи, - это один сетевой вызов базы данных от клиента. Я спрашиваю, группируются ли несколько запросов автоматически в Firestore как один цикл туда и обратно, или несколько запросов выполняются как несколько запросов в оба конца параллельно.
Джун

Ответы:

101

если вы находитесь в Node:

https://github.com/googleapis/nodejs-firestore/blob/master/dev/src/index.ts#L701

/**
* Retrieves multiple documents from Firestore.
*
* @param {...DocumentReference} documents - The document references
* to receive.
* @returns {Promise<Array.<DocumentSnapshot>>} A Promise that
* contains an array with the resulting document snapshots.
*
* @example
* let documentRef1 = firestore.doc('col/doc1');
* let documentRef2 = firestore.doc('col/doc2');
*
* firestore.getAll(documentRef1, documentRef2).then(docs => {
*   console.log(`First document: ${JSON.stringify(docs[0])}`);
*   console.log(`Second document: ${JSON.stringify(docs[1])}`);
* });
*/

Это специально для серверного SDK

ОБНОВЛЕНИЕ: «Cloud Firestore [клиентский sdk] теперь поддерживает IN запросы!»

https://firebase.googleblog.com/2019/11/cloud-firestore-now-supports-in-queries.html

myCollection.where(firestore.FieldPath.documentId(), 'in', ["123","456","789"])

Ник Франческина
источник
29
Для тех, кто хочет вызвать этот метод с динамически сгенерированным массивом ссылок на документы, вы можете сделать это следующим образом: firestore.getAll (... arrayOfReferences) .then ()
Horea
1
Мне очень жаль, @KamanaKisinga ... Я не делал никаких работ по firebase почти год и сейчас не могу реально помочь (послушайте, я действительно опубликовал этот ответ год назад сегодня!)
Ник Франческина,
2
SDK на стороне клиента теперь также предлагают эту функцию. см. ответ Джеодонары для примера: stackoverflow.com/a/58780369
Франк ван Пуффелен
6
предупреждение: входящий фильтр в настоящее время ограничен 10 элементами. Так что вы, вероятно, обнаружите, что это бесполезно, когда вы собираетесь начать производство.
Мартин Кремер,
9
на самом деле нужно использовать, firebase.firestore.FieldPath.documentId()а не'id'
Maddocks
23

Они только что объявили об этой функции, https://firebase.googleblog.com/2019/11/cloud-firestore-now-supports-in-queries.html .

Теперь вы можете использовать такие запросы, но помните, что размер ввода не может быть больше 10.

userCollection.where('uid', 'in', ["1231","222","2131"])

Jeadonara
источник
Это запрос whereIn, а не where. И я не знаю, как разработать запрос для нескольких документов из списка идентификаторов документов, принадлежащих определенной коллекции. Пожалуйста помоги.
Ошибка компиляции конец
17
@Compileerrorend не могли бы вы попробовать это? db.collection('users').where(firebase.firestore.FieldPath.documentId(), 'in',["123","345","111"]).get()
jeadonara 03
спасибо, особенно заfirebase.firestore.FieldPath.documentId()
Иван Черных
11

На практике вы бы использовали firestore.getAll как это

async getUsers({userIds}) {
    const refs = userIds.map(id => this.firestore.doc(`users/${id}`))
    const users = await this.firestore.getAll(...refs)
    console.log(users.map(doc => doc.data()))
}

или с синтаксисом обещания

getUsers({userIds}) {
    const refs = userIds.map(id => this.firestore.doc(`users/${id}`))
    this.firestore.getAll(...refs).then(users => console.log(users.map(doc => doc.data())))
}
Себастьян
источник
4
это действительно должен быть выбранный ответ, потому что он позволяет использовать более 10 идентификаторов
sshah98
10

Нет, прямо сейчас нет возможности пакетировать несколько запросов на чтение с помощью Cloud Firestore SDK и, следовательно, нет способа гарантировать, что вы можете прочитать все данные сразу.

Однако, как сказал Франк ван Пуффелен в комментариях выше, это не означает, что выборка трех документов будет в 3 раза медленнее, чем выборка одного документа. Прежде чем делать выводы, лучше провести собственные измерения.

Сэм Стерн
источник
1
Дело в том, что я хочу знать теоретические пределы производительности Firestore, прежде чем переходить на Firestore. Я не хочу мигрировать, а затем понимаю, что этого недостаточно для моего варианта использования.
Джун
2
Привет, здесь тоже есть соображения по поводу уюта. Допустим, я сохранил список всех идентификаторов моих друзей, и их число равно 500. Я могу получить список за 1 чтение, но для отображения их имени и photoURL мне потребуется 500 считываний.
Тапас Мукерджи
1
Если вы пытаетесь прочитать 500 документов, потребуется 500 прочтений. Если вы объедините необходимую информацию из всех 500 документов в один дополнительный документ, потребуется всего одно чтение. Это называется своего рода дублированием данных, что является нормальным явлением для большинства баз данных NoSQL, включая Cloud Firestore.
Франк ван Пуффелен
1
@FrankvanPuffelen Например, в mongoDb вы можете использовать ObjectId следующим образом: stackoverflow.com/a/32264630/648851 .
Ситян Лю
2
Как сказал @FrankvanPuffelen, дублирование данных довольно распространено в базе данных NoSQL. Здесь вы должны спросить себя, как часто эти данные необходимо читать и насколько они должны быть актуальными. Если вы храните информацию о 500 пользователях, скажем, их имя + фото + идентификатор, вы можете получить их за одно чтение. Но если они вам нужны в актуальном состоянии, вам, вероятно, придется использовать облачную функцию для обновления этих ссылок каждый раз, когда пользователь обновляет свое имя / фотографию, поэтому запускает облачную функцию + выполняет некоторые операции записи. Не существует "правильной" / "лучшей" реализации, это просто зависит от вашего варианта использования.
schankam
10

Вы можете использовать такую ​​функцию:

function getById (path, ids) {
  return firestore.getAll(
    [].concat(ids).map(id => firestore.doc(`${path}/${id}`))
  )
}

Его можно вызвать с одним идентификатором:

getById('collection', 'some_id')

или массив идентификаторов:

getById('collection', ['some_id', 'some_other_id'])
JP de la Torre
источник
5

Конечно, лучший способ сделать это - реализовать фактический запрос Firestore в облачной функции? Тогда от клиента к Firebase будет только один звонок туда и обратно, что, похоже, именно то, о чем вы просите.

В любом случае вы действительно хотите сохранить всю логику доступа к данным, как на этой стороне сервера.

Внутри, вероятно, будет такое же количество вызовов самой Firebase, но все они будут осуществляться через сверхбыстрые межсоединения Google, а не через внешнюю сеть, и в сочетании с конвейерной обработкой, которую объяснил Фрэнк ван Пуффелен, вы должны получить отличную производительность от этот подход.

Крис Уилсон
источник
3
Сохранение реализации в облачной функции - правильное решение в некоторых случаях, когда у вас сложная логика, но, вероятно, не в том случае, когда вы просто хотите объединить список с несколькими идентификаторами. Вы теряете кеширование на стороне клиента и стандартизованное форматирование возврата из обычных вызовов. Это вызывало больше проблем с производительностью, чем решало в некоторых случаях в моих приложениях, когда я использовал этот подход.
Иеремия
5

Если вы используете флаттер, вы можете сделать следующее:

Firestore.instance.collection('your collection name').where(FieldPath.documentId, whereIn:[list containing multiple document IDs]).getDocuments();

Это вернет Future, List<DocumentSnapshot>который вы можете повторять по своему усмотрению.

Монис
источник
2

Вот как вы могли бы сделать что-то подобное в Kotlin с Android SDK.
Может не обязательно быть в одном цикле, но он эффективно группирует результат и позволяет избежать множества вложенных обратных вызовов.

val userIds = listOf("123", "456")
val userTasks = userIds.map { firestore.document("users/${it!!}").get() }

Tasks.whenAllSuccess<DocumentSnapshot>(userTasks).addOnSuccessListener { documentList ->
    //Do what you need to with the document list
}

Обратите внимание, что выборка определенных документов намного лучше, чем выборка всех документов и фильтрация результата. Это потому, что Firestore взимает плату за набор результатов запроса.

Markymark
источник
1
Прекрасно работает, именно то, что я искал!
Георгий
1

Надеюсь, это вам поможет, у меня это работает.

getCartGoodsData(id) {

    const goodsIDs: string[] = [];

    return new Promise((resolve) => {
      this.fs.firestore.collection(`users/${id}/cart`).get()
        .then(querySnapshot => {
          querySnapshot.forEach(doc => {
            goodsIDs.push(doc.id);
          });

          const getDocs = goodsIDs.map((id: string) => {
            return this.fs.firestore.collection('goods').doc(id).get()
              .then((docData) => {
                return docData.data();
              });
          });

          Promise.all(getDocs).then((goods: Goods[]) => {
            resolve(goods);
          });
        });
    });
  }
Мухаммад Мабрук
источник
0

В настоящее время это невозможно в Firestore. Я не понимаю, почему ответ Александра принят, решение, которое он предлагает, просто возвращает все документы из коллекции «пользователей».

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

Хорея
источник
-1

Лучшее, что вы можете сделать, - это не использовать его в Promise.allкачестве клиента, а затем дождаться .allчтения, прежде чем продолжить.

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

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

Ронни Ройстон
источник
3
Он асинхронный, существует множество вариантов использования Promise.all... он не обязательно должен «замораживать» что-либо - вам может потребоваться дождаться всех данных, прежде чем вы сможете сделать что-то значимое
Райан Тейлор,
Есть несколько вариантов использования, когда вам действительно нужно загрузить все свои данные, поэтому ожидание (например, счетчик с соответствующим сообщением, нет необходимости «замораживать» какой-либо пользовательский интерфейс, как вы говорите) может быть полностью необходимо Promise.all .. Просто все зависит от того, какие продукты вы здесь создаете. Подобные комментарии, на мой взгляд, очень неуместны, и в них не должно быть «лучших» слов. Это действительно зависит от различных вариантов использования, с которыми можно столкнуться, и от того, что ваше приложение делает для пользователя.
schankam