Skip to content

FB4D Reference IFirestoreDatabase

Christoph Schneider edited this page Oct 2, 2024 · 42 revisions

Interface IFirestoreDatabase

This interface provides all functions for accessing the Firestore Database.

Create an instance for the interface IFirestoreDatabase

The interface will be created by the constructor of the class TFirestoreDatabase in the unit FB4D.Firestore. The constructor expects as parameters the Project ID of the Firebase Project and the instance of the interface IFirebaseAuthentication that is in the state signed-in. If you use more than one database in the same project, you must also pass the DatabaseID.

var
  Database: IFirestoreDatabase;
begin
  Database := TFirestoreDatabase.Create(const ProjectID: string; Auth: IFirebaseAuthentication; const DatabaseID: string = DefaultDatabaseID);

Alternatively, you can use the class factory TFirebaseConfiguration.Database to get an instance of IFirestoreDatabase.

Read one Document of a Collection by their Document Id

The method Get retrieves one distinct document from a collection when the collection and the document ID are passed in the params array. If the document was present in the Firestore collection, the function GetSynchronous returns a document list with one document. If no document was found in the Firestore, nil is returned.

  function GetSynchronous(Params: TRequestResourceParam; 
    QueryParams: TDictionary<string, string> = nil): IFirestoreDocuments;

The following examples show, how a document with a given document ID can be retrieved by using the asynchronous Get method:

Database.Get(['MyCollection', DocumentID: string], nil, OnDocuments, OnError); 

procedure OnDocuments(const Info: string; Documents: IFirestoreDocuments);
var
  Doc: IFirestoreDocument;
begin
  if assigned(Documents) and (Documents.Count = 1) then
  begin
    Doc := Documents.Document(0);
  end;
end;

procedure OnError(const RequestID, ErrMsg: string);
begin
  ShowMessage(ErrMsg);
end;

Read all Documents of a Collection

The Get method can also be used to retrieve all documents from a collection. For this purpose, only the collection name shall be passed within the params array. For collections with more than 20 documents (default page size), the Get method shall be called several times until IFirestoreDocuments.MorePagesToLoad is no longer true. Thereby, in the 2nd parameter TQueryParams the last PageToken must be passed.

var 
  DocsFromLastGet: IFirestoreDocuments; 
begin
  Query := TQueryParams.CreateQueryParams.AddPageSize(10);
  if DocsFromLastGet.MorePagesToLoad then
    Query.AddPageToken(DocsFromLastGet.PageToken);
  Database.Get(['MyCollection'], Query, OnDocuments, OnError); 
end;

Create a new Document

The method CreateDocument allows to create of an empty new document for a given document path.

function CreateDocumentSynchronous(DocumentPath: TRequestResourceParam;
  QueryParams: TQueryParams = nil): IFirestoreDocument

Insert or Update all Fields of a Document

The method InsertOrUpdateDocument allows storing a complete document with a set of fields. In case the document for the given Document.DocumentPath exists already this method overwrites the entire document.

function InsertOrUpdateDocumentSynchronous(Document: IFirestoreDocument;
  QueryParams: TQueryParams = nil): IFirestoreDocument;

If you want to save a loaded and existing document to a different path, the following overloaded method with the additional parameter DocumentPath is useful.

procedure InsertOrUpdateDocumentSynchronous(DocumentPath: TRequestResourceParam;
  Document: IFirestoreDocument; QueryParams: TQueryParams;
  OnDocument: TOnDocument; OnRequestError: TOnRequestError); overload;

Update parts of a Document

The method PatchDocument allows updating only some fields of a document. For this purpose, the parameter UpdateMask contains the list of fields which shall be overwritten from the given DocumentPart. With the empty Mask parameter, the method returns the entire updated document. If a list of fields is defined in the parameter Mask just these fields will be packed into the document that will be returned.

function PatchDocumentSynchronous(DocumentPart: IFirestoreDocument; 
  UpdateMask: TStringDynArray; Mask: TStringDynArray = []): IFirestoreDocument;

If you want to patch a loaded and existing document to a different path, the following overloaded method with the additional parameter DocumentPath is useful.

function PatchDocumentSynchronous(DocumentPath: TRequestResourceParam;
  DocumentPart: IFirestoreDocument; UpdateMask: TStringDynArray;
  Mask: TStringDynArray = []): IFirestoreDocument; overload;

Delete a Document in a Collection

