Firebase Firestore的“引用”数据类型有什么用?

311

我正在探索新的Firebase Firestore,它包含一种名为reference的数据类型。目前还不清楚它的作用。

  • 它像外键吗?
  • 它能用于指向在其他位置的集合吗?
  • 如果reference是实际引用,我能用它进行查询吗?例如,我能够有一个直接指向用户的引用,而不是在文本字段中存储userId吗?并且我能使用这个用户引用进行查询吗?

32
我认为Firebase团队的这个视频会为您详细讲解:youtube.com/watch?v=Elg2zDVIcLo(从4:36开始观看)。 - Adarsh
23
好的,以下是需要翻译的内容:"Another way to think about this is that you can use a game engine to create an interactive experience, but you can also use it to create non-interactive experiences like film or animation. In fact, many animated movies these days are created using game engines because they allow for real-time rendering and interactivity during the creation process." - Trevor
1
我不喜欢在Firebase中嵌套集合,出于多种原因。如果由于某种原因您需要在另一个根级集合上钻取到兄弟根级集合,假设需要4个级别才能到达文档,使用引用并只是使用db.doc('some_saved_ref')会使这变得更加容易,而不是再次从其他根集合匹配所有ID。 - JustDave
9个回答

202

以下是我在Firestore中使用引用所使用的方法。

与其他答案所说一样,它就像一个外键。但是,reference属性不返回引用文档的数据。例如,我有一个产品列表,其中一个属性是userRef引用到该产品的用户。获取产品列表会给我返回创建该产品的用户的引用。但它不提供该引用中用户的详细信息。我曾经使用过其他后端服务,并且在其中使用了指针,那里有一个“populate: true”标志,它会返回用户详细信息而不仅仅是用户的引用ID,这对于此处来说是很好的改进(希望未来能实现)。

下面是我用来设置引用以及获取产品列表集合,然后根据给定的用户引用ID获取用户详细信息的示例代码。

在集合上设置引用:

let data = {
  name: 'productName',
  size: 'medium',
  userRef: db.doc('users/' + firebase.auth().currentUser.uid)
};
db.collection('products').add(data);

获取一个集合(产品)以及每个文档(用户详细信息)上的所有引用:

db.collection('products').get()
    .then(res => {
      vm.mainListItems = [];
      res.forEach(doc => {
        let newItem = doc.data();
        newItem.id = doc.id;
        if (newItem.userRef) {
          newItem.userRef.get()
          .then(res => { 
            newItem.userData = res.data() 
            vm.mainListItems.push(newItem);
          })
          .catch(err => console.error(err));
        } else {
          vm.mainListItems.push(newItem);  
        }
        
      });
    })
    .catch(err => { console.error(err) });

4
谢谢分享!我认为在 "Get" 部分的第一行有一个拼写错误,应该是 db.collection('products').get()。你尝试过直接获取用户吗?我猜想 newItem.userRef.get() 应该可以替代 db.collection("users").doc(newItem.userRef.id).get() - Sergey Nefedyev
84
首先感谢您提供的示例。我希望未来能够添加“populate: true”的功能。否则,保存引用有些没有意义。同样的效果可以通过仅保存uid并通过它引用来实现。 - Jürgen Brandstetter
7
谢谢提供示例!但是,如果在查询文档时没有“填充(populate)”选项,那么存储引用类型的目的是什么? 如果有任何人知道"populate"选项,请告诉我。 - Harshil Shah
30
实际上它并不像外键。对我来说,它基本上没有什么作用 - 如果我们无法像真正的外键那样使用reference,那么拥有它的意义是什么呢? - jean d'arme
28
引用相比于字符串唯一的优势是可以直接调用get()方法。目前还不是很有用。希望他们能添加一个选项,自动将引用与相应的对象填充! - morgler
显示剩余17条评论

138

引用与外键非常相似。

目前发布的SDK无法存储对其他项目的引用。在项目内,引用可以指向任何其他集合中的任何其他文档。

您可以像使用任何其他值一样在查询中使用引用:用于筛选、排序和分页(startAt/startAfter)。

与SQL数据库中的外键不同,引用不适用于在单个查询中执行联接。您可以将它们用于依赖查找(类似于联接),但请注意,每个跳转都会导致另一个到服务器的往返。


