Firestore唯一索引或唯一约束?

Gre*_*nis 6 google-cloud-firestore

在Firestore中是否可以定义具有唯一约束的索引?如果没有,如何在文档字段上强制唯一性(不使用文档ID)?

Bri*_*ler 25

是的,这可以通过结合使用两个集合、Firestore 规则和批量写入来实现。

https://cloud.google.com/firestore/docs/manage-data/transactions#batched-writes

简单的想法是,使用批量写入,您将文档写入“数据”集合,同时写入单独的“索引”集合,在其中索引要唯一的字段的值。

使用 Firestore 规则,您可以确保“数据”集合只能在文档字段的值也存在于索引集合中的情况下才能写入文档,反之亦然,如果值,则只能写入索引集合索引中的内容与数据集合中的内容相匹配。

例子

假设我们有一个User集合并且我们希望确保该username字段是唯一的。

我们的User收藏将只包含username

/User/{id}
{
  username: String 
}
Run Code Online (Sandbox Code Playgroud)

我们的Index集合将包含路径中的用户名和一个value包含索引用户 ID 的属性。

/Index/User/username/{username}
{
  value: User.id
}
Run Code Online (Sandbox Code Playgroud)

为了创建我们的User我们使用批量写入同时创建User文档和Index文档。

/User/{id}
{
  username: String 
}
Run Code Online (Sandbox Code Playgroud)

为了更新我们User的用户名,我们使用批量写入来更新User文档,同时删除以前的Index文档并创建一个新Index文档。

/Index/User/username/{username}
{
  value: User.id
}
Run Code Online (Sandbox Code Playgroud)

要删除一个,User我们使用批量写入同时删除User文档和Index文档。

const firebaseApp = ...construct your firebase app

const createUser = async (username) => {
  const database = firebaseApp.firestore()
  const batch = database.batch()

  const Collection = database.collection('User')
  const ref = Collection.doc()
  batch.set(ref, {
    username
  })

  const Index = database.collection('Index')
  const indexRef = Index.doc(`User/username/${username}`)
  batch.set(indexRef, {
    value: ref.id
  })

  await batch.commit()
}
Run Code Online (Sandbox Code Playgroud)

然后我们设置我们的 Firestore 规则,以便它们只允许User在用户名尚未为不同的User. AUser的用户名只有在用户名Index不存在的情况下才能更新,并且User只有在删除了的情况下才能删除Index。如果已存在User相同的权限,则创建和更新将失败并显示“权限缺失或不足”错误username

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {


    // Index collection helper methods

    function getIndexAfter(path) {
      return getAfter(/databases/$(database)/documents/Index/$(path))
    }

    function getIndexBefore(path) {
      return get(/databases/$(database)/documents/Index/$(path))
    }

    function indexExistsAfter(path) {
      return existsAfter(/databases/$(database)/documents/Index/$(path))
    }

    function indexExistsBefore(path) {
      return exists(/databases/$(database)/documents/Index/$(path))
    }


    // User collection helper methods

    function getUserAfter(id) {
      return getAfter(/databases/$(database)/documents/User/$(id))
    }

    function getUserBefore(id) {
      return get(/databases/$(database)/documents/User/$(id))
    }

    function userExistsAfter(id) {
      return existsAfter(/databases/$(database)/documents/User/$(id))
    }


    match /User/{id} {
      allow read: true;

      allow create: if
        getIndexAfter(/User/username/$(getUserAfter(id).data.username)).data.value == id;

      allow update: if
        getIndexAfter(/User/username/$(getUserAfter(id).data.username)).data.value == id &&
        !indexExistsBefore(/User/username/$(getUserAfter(id).data.username));

      allow delete: if
        !indexExistsAfter(/User/username/$(getUserBefore(id).data.username));
    }

    match /Index/User/username/{username} {
      allow read: if true;

      allow create: if
        getUserAfter(getIndexAfter(/User/username/$(username)).data.value).data.username == username;

      allow delete: if 
        !userExistsAfter(getIndexBefore(/User/username/$(username)).data.value) || 
        getUserAfter(getIndexBefore(/User/username/$(username)).data.value).data.username != username;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一个非常聪明的解决方案,但是,处理一个非常基本的缺失功能肯定需要做很多工作。 (28认同)