firebase firestore 총정리본

https://firebase.google.com/docs/firestore/quickstart

Get started with Cloud Firestore

This quickstart shows you how to set up Cloud Firestore, add data, then view the data you just added in the Firebase console.

Create a Cloud Firestore project

image

When you create a Cloud Firestore project, it also enables the API in the Cloud API Manager.

Set up your development environment

Add the required dependencies and client libraries to your app.

  1. Follow the instructions to add Firebase to your iOS app.
  2. Add the Cloud Firestore pod to your Podfile
  3. Save the file and run pod install.
pod 'Firebase/Core'
pod 'Firebase/Firestore'

Initialize Cloud Firestore

Initialize an instance of Cloud Firestore:

import Firebase

FirebaseApp.configure()

let db = Firestore.firestore()

Add data

Cloud Firestore stores data in Documents, which are stored in Collections. Cloud Firestore creates collections and documents implicitly the first time you add data to the document. You do not need to explicitly create collections or documents.

Create a new collection and a document using the following example code.

// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("users").addDocument(data: [
   "first": "Ada",
   "last": "Lovelace",
   "born": 1815
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

Now add another document to the users collection. Notice that this document includes a key-value pair (middle name) that does not appear in the first document. Documents in a collection can contain different sets of information.

// Add a second document with a generated ID.
ref = db.collection("users").addDocument(data: [
   "first": "Alan",
   "middle": "Mathison",
   "last": "Turing",
   "born": 1912
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

Read data

To quickly verify that you’ve added data to Cloud Firestore, use the data viewer in the Firebase console.

You can also use the “get” method to retrieve the entire collection.

db.collection("users").getDocuments() { (querySnapshot, err) in
   if let err = err {
       print("Error getting documents: (err)")
   } else {
       for document in querySnapshot!.documents {
           print("(document.documentID) => (document.data())")
       }
   }
}


https://firebase.google.com/docs/firestore/data-model

References

Every document in Cloud Firestore is uniquely identified by its location within the database. The previous example showed a document alovelace within the collection users. To refer to this location in your code, you can create areference to it.

let alovelaceDocumentRef = db.collection("users").document("alovelace")

A reference is a lightweight object that just points to a location in your database. You can create a reference whether or not data exists there, and creating a reference does not perform any network operations.

You can also create references to collections:

let usersCollectionRef = db.collection("users")

For convenience, you can also create references by specifying the path to a document or collection as a string, with path components separated by a forward slash (/). For example, to create a reference to the alovelace document:

let aLovelaceDocumentReference = db.document("users/alovelace")

https://firebase.google.com/docs/firestore/manage-data/data-types

This page describes the data types that Cloud Firestore supports.

image

image

Value type ordering

When a query involves a field with values of mixed types, Cloud Firestore uses a deterministic ordering based on the internal representations. The following list shows the order:

  1. Null values
  2. Boolean values
  3. Integer and floating-point values, sorted in numerical order
  4. Date values
  5. Text string values
  6. Byte values
  7. Cloud Firestore references
  8. Geographical point values
  9. Array values
  10. Map values

https://firebase.google.com/docs/firestore/manage-data/add-data

Add data to Cloud Firestore

Set a document

To create or overwrite a single document, use the set() method:

// Add a new document in collection "cities"
db.collection("cities").document("LA").setData([
   "name": "Los Angeles",
   "state": "CA",
   "country": "USA"
]) { err in
   if let err = err {
       print("Error writing document: (err)")
   } else {
       print("Document successfully written!")
   }
}

If the document does not exist, it will be created. If the document does exist, its contents will be overwritten with the newly provided data, unless you specify that the data should be merged into the existing document, as follows:

// Update one field, creating the document if it does not exist.
db.collection("cities").document("BJ").setData([ "capital": true ], merge: true)

If you’re not sure whether the document exists, pass the option to merge the new data with any existing document to avoid overwriting entire documents.

Data types

Cloud Firestore lets you write a variety of data types inside a document, including strings, booleans, numbers, dates, null, and nested arrays and objects. Cloud Firestore always stores numbers as doubles, regardless of what type of number you use in your code.

Custom objects

java로 만 가능하다.즉 android에서만 적용 가능

Add a document

When you use set() to create a document, you must specify an ID for the document to create. For example:

db.collection("cities").document("new-city-id").setData(data)

But sometimes there isn’t a meaningful ID for the document, and it’s more convenient to let Cloud Firestore auto-generate an ID for you. You can do this by calling add():

// Add a new document with a generated id.
var ref: DocumentReference? = nil
ref = db.collection("cities").addDocument(data: [
   "name": "Tokyo",
   "country": "Japan"
]) { err in
   if let err = err {
       print("Error adding document: (err)")
   } else {
       print("Document added with ID: (ref!.documentID)")
   }
}

In some cases, it can be useful to create a document reference with an auto-generated ID, then use the reference later. For this use case, you can call doc():

let newCityRef = db.collection("cities").document()

// later...
newCityRef.setData([
   // ...
])

Behind the scenes, .add(...) and .doc().set(...) are completely equivalent, so you can use whichever is more convenient.

Update a document

To update some fields of a document without overwriting the entire document, use the update() method:

let washingtonRef = db.collection("cities").document("DC")

// Set the "capital" field of the city 'DC'
washingtonRef.updateData([
   "capital": true
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Update fields in nested objects

If your document contains nested objects, you can use “dot notation” to reference nested fields within the document when you call update():

// Create an initial document to update.
let frankDocRef = db.collection("users").document("frank")
frankDocRef.setData([
   "name": "Frank",
   "favorites": [ "food": "Pizza", "color": "Blue", "subject": "recess" ],
   "age": 12
   ])

// To update age and favorite color:
db.collection("users").document("frank").updateData([
   "age": 13,
   "favorites.color": "Red"
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

You can also add server timestamps to specific fields in your documents, to track when an update was received by the server:

db.collection("objects").document("some-id").updateData([
   "lastUpdated": FieldValue.serverTimestamp(),
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Update elements in an array

If your document contains an array field, you can use arrayUnion() and arrayRemove() to add and remove elements. arrayUnion() adds elements to an array but only elements not already present. arrayRemove() removes all instances of each given element.

let washingtonRef = db.collection("cities").document("DC")

// Atomically add a new region to the "regions" array field.
washingtonRef.updateData([
   "regions": FieldValue.arrayUnion(["greater_virginia"])
])

// Atomically remove a region from the "regions" array field.
washingtonRef.updateData([
   "regions": FieldValue.arrayRemove(["east_coast"])
])

Transactions & Batched Writes

https://firebase.google.com/docs/firestore/manage-data/transactions

Transactions and batched writes

  • Transactions: a transaction is a set of read and write operations on one or more documents.
  • Batched Writes: a batched write is a set of write operations on one or more documents.

Updating data with transactions

Using the Cloud Firestore client libraries, you can group multiple operations into a single transaction. Transactions are useful when you want to update a field’s value based on its current value, or the value of some other field. You could increment a counter by creating a transaction that reads the current value of the counter, increments it, and writes the new value to Cloud Firestore.

A transaction consists of any number of get() operations followed by any number of write operations such as set(),update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.

Transactions never partially apply writes. All writes execute at the end of a successful transaction.

When using transactions, note that:

  • Read operations must come before write operations.
  • A function calling a transaction (transaction function) might run more than once if a concurrent edit affects a document that the transaction reads.
  • Transaction functions should not directly modify application state.
  • Transactions will fail when the client is offline.

The following example shows how to create and run a transaction:

let sfReference = db.collection("cities").document("SF")

db.runTransaction({ (transaction, errorPointer) -> Any? in
   let sfDocument: DocumentSnapshot
   do {
       try sfDocument = transaction.getDocument(sfReference)
   } catch let fetchError as NSError {
       errorPointer?.pointee = fetchError
       return nil
   }

   guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -1,
           userInfo: [
               NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot (sfDocument)"
           ]
       )
       errorPointer?.pointee = error
       return nil
   }

   transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
   return nil
}) { (object, error) in
   if let error = error {
       print("Transaction failed: (error)")
   } else {
       print("Transaction successfully committed!")
   }
}

Passing information out of transactions

Do not modify application state inside of your transaction functions. Doing so will introduce concurrency issues, because transaction functions can run multiple times and are not guaranteed to run on the UI thread. Instead, pass information you need out of your transaction functions. The following example builds on the previous example to show how to pass information out of a transaction:

let sfReference = db.collection("cities").document("SF")

db.runTransaction({ (transaction, errorPointer) -> Any? in
   let sfDocument: DocumentSnapshot
   do {
       try sfDocument = transaction.getDocument(sfReference)
   } catch let fetchError as NSError {
       errorPointer?.pointee = fetchError
       return nil
   }

   guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -1,
           userInfo: [
               NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot (sfDocument)"
           ]
       )
       errorPointer?.pointee = error
       return nil
   }

   let newPopulation = oldPopulation + 1
   guard newPopulation <= 1000000 else {
       let error = NSError(
           domain: "AppErrorDomain",
           code: -2,
           userInfo: [NSLocalizedDescriptionKey: "Population (newPopulation) too big"]
       )
       errorPointer?.pointee = error
       return nil
   }

   transaction.updateData(["population": newPopulation], forDocument: sfReference)
   return newPopulation
}) { (object, error) in
   if let error = error {
       print("Error updating population: (error)")
   } else {
       print("Population increased to (object!)")
   }
}

Transaction failure

A transaction can fail for the following reasons:

  • The transaction contains read operations after write operations. Read operations must always come before any write operations.
  • The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.

A failed transaction returns an error and does not write anything to the database. You do not need to roll back the transaction; Cloud Firestore does this automatically.

Batched writes

If you do not need to read any documents in your operation set, you can execute multiple write operations as a single batch that contains any combination of set(), update(), or delete() operations. A batch of writes completes atomically and can write to multiple documents.

Batched writes are also useful for migrating large data sets to Cloud Firestore. A batched write can contain up to 500 operations and batching operations together reduces connection overhead resulting in faster data migration.

Batched writes have fewer failure cases than transactions and use simpler code. They are not affected by contention issues, because they don’t depend on consistently reading any documents. Batched writes execute even when the user’s device is offline. The following example shows how to build and commit a batch of writes:

// Get new write batch
let batch = db.batch()

// Set the value of 'NYC'
let nycRef = db.collection("cities").document("NYC")
batch.setData([:], forDocument: nycRef)

// Update the population of 'SF'
let sfRef = db.collection("cities").document("SF")
batch.updateData(["population": 1000000 ], forDocument: sfRef)

// Delete the city 'LA'
let laRef = db.collection("cities").document("LA")
batch.deleteDocument(laRef)

// Commit the batch
batch.commit() { err in
   if let err = err {
       print("Error writing batch (err)")
   } else {
       print("Batch write succeeded.")
   }
}

Data validation for atomic operations

For mobile/web client libraries, you can validate data using Cloud Firestore Security Rules. You can ensure that related documents are always updated atomically and always as part of a transaction or batched write. Use the getAfter()security rule function to access and validate the state of a document after a set of operations completes but beforeCloud Firestore commits the operations.

For example, imagine that the database for the cities example also contains a countries collection. Each countrydocument uses a last_updated field to keep track of the last time any city related to that country was updated. The following security rules require that an update to a city document must also atomically update the related country’s last_updated field:

service cloud.firestore {
 match /databases/{database}/documents {
   // If you update a city doc, you must also
   // update the related country's last_updated field.
   match /cities/{city} {
     allow write: if request.auth.uid != null &&
       getAfter(
         /databases/$(database)/documents/countries/$(request.resource.data.country)
       ).data.last_updated == request.time;
   }

   match /countries/{country} {
     allow write: if request.auth.uid != null;
   }
 }
}

Security rules limits

In security rules for transactions or batched writes, there is a limit of 20 document access calls for the entire atomic operation in addition to the normal 10 call limit for each single document operation in the batch.

For example, consider the following rules for a chat application:

service cloud.firestore {
 match /databases/{db}/documents {
   function prefix() {
     return /databases/{db}/documents;
   }
   match /chatroom/{roomId} {
     allow read, write: if roomId in get(/$(prefix())/users/$(request.auth.uid)).data.chats
                           || exists(/$(prefix())/admins/$(request.auth.uid));
   }
   match /users/{userId} {
     allow read, write: if userId == request.auth.uid
                           || exists(/$(prefix())/admins/$(request.auth.uid));
   }
   match /admins/{userId} {
     allow read, write: if exists(/$(prefix())/admins/$(request.auth.uid));
   }
 }
}

The snippets below illustrate the number of document access calls used for a few data access patterns:

// 0 document access calls used, because the rules evaluation short-circuits
// before the exists() call is invoked.
db.collection('user').doc('myuid').get(...);

// 1 document access call used. The maximum total allowed for this call
// is 10, because it is a single document request.
db.collection('chatroom').doc('mygroup').get(...);

// Initializing a write batch...
var batch = db.batch();

// 2 document access calls used, 10 allowed.
var group1Ref = db.collection("chatroom").doc("group1");
batch.set(group1Ref, {msg: "Hello, from Admin!"});

// 1 document access call used, 10 allowed.
var newUserRef = db.collection("users").doc("newuser");
batch.update(newUserRef, {"lastSignedIn": new Date()});

// 1 document access call used, 10 allowed.
var removedAdminRef = db.collection("admin").doc("otheruser");
batch.delete(removedAdminRef);

// The batch used a total of 2 + 1 + 1 = 4 document access calls, out of a total
// 20 allowed.
batch.commit();

Delete data from Cloud Firestore

https://firebase.google.com/docs/firestore/manage-data/delete-data

delete() 를 이용한다.

db.collection("cities").document("DC").delete() { err in
   if let err = err {
       print("Error removing document: (err)")
   } else {
       print("Document successfully removed!")
   }
}

Warning: Deleting a document does not delete its subcollections!

각각 하부의 subcollection은 손수 삭제해야 한다.


Delete fields

To delete specific fields from a document, use the FieldValue.delete() method when you update a document:

db.collection("cities").document("BJ").updateData([
   "capital": FieldValue.delete(),
]) { err in
   if let err = err {
       print("Error updating document: (err)")
   } else {
       print("Document successfully updated")
   }
}

Delete collections

To delete an entire collection or subcollection in Cloud Firestore, retrieve all the documents within the collection or subcollection and delete them. If you have larger collections, you may want to delete the documents in smaller batches to avoid out-of-memory errors. Repeat the process until you’ve deleted the entire collection or subcollection.

Deleting a collection requires coordinating an unbounded number of individual delete requests. If you need to delete entire collections, do so only from a trusted server environment. While it is possible to delete a collection from a mobile/web client, doing so has negative security and performance implications.

The snippets below are somewhat simplified and do not deal with error handling, security, deleting subcollections, or maximizing performance. To learn more about one recommended approach to deleting collections in production, seeDeleting Collections and Subcollections.

Delete data with the Firebase CLI

You can also use the Firebase CLI to delete documents and collections. Use the following command to delete data:

firebase firestore:delete [options] <<path>>

Get data

https://firebase.google.com/docs/firestore/query-data/get-data

There are two ways to retrieve data stored in Cloud Firestore.

  • Call a method to get the data.
  • Set a listener to receive data-change events.

Get a document

The following example shows how to retrieve the contents of a single document using get():


let docRef = db.collection("cities").document("SF")

docRef.getDocument { (document, error) in
   if let document = document, document.exists {
       let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
       print("Document data: (dataDescription)")
   } else {
       print("Document does not exist")
   }
}

Note: If there is no document at the location referenced by docRef, the resulting document will be empty and calling existson it will return false.

Source Options

For platforms with offline support, you can set the source option to control how a get call uses the offline cache.

By default, a get call will attempt to fetch the latest document snapshot from your database. On platforms with offline support, the client library will use the offline cache if the network is unavailable or if the request times out.

You can specify the source option in a get() call to change the default behavior. You can fetch from only the database and ignore the offline cache, or you can fetch from only the offline cache. For example:

let docRef = db.collection("cities").document("SF")

// Force the SDK to fetch the document from the cache. Could also specify
// FirestoreSource.server or FirestoreSource.default.
docRef.getDocument(source: .cache) { (document, error) in
 if let document = document {
   let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
   print("Cached document data: (dataDescription)")
 } else {
   print("Document does not exist in cache")
 }
}

Custom objects

The previous example retrieved the contents of the document as a map, but in some languages it’s often more convenient to use a custom object type. In Add Data, you defined a City class that you used to define each city. You can turn your document back into a City object:

let docRef = db.collection("cities").document("BJ")

docRef.getDocument { (document, error) in
   if let city = document.flatMap({
     $0.data().flatMap({ (data) in
       return City(dictionary: data)
     })
   }) {
       print("City: (city)")
   } else {
       print("Document does not exist")
   }
}

Important: Each custom class must have a public constructor that takes no arguments. In addition, the class must include a public getter for each property.

Get multiple documents from a collection

You can also retrieve multiple documents with one request by querying documents in a collection. For example, you can use where() to query for all of the documents that meet a certain condition, then use get() to retrieve the results:

db.collection("cities").whereField("capital", isEqualTo: true)
   .getDocuments() { (querySnapshot, err) in
       if let err = err {
           print("Error getting documents: (err)")
       } else {
           for document in querySnapshot!.documents {
               print("(document.documentID) => (document.data())")
           }
       }
}

By default, Cloud Firestore retrieves all documents that satisfy the query in ascending order by document ID, but you can order and limit the data returned.

Get all documents in a collection

In addition, you can retrieve all documents in a collection by omitting the where() filter entirely:

db.collection("cities").getDocuments() { (querySnapshot, err) in
   if let err = err {
       print("Error getting documents: (err)")
   } else {
       for document in querySnapshot!.documents {
           print("(document.documentID) => (document.data())")
       }
   }
}

List subcollections of a document

The getCollections() method of the Cloud Firestore server client libraries lists all subcollections of a document reference.

Retrieving a list of collections is not possible with the mobile/web client libraries. You should only look up collection names as part of administrative tasks in trusted server environments. If you find that you need this capability in the mobile/web client libraries, consider restructuring your data so that subcollection names are predictable.

Get real time updates

https://firebase.google.com/docs/firestore/query-data/listen

db.collection("cities").document("SF")
   .addSnapshotListener { documentSnapshot, error in
     guard let document = documentSnapshot else {
       print("Error fetching document: (error!)")
       return
     }
     guard let data = document.data() else {
       print("Document data was empty.")
       return
     }
     print("Current data: (data)")
   }

Events for local changes

Local writes in your app will invoke snapshot listeners immediately. This is because of an important feature called “latency compensation.” When you perform a write, your listeners will be notified with the new data before the data is sent to the backend.

Retrieved documents have a metadata.hasPendingWrites property that indicates whether the document has local changes that haven’t been written to the backend yet. You can use this property to determine the source of events received by your snapshot listener:

db.collection("cities").document("SF")
   .addSnapshotListener { documentSnapshot, error in
       guard let document = documentSnapshot else {
           print("Error fetching document: (error!)")
           return
       }
       let source = document.metadata.hasPendingWrites ? "Local" : "Server"
       print("(source) data: (document.data() ?? [:])")
   }


Events for metadata changes

// Listen to document metadata.
db.collection("cities").document("SF")
   .addSnapshotListener(includeMetadataChanges: true) { documentSnapshot, error in
       // ...
   }

ref) https://stackoverflow.com/a/50186785

Listen to multiple documents in a collection

db.collection("cities").whereField("state", isEqualTo: "CA")
   .addSnapshotListener { querySnapshot, error in
       guard let documents = querySnapshot?.documents else {
           print("Error fetching documents: (error!)")
           return
       }
       let cities = documents.map { $0["name"]! }
       print("Current cities in CA: (cities)")
   }

The snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified).

Important: As explained above under

Events for local changes

, you will receive events immediately for your local writes. Your listener can use the

metadata.hasPendingWrites

field on each document to determine whether the document has local changes that have not yet been written to the backend.

View changes between snapshots

document에 어떤 변화( add, modify,remove )가 있었는지를 파악하는 방법

db.collection("cities").whereField("state", isEqualTo: "CA")
    .addSnapshotListener { querySnapshot, error in
        guard let snapshot = querySnapshot else {
            print("Error fetching snapshots: (error!)")
            return
        }
        snapshot.documentChanges.forEach { diff in
            if (diff.type == .added) {
                print("New city: (diff.document.data())")
            }
            if (diff.type == .modified) {
                print("Modified city: (diff.document.data())")
            }
            if (diff.type == .removed) {
                print("Removed city: (diff.document.data())")
            }
        }
    }

Detach a listener

listener를 사용후 필요하지 않는 경우 제거해주어야 한다.

let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
   // ...
}

// ...

// Stop listening to changes
listener.remove()

Perform simple and compound queries

https://firebase.google.com/docs/firestore/query-data/queries

collection으로 부터 특정 조건이 맞는 document를 찾아내거나 추리거나 하는 작업을 query라고 한다. 

Simple queries

// Create a reference to the cities collection
let citiesRef = db.collection("cities")

// Create a query against the collection.
let query = citiesRef.whereField("state", isEqualTo: "CA")
let capitalCities = db.collection("cities").whereField("capital", isEqualTo: true)
citiesRef.whereField("state", isEqualTo: "CA")
citiesRef.whereField("population", isLessThan: 100000)
citiesRef.whereField("name", isGreaterThanOrEqualTo: "San Francisco")

Array membership

citiesRef
 .whereField("regions", arrayContains: "west_coast")

Compound queries

You can also chain multiple where() methods to create more specific queries (logical AND). However, to combine the equality operator (==) with a range or array-contains clause (<, <=, >, >=, or array_contains), make sure to create a composite index.

citiesRef
   .whereField("state", isEqualTo: "CO")
   .whereField("name", isEqualTo: "Denver")
citiesRef
   .whereField("state", isEqualTo: "CA")
   .whereField("population", isLessThan: 1000000)

You can only perform range comparisons (<, <=, >, >=) on a single field, and you can include at most one array_contains clause in a compound query:

citiesRef
   .whereField("state", isGreaterThanOrEqualTo: "CA")
   .whereField("state", isLessThanOrEqualTo: "IN")
citiesRef
   .whereField("state", isEqualTo: "CA")
   .whereField("population", isGreaterThan: 1000000)


틀린예)