12
请问您能否分享可能的使用案例?是否可以在引用中查询字段?例如,我有一个“friends”集合,列出了所有我的朋友(“friends/myId”)。然后,在另一个文档(“group/groupId”)的“friends”字段中引用此文档。我想仅显示属于该组的好友,可以像这样执行:where('friends.myId', '==', true) - Will
143
顺便说一句,更新文档以包含添加引用类型的示例可能会很有用。 - Will
13
我找不到任何关于这个的信息?这将改变我的整个数据库结构,我需要了解更多... - Ruben
3
你有没有一个使用引用进行查询的示例(最好是使用Swift)?现在,我可以通过将原始uid作为字符串存储来实现查询,但这似乎不太合适。 - Mickey Cheong
7
我需要将所有的引用类型更改为字符串,因为使用引用类型查询时总是会失败。我找不到有关如何查询引用类型的任何信息:( 如果有人发现如何按引用类型查询,请告诉我... - Sam Trent
显示剩余7条评论

35

如果你正在寻找一种JavaScript解决方案来查询引用,那么你需要在查询语句中使用“文档引用”对象。

teamDbRef = db.collection('teams').doc('CnbasS9cZQ2SfvGY2r3b'); /* CnbasS9cZQ2SfvGY2r3b being the collection ID */
//
//
db.collection("squad").where('team', '==', teamDbRef).get().then((querySnapshot) => {
  //
}).catch(function(error) {
  //
});

(致敬这里的答案:https://dev59.com/HVQJ5IYBdhLWcg3wx40V#53141199)


11

18
仍然无效 - 至少我在界面中找不到它 :) - Boern
2
你为什么要构建一个数据类型,主要目的是在Firebase控制台UI中进行链接? - Kavin Mehta

11
很多回答提到它只是指向另一个文档的引用,但不会返回该引用的数据,但我们可以使用它单独获取数据。以下是如何在Firebase JavaScript SDK 9(模块化)版本中使用它的示例。假设您的firestore有一个名为“products”的集合,并且其中包含以下文档。
{
  name: 'productName',
  size: 'medium',
  userRef: 'user/dfjalskerijfs'
}

这里的用户有一个对users集合中文档的引用。我们可以使用以下代码段获取产品,然后从引用中检索用户。

import { collection, getDocs, getDoc, query, where } from "firebase/firestore";
import { db } from "./main"; // firestore db object

let productsWithUser = []
const querySnaphot = await getDocs(collection(db, 'products'));
querySnapshot.forEach(async (doc) => {
  let newItem = {id: doc.id, ...doc.data()};
  if(newItem.userRef) {
    let userData = await getDoc(newItem.userRef);
    if(userData.exists()) {
      newItem.userData = {userID: userData.id, ...userData.data()}
    }
    productwithUser.push(newItem);
  } else {
    productwithUser.push(newItem);
  }
});

这里的collection,getDocs,getDoc,query,where是与Firestore相关的模块,我们可以在必要时使用它们来获取数据。我们直接使用从products文档返回的用户引用来获取该引用对应的用户文档,使用以下代码:

let userData = await getDoc(newItem.userRef);

了解如何使用模块化版本的 SDK,请参阅官方文档以获取更多信息。


3
请注意,每次对引用的请求都会被计入读取次数。 - Kavin Mehta

5

如果您不使用引用数据类型,则需要更新每个文档

例如,您有两个集合"categories""products",并且将类别名称"水果"存储在"categories"中,然后将"苹果""柠檬"的每个文档存储在"products"中,如下所示。但是,如果您更新"categories"中的类别名称"水果",您还需要更新"products""苹果""柠檬"的每个文档中的类别名称"水果"

collection | document | field

categories > 67f60ad3 > name: "Fruits"

collection | document | field

  products > 32d410a7 > name: "Apple", category: "Fruits"
             58d16c57 > name: "Lemon", category: "Fruits"

但是,如果您在产品中的每个文档中将"水果"的引用存储在类别中,并将其与"苹果""柠檬"相关联,则在更新类别中的名称"水果"时,您不需要更新"苹果""柠檬"的每个文档:

collection | document | field

  products > 32d410a7 > name: "Apple", category: categories/67f60ad3
             58d16c57 > name: "Lemon", category: categories/67f60ad3

这是关于引用数据类型的优点。

9
讨论的重点并不是存储静态名称与“类似外键”的ID之间的区别,而是使用文档引用与仅使用文档ID作为字符串的好处。 - egalvan10

1

迟来的是,这篇博客有两个优点:

enter image description here

如果我希望按评分、发布日期或最多点赞数对餐厅评论进行排序,我可以在评论子集合中实现这一点,而无需创建复合索引。但是在更大的顶级集合中,我需要为每一个创建单独的复合索引,并且我还有200个复合索引的限制。
我不需要200个复合索引,但是有一些限制存在。
此外,从安全规则的角度来看,基于父级存在的某些数据来限制子文档的操作是相当常见的,当您在子集合中设置数据时,这样做会更容易。
其中一个例子是,如果用户没有父字段中的特权,则限制插入子集合的权限。

1

更新于12/18/22 - 我将其打包。

原博客文章

这个包使用RXJS循环遍历文档中的每个字段。如果该文档类型是引用类型,则获取该外部文档类型。集合版本获取集合中所有文档中每个引用字段的外键值。您还可以手动输入要解析以加快搜索速度的字段(请参阅我的帖子)。与使用Firebase函数手动聚合相比,这绝对不如效率高,因为您将为每个读取的文档付费大量读取费用,但对于想要在前端快速连接数据的人来说,这可能会很有用。

如果您缓存数据并且确实只需要执行一次此操作,则这也可能会很有用。

J

install

npm i j-firebase

导入

import { expandRef, expandRefs } from 'j-firebase';

https://github.com/jdgamble555/j-firebase


原始帖子


自动连接:

文档

expandRef<T>(obs: Observable<T>, fields: any[] = []): Observable<T> {
  return obs.pipe(
    switchMap((doc: any) => doc ? combineLatest(
      (fields.length === 0 ? Object.keys(doc).filter(
        (k: any) => {
          const p = doc[k] instanceof DocumentReference;
          if (p) fields.push(k);
          return p;
        }
      ) : fields).map((f: any) => docData<any>(doc[f]))
    ).pipe(
      map((r: any) => fields.reduce(
        (prev: any, curr: any) =>
          ({ ...prev, [curr]: r.shift() })
        , doc)
      )
    ) : of(doc))
  );
}

COLLECTION

expandRefs<T>(
  obs: Observable<T[]>,
  fields: any[] = []
): Observable<T[]> {
  return obs.pipe(
    switchMap((col: any[]) =>
      col.length !== 0 ? combineLatest(col.map((doc: any) =>
        (fields.length === 0 ? Object.keys(doc).filter(
          (k: any) => {
            const p = doc[k] instanceof DocumentReference;
            if (p) fields.push(k);
            return p;
          }
        ) : fields).map((f: any) => docData<any>(doc[f]))
      ).reduce((acc: any, val: any) => [].concat(acc, val)))
        .pipe(
          map((h: any) =>
            col.map((doc2: any) =>
              fields.reduce(
                (prev: any, curr: any) =>
                  ({ ...prev, [curr]: h.shift() })
                , doc2
              )
            )
          )
        ) : of(col)
    )
  );
}

将此函数应用于您的可观察对象,它将自动展开所有引用数据类型并提供自动连接。 用法
this.posts = expandRefs(
  collectionData(
    query(
      collection(this.afs, 'posts'),
      where('published', '==', true),
      orderBy(fieldSort)
    ), { idField: 'id' }
  )
);

注意: 您现在可以使用数组作为第二个参数来输入要扩展的字段。

['imageDoc', 'authorDoc']

这将提高速度!

在末尾添加.pipe(take(1)).toPromise();以获得Promise版本!

有关更多信息,请参见此处。 在Firebase 8或9中有效!

很简单!

J


2
请问您能否详细解释一下您的代码?我很难理解这些管道和映射-归约器在做什么。谢谢! - dylan-myers
我同意Dylan的观点。这段代码非常难以阅读,这使得很难从中获得任何好处。你能否尝试注释一下它的某些部分是在做什么?此外,它似乎嵌套了mapreduce,这将增加显著的运行时复杂度。 - Abir Taheer
这不是更新,而是有人试图在Firebase的错误基础上构建第三方库... - Rafael Lima
@AbirTaheer - 如果您不输入要扩展的特定字段,则它只会通过map中的每个字段。 @-RafaelLima - 同意这是Firestore的一个问题,但我只是想分享我编写并发现有用的代码。即使Firestore有连接,它仍然会向您收取阅读其他文档的费用。我可以对代码本身进行评论,但它确实只会遍历引用类型的每个字段并将其扩展,或者只有您输入的字段。 - Jonathan

1

2022更新

let coursesArray = [];
const coursesCollection = async () => {
    const queryCourse = query(
        collection(db, "course"),
        where("status", "==", "active")
    )
    onSnapshot(queryCourse, (querySnapshot) => {
        querySnapshot.forEach(async (courseDoc) => {

            if (courseDoc.data().userId) {
                const userRef = courseDoc.data().userId;
                getDoc(userRef)
                    .then((res) => {
                        console.log(res.data());
                    })
            }
            coursesArray.push(courseDoc.data());
        });
        setCourses(coursesArray);
    });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接