The method Delete allows deleting a distinct document in a collection.

function DeleteSynchronous(Params: TRequestResourceParam): IFirebaseResponse;

The status in the response informs if the deletion was successfully done.

Hint: The deprecated version of this method has an additional optional parameter QuerParams which had no functional purpose. The previous method is marked as deprecated and will be removed with version 1.3.

Search for Documents in a Collection by using a Filter Criteria for Fields

The method IFirestoreDatabase.RunQuery allows retrieving data from the Firestore by using a structured query that is comparable to an SQL query statement. As a parameter, an instance to a structured query is expected. It returns by IFirestoreDocuments a list of documents that contains usually just one document.

function RunQuerySynchronous(StructuredQuery: IStructuredQuery): 
  IFirestoreDocuments;

For queries on child collections there is an additional overloaded method available that takes a document path:

function TFirestoreDatabase.RunQuerySynchronous(
  DocumentPath: TRequestResourceParam;
  StructuredQuery: IStructuredQuery): IFirestoreDocuments; overload;

Be aware that the last (lowest) level of the document path must not be written in the DocumentPath because this document name shall be passed in IStructuredQuery.Collection.

Firestore listener

In order to be able to react to changes in the Firestore, a listener has been introduced for the following monitoring functions:

  • Monitoring of a single document
  • Monitoring of a whole collection:
    • A new document has been added
    • A document has been changed
    • A document has been deleted
  • Monitoring a collection with a filter criterion

The listener can monitor several documents and collections at the same time. To do this, the monitoring must first be registered with Subscribe before the listener can be started.

The following function monitors a single document. In case of changes in the document, the OnChanchedDoc call back method will be called and returns the entire document. In case the observed document was deleted, the OnDeletedDoc call back method will be started. You can subscribe to more than one document by a repeated call of SubscribeDocument. The function returns the target ID for this subscription.

function SubscribeDocument(DocumentPath: TRequestResourceParam;
  OnChangedDoc: TOnChangedDocument;
  OnDeletedDoc: TOnDeletedDocument): cardinal;

type
  TOnChangedDocument = procedure(ChangedDocument: IFirestoreDocument) of object;
  TOnDeletedDocument = procedure(const DeleteDocumentPath: string;
    TimeStamp: TDateTime) of object;

To be able to monitor all documents in a collection, use SubscribeQuery. Thereby you can use the filter options to restrict the resulting set of documents by using a query. The sort ordering of the resulting document set is also possible. For each changed or newly created document you get a call of the call back method OnChangedDoc.

function SubscribeQuery(Query: IStructuredQuery;
  OnChangedDoc: TOnChangedDocument; OnDeletedDoc: TOnDeletedDocument;
  DocumentPath: TRequestResourceParam = []): cardinal;

If all documents of a subcollection should be monitored, the path to the subcollection without the subcollection's own name can be specified with the optional parameter DocumentenPath. Unfortunately, the Firestore does not support monitoring changes in nested collections. In this case IncludesDescendants must be set to false.