citiesRef
   .whereField("state", isGreaterThanOrEqualTo: "CA")
   .whereField("population", isGreaterThan: 1000000)

Query limitations

Cloud Firestore does not support the following types of queries:

  • Queries with range filters on different fields, as described in the previous section.
  • Single queries across multiple collections or subcollections. Each query runs against a single collection of documents. For more information about how your data structure affects your queries, see Choose a Data Structure.
  • Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app.
  • Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although the query clause where("age", "!=", "30") is not supported, you can get the same result set by combining two queries, one with the clause where("age", "<", "30") and one with the clause where("age", ">", 30).

Order and limit data

https://firebase.google.com/docs/firestore/query-data/order-limit-data

Order and limit data

citiesRef.order(by: "name").limit(to: 3)
citiesRef.order(by: "name", descending: true).limit(to: 3)
citiesRef
   .order(by: "state")
   .order(by: "population", descending: true)

You can combine where() filters with orderBy() and limit(). In the following example, the queries define a population threshold, sort by population in ascending order, and return only the first few results that exceed the threshold:

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "population")
   .limit(to: 2)

However, if you have a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field:

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "population")

틀린예)

citiesRef
   .whereField("population", isGreaterThan: 100000)
   .order(by: "country")

Pagination

https://firebase.google.com/docs/firestore/query-data/query-cursors

