Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firestore unique index or unique constraint?

Is it possible in Firestore to define an index with a unique constraint? If not, how is it possible to enforce uniqueness on a document field (without using document ID)?

like image 915
Greg Ennis Avatar asked Nov 29 '17 00:11

Greg Ennis


People also ask

How do you make a field unique in firestore?

The simple idea is, using a batched write, you write your document to your "data" collection and at the same write to a separate "index" collection where you index the value of the field that you want to be unique.

What types of indexes are automatically created in cloud firestore?

Automatic indexing For each map field, Cloud Firestore creates one collection-scope ascending index and one descending index for each non-array and non-map subfield in the map. For each array field in a document, Cloud Firestore creates and maintains a collection-scope array-contains index.

How do I prevent duplicates in firestore?

The best way to prevent duplicate nodes in firebase realtime database or duplicate documents in firebase firestore database is to keep a check in the app itself to verify that the record to be inserted doesn't already exist in the database by querying for data using key field in where clause.

What file should be used for firestore indexes?

From the CLI, edit your index configuration file, with default filename firestore. indexes. json , and deploy using the firebase deploy command.

How to enable uniqueness for a field value in FireStore?

Currently, there is no way for you to enable uniqueness in field values in the whole database. There is a quick workaround, though with queries that you can use. You want to enforce uniqueness for a field value in your Firestore database for your app. An app where users have their profiles and a unique username assigned to them.

What are unique constraints and unique indexes?

A unique constraint also guarantees that no duplicate values can be inserted into the column (s) on which the constraint is created. When a unique constraint is created a corresponding unique index is automatically created on the column (s). Let's investigate these concepts with an example. First, let's create a test environment:

What is a single field Index in FireStore?

A single-field index stores a sorted mapping of all the documents in a collection that contain a specific field. Each entry in a single-field index records a document's value for a specific field and the location of the document in the database. Cloud Firestore uses these indexes to perform many basic queries.

How does Cloud Firestore handle indexing in a map?

For each map field, Cloud Firestore creates one collection-scope ascending index and one descending index for each non-array and non-map subfield in the map. For each array field in a document, Cloud Firestore creates and maintains a collection-scope array-contains index.


1 Answers

Yes, this is possible using a combination of two collections, Firestore rules and batched writes.

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

The simple idea is, using a batched write, you write your document to your "data" collection and at the same write to a separate "index" collection where you index the value of the field that you want to be unique.

Using the Firestore rules, you can then ensure that the "data" collection can only have a document written to it if the document field's value also exists in the index collection and, vice versa, that the index collection can only be written to if value in the index matches what's in the data collection.

Example

Let's say that we have a User collection and we want to ensure that the username field is unique.

Our User collection will contain simply the username

/User/{id} {   username: String  } 

Our Index collection will contain the username in the path and a value property that contains the id of the User that is indexed.

/Index/User/username/{username} {   value: User.id } 

To create our User we use a batch write to create both the User document and the Index document at the same time.

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() } 

To update our User's username we use a batch write to update the User document, delete the previous Index document and create a new Index document all at the same time.

const firebaseApp = ...construct your firebase app  const updateUser = async (id, username) => {   const database = firebaseApp.firestore()   const batch = database.batch()    const Collection = database.collection('User')   const ref = Collection.doc(id)   const refDoc = await ref.get()   const prevData = refDoc.data()   batch.update(ref, {     username   })    const Index = database.collection('Index')   const prevIndexRef = Index.doc(`User/username/${prevData.username}`)   const indexRef = Index.doc(`User/username/${username}`)   batch.delete(prevIndexRef)   batch.set(indexRef, {     value: ref.id   })    await batch.commit() } 

To delete a User we use a batch write to delete both the User document and the Index document at the same time.

const firebaseApp = ...construct your firebase app  const deleteUser = async (id) => {   const database = firebaseApp.firestore()   const batch = database.batch()    const Collection = database.collection('User')   const ref = Collection.doc(id)   const refDoc = await ref.get()   const prevData = refDoc.data()   batch.delete(ref)     const Index = database.collection('Index')   const indexRef = Index.doc(`User/username/${prevData.username}`)   batch.delete(indexRef)    await batch.commit() } 

We then setup our Firestore rules so that they only allow a User to be created if the username is not already indexed for a different User. A User's username can only be updated if an Index does not already exist for the username and a User can only be deleted if the Index is deleted as well. Create and update will fail with a "Missing or insufficient permissions" error if a User with the same username already exists.

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;     }   } } 
like image 61
Brian Neisler Avatar answered Dec 03 '22 04:12

Brian Neisler