The following example shows how to monitor all documents in the users/christoph/bookings/* subcollection:

fDatabase.SubscribeQuery(TStructuredQuery.CreateForCollection('bookings'),
  OnFSChangedDocInCollection, OnFSDeletedDocCollection, ['users', 'christoph']);

After stopping the listener, you can change your subscription by removing previous targets with Unscubscribe and adding new ones.

procedure Unsubscribe(TargetID: cardinal);

Finally, you can start the Firestore listener. Thereby giving a method that will be called when the listener stops again. Additionally, you register an event handler for the error case. Optionally, you can be informed when the authorization tokens need to be renewed. If the connection to the Firestore Server will be interrupted, the listener will automatically re-establish the connection as soon as the server is available again. To let the application know if the Firestore Server Connection is active, a callback method for OnConnectionStateChange can be registered. In case the optional flag DoNotSynchronizeEvents is set, the listener calls all callback methods including OnChangedDoc and _OnDeletedDoc from the background thread. This can be useful within a service application or if no GUI update shall be done within the callback methods.

procedure StartListener(OnStopListening: TOnStopListenEvent;
  OnError: TOnRequestError; OnAuthRevoked: TOnAuthRevokedEvent = nil;
  OnConnectionStateChange: TOnConnectionStateChange = nil;
  DoNotSynchronizeEvents: boolean = false);

type
  TOnStopListenEvent = procedure(Sender: TObject) of object;
  TOnRequestError = procedure(const RequestID, ErrMsg: string) of object;
  TOnAuthRevokedEvent = procedure(TokenRenewPassed: boolean) of object;
  TOnConnectionStateChange = procedure(ListenerConnected: boolean) of object;

If you are no longer interested in change notification for the subscriptions or the monitoring targets need to be changed, call StopListener.

procedure StopListener(RemoveAllSubscription: boolean = true);

In case of a connection interruption to the Firestore Server, you would like to know when the last message from the server arrived? The following function returns this local PC time. The last message can be from the listener or from the last Firestore request.

function GetTimeStampOfLastAccess: TDateTime;

There are situations in OnChangedDoc and OnDeletedDoc of the listener in which it is important to know whether other document changes have already been received and will be processed next. In this situation, for example, a GUI update can be postponed, which makes the application faster and can eliminate annoying flickering. For this purpose, the method CheckListenerHasUnprocessedDocument or the property ListenerHasUnprocessedDocuments: boolean was introduced, which informs about pending documents.

Note about the exception while stopping the listener

When running the application in the Delphi IDE, you will see this silent exception of the class ENetHTTPResponseException with message 'Error reading data: (12017) The operation has been canceled'. This is not a defect, because it is a silent exception that the user will never see.

Read Transactions

To read multiple documents from the same or even different collections at the same time, you can use a Read transaction. For example, use the following methods to read both documents MyCollection/MyDocId1 and MyCollection2/MyDocId2 from the same timestamp:

var Transaction: TFirestoreReadTransaction;

Transaction := IFirestoreDatabase.BeginReadTransaction;

Query := TQueryParams.CreateQueryParams.AddTransaction(Transaction);
fDatabase.Get(['MyCollection', 'MyDocId'], Query, OnFirestoreGet, OnFirestoreError)
fDatabase.Get(['MyCollection2', 'MyDocId2'], Query, OnFirestoreGet, OnFirestoreError)

Write Transactions

To start a write transaction, the BeginWriteTransaction method is called, which returns an interface to create, modify or extend documents.

function BeginWriteTransaction: IFirestoreWriteTransaction;

type
  IFirestoreWriteTransaction = interface
    function NumberOfTransactions: cardinal;
    procedure UpdateDoc(Document: IFirestoreDocument);
    procedure PatchDoc(Document: IFirestoreDocument;
      UpdateMask: TStringDynArray);
   procedure TransformDoc(const FullDocumentName: string;
      Transform: IFirestoreDocTransform);
    procedure DeleteDoc(const DocumentFullPath: string);
  end;

The NumberOfTransactions method returns the number of UpdateDoc, PatchDoc or DeleteDoc calls.

The UpdateDoc method prepares to create a new document or overwrite an existing one.

The PatchDoc method prepares the modification of an existing document by adding or changing existing fields.

The DeleteDoc method prepares to delete an existing document.

The TransformDoc method offers to update a document by using the following server side calculations.

  IFirestoreDocTransform = interface
    function SetServerTime(const FieldName: string): IFirestoreDocTransform;
    function Increment(const FieldName: string;
      Value: TJSONObject): IFirestoreDocTransform;
    function Maximum(const FieldName: string;
      Value: TJSONObject): IFirestoreDocTransform;
    function Minimum(const FieldName: string;
      Value: TJSONObject): IFirestoreDocTransform;
  end;
  • SetServerTime set the server time of the commited write transaction into a timestamp field.
  • Increment allows to increment an integer of double field with a given increment value.
  • Maximum calculates the maximal value of the field content and a given value and stores the result into an integer or a double field.
  • Minimum calculates the minimum value of the field content and a given value and stores the result into an integer or a double field.

However, the documents in the database are not changed until a commit write transaction is executed, which executes all cumulated changes from the IFirestoreWriteTransaction.

  function CommitWriteTransactionSynchronous(
    Transaction: IFirestoreWriteTransaction): IFirestoreCommitTransaction;

  IFirestoreCommitTransaction = interface
    function CommitTime(TimeZone: TTimeZone = tzUTC): TDateTime;
    function NoUpdates: cardinal;
    function UpdateTime(Index: cardinal; TimeZone: TTimeZone = tzUTC): TDateTime;
  end;

If all changes could be written into the database with Commit, the method CommitWriteTransaction returns all timestamps of the updates and the whole Commit action.

Clone this wiki locally