Add a simple cursor to a query

// Get all cities with population over one million, ordered by population.
db.collection("cities")
   .order(by: "population")
   .start(at: [1000000])
// Get all cities with population less than one million, ordered by population.
db.collection("cities")
   .order(by: "population")
   .end(at: [1000000])

Use a document snapshot to define the query cursor

document를 cursor로 직접 이용하는 경우 ( start()에 직접 document obj를 pass 한다 )

db.collection("cities")
   .document("SF")
   .addSnapshotListener { (document, error) in
       guard let document = document else {
           print("Error retreving cities: (error.debugDescription)")
           return
       }

       // Get all cities with a population greater than or equal to San Francisco.
       let sfSizeOrBigger = db.collection("cities")
           .order(by: "population")
           .start(atDocument: document)
}

Paginate a query

// Construct query for first 25 cities, ordered by population
let first = db.collection("cities")
   .order(by: "population")
   .limit(to: 25)

first.addSnapshotListener { (snapshot, error) in
   guard let snapshot = snapshot else {
       print("Error retreving cities: (error.debugDescription)")
       return
   }

   guard let lastSnapshot = snapshot.documents.last else {
       // The collection is empty.
       return
   }

   // Construct a new query starting after this document,
   // retrieving the next 25 cities.
   let next = db.collection("cities")
       .order(by: "population")
       .start(afterDocument: lastSnapshot)

   // Use the query for pagination.
   // ...
}

Set multiple cursor conditions

조건에 해당하는 document가 여러개 인경우 조건을 좀더 명확하게 지정할수 있다.

// Will return all Springfields
db.collection("cities")
   .order(by: "name")
   .order(by: "state")
   .start(at: ["Springfield"])

// Will return "Springfield, Missouri" and "Springfield, Wisconsin"
db.collection("cities")
   .order(by: "name")
   .order(by: "state")
   .start(at: ["Springfield", "Missouri"])

index

https://firebase.google.com/docs/firestore/query-data/index-overview

Index types

Cloud Firestore uses two types of indexes, single-field and composite. Both types of indexes are uniquely defined by the fields they index and the index mode on each field.

Automatic indexing

By default, Cloud Firestore automatically maintains an index for each field in a document and each subfield in a map. Cloud Firestore uses the following settings for automatically created single-field indexes:

  • For each non-array and non-map field, Cloud Firestore defines two single-field indexes, one in ascending mode and one in descending mode.
  • For each map field, Cloud Firestore creates one 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 an array-contains index.

If you need to run a compound query that uses a range comparison (<, <=, >, or >=) or if you need to sort by a different field, you must create a composite index for that query.


Composite indexes

A composite index stores a sorted mapping of all the documents in a collection that contain multiple specific fields instead of just one. A composite index also defines an index mode for each of its fields (ascending, descending, or array-contains), and the index is sorted based on the index modes.

Note: You can have at most one array field per composite index.



Security Rules

https://firebase.google.com/docs/firestore/security/get-started

Note: The server client libraries bypass all Cloud Firestore Security Rules and instead authenticate through Google Application Default Credentials. If you are using the server client libraries or the REST or RPC APIs, make sure to set up Cloud Identity and Access Management for Cloud Firestore.

server에서 접근하는 연결의 security rule은 IAM으로 한다.

예)

// Allow read/write access on all documents to any user signed in to the application
service cloud.firestore {
 match /databases/{database}/documents {
   match /{document=**} {
     allow read, write: if request.auth.uid != null;
   }
 }
}

CLI로 rule을 수정하는 경우 주의사항

Note: When you deploy security rules using the Firebase CLI, the rules defined in your project directory overwrite any existing rules in the Firebase console. So, if you choose to define or edit your security rules using the Firebase console, make sure that you also update the rules defined in your project directory.

Structuring Cloud Firestore Security Rules

https://firebase.google.com/docs/firestore/security/rules-structure

Service and database declaration

Cloud Firestore Security Rules always begin with the following declaration:

service cloud.firestore {
 match /databases/{database}/documents {
   // ...
 }
}

Basic read/write rules

service cloud.firestore {
 match /databases/{database}/documents {

   // Match any document in the 'cities' collection
   match /cities/{city} {
     allow read: if <condition>;
     allow write: if <condition>;
   }
 }
}

In the example above, the match statement uses the {city} wildcard syntax. This means the rule applies to any document in the cities collection, such as /cities/SF or /cities/NYC. When the allow expressions in the match statement are evaluated, the city variable will resolve to the city document name, such as SF or NYC.

Granular operations

service cloud.firestore {
 match /databases/{database}/documents {
   // A read rule can be divided into get and list rules
   match /cities/{city} {
     // Applies to single document read requests
     allow get: if <condition>;

     // Applies to queries and collection read requests
     allow list: if <condition>;
   }

   // A write rule can be divided into create, update, and delete rules
   match /cities/{city} {
     // Applies to writes to nonexistent documents
     allow create: if <condition>;

     // Applies to writes to existing documents
     allow update: if <condition>;

     // Applies to delete operations
     allow delete: if <condition>;
   }
 }
}

Hierarchical data

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     allow read, write: if <condition>;

       // Explicitly define rules for the 'landmarks' subcollection
       match /landmarks/{landmark} {
         allow read, write: if <condition>;
       }
   }
 }
}

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     match /landmarks/{landmark} {
       allow read, write: if <condition>;
     }
   }
 }
}

위와 아래는 같은 내용

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city}/landmarks/{landmark} {
     allow read, write: if <condition>;
   }
 }
}

If you want rules to apply to an arbitrarily deep hierarchy, use the recursive wildcard syntax, {name=**}:

service cloud.firestore {
 match /databases/{database}/documents {
   // Matches any document in the cities collection as well as any document
   // in a subcollection.
   match /cities/{document=**} {
     allow read, write: if <condition>;
   }
 }
}

When using the recursive wildcard syntax, the wildcard variable will contain the entire matching path segment, even if the document is located in a deeply nested subcollection. For example, the rules listed above would match a document located at /cities/SF/landmarks/coit_tower, and the value of the document variable would be SF/landmarks/coit_tower.

Recursive wildcards cannot match an empty path, so match /cities/{city}/{document=**} will match documents in subcollections but not in the cities collection, whereas match /cities/{document=**} will match both documents in the cities collection and subcollections.

It’s possible for a document to match more than one match statement. In the case where multiple allow expressions match a request, the access is allowed if any of the conditions is true:

service cloud.firestore {
 match /databases/{database}/documents {
   // Matches any document in the 'cities' collection.
   match /cities/{city} {
     allow read, write: if false;
   }

   // Matches any document in the 'cities' collection or subcollections.
   match /cities/{document=**} {
     allow read, write: if true;
   }
 }
}

In the example above, all reads and writes to the cities collection will be allowed because the second rule is always true, even though the first rule is always false.

firestore에서 사용되는 class reference doc

ref) https://firebase.google.com/docs/reference/security/database/

firestore security rules설정 예시

https://www.youtube.com/watch?v=b7PUm7LmAOw

Writing conditions for Cloud Firestore Security Rules

https://firebase.google.com/docs/firestore/security/rules-conditions

Authentication

One of the most common security rule patterns is controlling access based on the user’s authentication state. For example, your app may want to allow only signed-in users to write data:

service cloud.firestore {
 match /databases/{database}/documents {
   // Allow the user to access documents in the "cities" collection
   // only if they are authenticated.
   match /cities/{city} {
     allow read, write: if request.auth.uid != null;
   }
 }
}

자세한 Request class 내용

https://firebase.google.com/docs/reference/rules/rules.firestore.Request

If your app uses Firebase Authentication, the request.auth variable contains the authentication information for the client requesting data. For more information about request.auth, see the reference documentation.

service cloud.firestore {
 match /databases/{database}/documents {
   // Make sure the uid of the requesting user matches name of the user
   // document. The wildcard expression {userId} makes the userId variable
   // available in rules.
   match /users/{userId} {
     allow read, update, delete: if request.auth.uid == userId;
     allow create: if request.auth.uid != null;
   }
 }
}

Data validation

Many apps store access control information as fields on documents in the database. Cloud Firestore Security Rules can dynamically allow or deny access based on document data:

service cloud.firestore {
 match /databases/{database}/documents {
   // Allow the user to read data if the document has the 'visibility'
   // field set to 'public'
   match /cities/{city} {
     allow read: if resource.data.visibility == 'public';
   }
 }
}

The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document. For more information on the resource variable, see the reference documentation.

When writing data, you may want to compare incoming data to existing data. In this case, if your ruleset allows the pending write, the request.resource variable contains the future state of the document. For update operations that only modify a subset of the document fields, the request.resource variable will contain the pending document state after the operation. You can check the field values in request.resource to prevent unwanted or inconsistent data updates:

service cloud.firestore {
 match /databases/{database}/documents {
   // Make sure all cities have a positive population and
   // the name is not changed
   match /cities/{city} {
     allow update: if request.resource.data.population > 0
                   && request.resource.data.name == resource.data.name;
   }
 }
}

Access other documents

Using the get() and exists() functions, your security rules can evaluate incoming requests against other documents in the database. The get() and exists() functions both expect fully specified document paths. When using variables to construct paths for get() and exists(), you need to explicitly escape variables using the $(variable) syntax.

In the example below, the database variable is captured by the match statement match /databases/{database}/documents and used to form the path:

service cloud.firestore {
 match /databases/{database}/documents {
   match /cities/{city} {
     // Make sure a 'users' document exists for the requesting user before
     // allowing any writes to the 'cities' collection
     allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid))

     // Allow the user to delete cities if their user document has the
     // 'admin' field set to 'true'
     allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
   }
 }
}

For writes, you can use the getAfter() function to access the state of a document after a transaction or batch of writes completes but before the transaction or batch commits. Like get(), the getAfter() function takes a fully specified document path. You can use getAfter() to define sets of writes that must take place together as a transaction or batch.

Custom functions

  • Functions can contain only a single return statement. They cannot contain any additional logic. For example, they cannot create intermediate variables, execute loops, or call external services.
  • Functions can automatically access functions and variables from the scope in which they are defined. For example, a function defined within the service cloud.firestore scope has access to the resource variable and built-in functions such as get() and exists().
  • Functions may call other functions but may not recurse. The total call stack depth is limited to 10.

A function is defined with the function keyword and takes zero or more arguments. For example, you may want to combine the two types of conditions used in the examples above into a single function:

service cloud.firestore {
 match /databases/{database}/documents {
   // True if the user is signed in or the requested data is 'public'
   function signedInOrPublic() {
     return request.auth.uid != null || resource.data.visibility == 'public';
   }

   match /cities/{city} {
     allow read, write: if signedInOrPublic();
   }

   match /users/{user} {
     allow read, write: if signedInOrPublic();
   }
 }
}

Securely query data

https://firebase.google.com/docs/firestore/security/rules-query

Queries and security rules

When writing queries to retrieve documents, keep in mind that security rules are not filters—queries are all or nothing. To save you time and resources, Cloud Firestore evaluates a query against its potential result set instead of the actual field values for all of your documents. If a query could potentially return documents that the client does not have permission to read, the entire request fails.

Note: This behavior applies to queries that retrieve one or more documents from a collection and not to individual document retrievals. When you use a document ID to retrieve a single document, Cloud Firestore reads the document and evaluates the request using your security rules and the actual document properties.

As the examples below demonstrate, you must write your queries to fit the constraints of your security rules.


Secure and query documents based on

auth.uid

The following example demonstrates how to write a query to retrieve documents protected by a security rule. Consider a database that contains a collection of story documents:

/stories/{storyid}

{
 title: "A Great Story",
 content: "Once upon a time...",
 author: "some_auth_id",
 published: false
}

In addition to the title and content fields, each document stores the author and published fields to use for access control. These examples assume the app uses Firebase Authentication to set the author field to the UID of the user who created the document. Firebase Authentication also populates the request.auth variable in the security rules.

The following security rule uses the request.auth and resource.data variables to restrict read and write access for each story to its author:

service cloud.firestore {
 match /databases/{database}/documents {
   match /stories/{storyid} {
     // Only the authenticated user who authored the document can read or write
     allow read, write: if request.auth.uid == resource.data.author;
   }
 }
}

Suppose that your app includes a page that shows the user a list of story documents that they authored. You might expect that you could use the following query to populate this page. However, this query will fail, because it does not include the same constraints as your security rules:

Invalid: Query constraints do not match security rules constraints

// This query will fail
db.collection("stories").get()

The query fails even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database. If a query could potentially include documents that violate your security rules, the query will fail.

In contrast, the following query succeeds, because it includes the same constraint on the author field as the security rules:

Valid: Query constraints match security rules constraints

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Secure and query documents based on a field

To further demonstrate the interaction between queries and rules, the security rules below expand read access for the stories collection to allow any user to read story documents where the published field is set to true.

service cloud.firestore {
 match /databases/{database}/documents {
   match /stories/{storyid} {
     // Anyone can read a published story; only story authors can read unpublished stories
     allow read: if resource.data.published == true || request.auth.uid == resource.data.author;
     // Only story authors can write
     allow write: if request.auth.uid == resource.data.author;
   }
 }
}

The query for published pages must include the same constraints as the security rules:

db.collection("stories").where("published", "==", true).get()

The query constraint .where("published", "==", true) guarantees that resource.data.published is true for any result. Therefore, this query satisfies the security rules and is allowed to read data.

Evaluating constraints on queries

Your security rules can also accept or deny queries based on their constraints. The request.query variable contains the limit, offset, and orderBy properties of a query. For example, your security rules can deny any query that doesn’t limit the maximum number of documents retrieved to a certain range:

allow list: if request.query.limit <= 10;

Note:

You can break read rules into get and list rules

. Rules for

get

apply to requests for single documents, and rules for

list

apply to queries and requests for collections.

The following ruleset demonstrates how to write security rules that evaluate constraints placed on queries. This example expands the previous stories ruleset with the following changes:

  • The ruleset separates the read rule into rules for get and list.
  • The get rule restricts retrieval of single documents to public documents or documents the user authored.
  • The list rule applies the same restrictions as get but for queries. It also checks the query limit, then denies any query without a lmit or with a limit greater than 10.
  • The ruleset defines an authorOrPublished() function to avoid code duplication.
service cloud.firestore {

 match /databases/{database}/documents {

   match /stories/{storyid} {

     // Returns `true` if the requested story is 'published'
     // or the user authored the story
     function authorOrPublished() {
       return resource.data.published == true || request.auth.uid == resource.data.author;
     }

     // Deny any query not limited to 10 or fewer documents
     // Anyone can query published stories
     // Authors can query their unpublished stories
     allow list: if request.query.limit <= 10 &&
                    authorOrPublished();

     // Anyone can retrieve a published story
     // Only a story's author can retrieve an unpublished story
     allow get: if authorOrPublished();

     // Only a story's authors can write to a story
     allow write: if request.auth.uid == resource.data.author;
   }

 }
}

Enable offline data

https://firebase.google.com/docs/firestore/manage-data/enable-offline

Note: Offline persistence is supported only in Android, iOS, and web apps.

  • For Android and iOS, offline persistence is enabled by default. To disable persistence, set the PersistenceEnabled option to false.
  • For the web, offline persistence is disabled by default.
let settings = FirestoreSettings()
settings.isPersistenceEnabled = true

// Any additional options
// ...

// Enable offline data persistence
let db = Firestore.firestore()
db.settings = settings

Listen to offline data

While the device is offline, if you have enabled offline persistence, your listeners will receive listen events when the locally cached data changes. You can listen to documents, collections, and queries.

To check whether you’re receiving data from the server or the cache, use the fromCache property on the SnapshotMetadata in your snapshot event. If fromCache is true, the data came from the cache and might be stale or incomplete. If fromCache is false, the data is complete and current with the latest updates on the server.

By default, no event is raised if only the SnapshotMetadata changed. If you rely on the fromCache values, specify the includeMetadataChanges listen option when you attach your listen handler.

// Listen to metadata updates to receive a server snapshot even if
// the data is the same as the cached data.
db.collection("cities").whereField("state", isEqualTo: "CA")
   .addSnapshotListener(includeMetadataChanges: true) { querySnapshot, error in
       guard let snapshot = querySnapshot else {
           print("Error retreiving snapshot: (error!)")
           return
       }

       for diff in snapshot.documentChanges {
           if diff.type == .added {
               print("New city: (diff.document.data())")
           }
       }

       let source = snapshot.metadata.isFromCache ? "local cache" : "server"
       print("Metadata: Data fetched from (source)")
}

Get offline data

If you get a document while the device is offline, Cloud Firestore returns data from the cache. If the cache does not contain data for that document, or the document does not exist, the get call returns an error.

Query offline data

Querying works with offline persistence. You can retrieve the results of queries with either a direct get or by listening, as described in the preceding sections. You can also create new queries on locally persisted data while the device is offline, but the queries will initially run only against the cached documents.

Disable and enable network access

You can use method below to disable network access for your Cloud Firestore client. While network access is disabled, all snapshot listeners and document requests retrieve results from the cache. Write operations are queued until network access is re-enabled.

Firestore.firestore().disableNetwork { (error) in
   // Do offline things
   // ...
}

Use the following method to re-enable network access:

Firestore.firestore().enableNetwork { (error) in
   // Do online things
   // ...
}

스텝바이 스텝 노트

https://codelabs.developers.google.com/codelabs/firestore-ios/#0

실전 퀵 치트노트

https://firebaseopensource.com/projects/firebase/quickstart-ios/

.