Skip to content

FB4D Reference IFirestoreDocument

Christoph Schneider edited this page Sep 17, 2021 · 26 revisions

Interface IFirestoreDocument

The Firestore database stores and retrieves documents with structured data. Documents can contain either a simple list of fields as well as complex nested objects organized in arrays and records (maps). Documents are organized in collections. In most cases, a collection contains multiple identical structured documents comparable to records in a table of a relational database. However, a collection is more flexible and may contain differently structured documents.

In FB4D, the interface IFirestoreDocument enables read access to documents retrieved from the Firestore and write access to documents that will be written to the Firestore database.

Write Access

The constructor TFirestoreDocument.Create in the unit FB4D.Document allows to create an empty document that can be written later into the database by using the method IFirestoreDatabase.InsertOrUpdateDocument or IFirestoreDatabase.PatchDocument.

To add content to the document, the AddOrUpdateField method allows you to insert a new field or, if the field already exists, to overwrite it with new content. As field value a JSON value is expected. Because the Firestore expects field data with the field name, the declared data type and the value in a Firestore specific JSON format. For this purpose the unit FB4D.Helpers contains a class helper for TJSONObject that offers several class functions to build all types of Firestore fields. The following example shows how easy it is to generate a string value that is added to the field named strField:

Doc.AddOrUpdateField(TJSONObject.SetString('strField', 'This is the string value'));

The following field types are supported in Firestore and FB4D:

TFirestoreFieldType = (fftNull, fftBoolean, fftInteger, fftDouble,
 fftTimeStamp, fftString, fftBytes, fftReference, fftGeoPoint, fftArray,
 fftMap);

For this purpose the TJSONHelper class helper offers these functions that returns a TJSONPair for the field name and the field body.

class function SetString(const VarName, Val: string): TJSONPair;
class function SetInteger(const VarName: string; Val: integer): TJSONPair;
class function SetBoolean(const VarName: string; Val: boolean): TJSONPair;
class function SetDouble(const VarName: string; Val: double): TJSONPair;
class function SetTimeStamp(const VarName: string; Val: TDateTime): TJSONPair;
class function SetNull(const VarName: string): TJSONPair;
class function SetReference(const Name, ProjectID, Ref: string): TJSONPair;
class function SetGeoPoint(const VarName: string; Val: TLocationCoord2D): TJSONPair;
class function SetBytes(const VarName: string; Val: TBytes): TJSONPair;
class function SetMap(const VarName: string; MapVars: array of TJSONPair): TJSONPair;
class function SetArray(const VarName: string; ArrayVars: array of TJSONValue): TJSONPair

The type array and map are nested data types that are built by combinations of other simple data types.

The map type is comparable to a record in Pascal. The following example creates a map that contains two subfields: The string field MapStr and the integer field MapInt:

IFirestoreDocument.AddOrUpdateField(TJSONObject.SetMap('MyMap', [
   TJSONObject.SetString('MapStr', 'Map would be called in Delphi "record"'),
   TJSONObject.SetInteger('MapInt', 324)]);

The array type is more flexible than its pendant in Pascal. The array type is more flexible than an array in Pascal because each element can store a different type. Unlike the type map, the array does not have a field name per element.

In order to build nested data structures with arrays, the TJSONHelper class helper offers for each field type a second class functions to build just the field body without a field name. This second function is named Set<Type>Value and requires only the field value as a parameter:

class function SetString(const VarName, Val: string): TJSONPair;
class function SetStringValue(const Val: string): TJSONObject;

The following examples creates an array that contains three elements: Firstly a string, secondly an integer and thirdly a boolean field:

TJSONObject.SetArray('MyArr', 
  [TJSONObject.SetStringValue('Element0'),
   TJSONObject.SetIntegerValue(1),
   TJSONObject.SetBooleanValue(true)]);

In arrays, each element often contains an identical map as following example shows:

TJSONObject.SetArray('MyCompoundArr', 
 [TJSONObject.SetMapValue(
   [TJSONObject.SetString('FieldA', 'Delphi rocks with Firebase 👨'),
    TJSONObject.SetInteger('FieldB', 4711)],
 [TJSONObject.SetMapValue(
   [TJSONObject.SetString('FieldA', 'A second map element'),
    TJSONObject.SetInteger('FieldB', 4712)]]);

For the sake of completeness, it should also be mentioned that the function AddOrUpdateField offers an overloaded variant with field name and field-body as a parameter:

IFirestoreDocument = interface(IInterface)
  function AddOrUpdateField(Field: TJSONPair): IFirestoreDocument; overload;
  function AddOrUpdateField(const FieldName: string; Val: TJSONValue): IFirestoreDocument; overload;

Hint: The AddOrUpdateField method supports optionally the use of fluent interface design os you can easily add multiple fields in one step without the need of store the IFirestoreDocument in a local variable because you can pass it directly to the consuming method e.g. CreateDocument.

Read Access

When you read a document from the database, the document object is usually created by IFirestoreDatabase. Therefore, the document does not have to be created in the application code.

Following read functions offers to read the header information and the structure of a document:

FirestoreDocument = interface(IInterface)
  function DocumentName(FullPath: boolean): string;
  function DocumentFullPath: TRequestResourceParam;
  function DocumentPathWithinDatabase: TRequestResourceParam;
  function CreateTime: TDateTime;
  function UpdateTime: TDatetime;
  function CountFields: integer;
  function Fields(Ind: integer): TJSONObject;
  function FieldName(Ind: integer): string;
  function FieldByName(const FieldName: string): TJSONObject;
  function FieldValue(Ind: integer): TJSONObject;
  function FieldType(Ind: integer): TFirestoreFieldType;
  function FieldTypeByName(const FieldName: string): TFirestoreFieldType;

The function DocumentName returns the entire path of the document in the Firestore if the boolean parameter is true. If false, the function returns only the document ID in the surrounding collection.

The function DocumentFullPath returns the entire path of the document including the project and database identifier split into an array of strings.

The function DocumentPathWithinDatabase returns do the same but without the project and database identifier.

The function CreateTime returns the time when a document was first written into the database. As long the document is still under construction in FB4D there is no creation time available.

The function UpdateTime returns the timestamp of the last write access for this document.

With the function CountFields and the function Fields(Ind) you can read out the root fields of the document. The function FieldType(Ind) informs about the field type (fftNull..fftMap) of the field.

Read Data of Simple Field Types from Documents

Most of the time you have an expectation regarding the field structure of the read document, so you can use the function FieldByName which returns a TJSONObject. In this way, you can quickly access their field contents via the name and the known field type. With the help of the class helper for TJSONObject in the unit FB4D.Helpers you can easily read a value of the known type.

For root fields, the interface IFirestoreDocument provides an even faster way:

function IFirestoreDocument.GetStringValue(StringFieldName: string): string;
function IFirestoreDocument.GetIntegerValue(IntegerFieldName: string): integer;
function IFirestoreDocument.GetDoubleValue(DoubleFieldName: string): double;
function IFirestoreDocument.GetTimeStampValue(TimeStampFieldName: string): TDateTime;
function IFirestoreDocument.GetBoolValue(BooleanFieldName: string): boolean;
function IFirestoreDocument.GetGeoPoint(GeoFieldName: string): TLocationCoord2DValue;
function IFirestoreDocument.GetReference(ReferenceFieldName: string): string;
function IFirestoreDocument.GetBytes(ByteFieldName: string): TBytes;

If the field in the document is optional, there are additional methods for each field type that require a default value. While GetStringValue throws an exception if the field is not present in the document, the function GetStringValueDef(const FieldName, Default: string): string returns the default value in the same situation.

Read Nested Data Structures from Documents

Read Arrays

For read arrays, the IFirestoreDocument interface offers the following functions:

function GetArraySize(const FieldName: string): integer;
function GetArrayType(const FieldName: string; Index: integer): TFirestoreFieldType;
function GetArrayItem(const FieldName: string; Index: integer): TJSONPair;
function GetArrayValue(const FieldName: string; Index: integer): TJSONValue;
function GetArrayValues(const FieldName: string): TJSONObjects;

While GetArraySize returns the number of element, the function GetArrayType(Index) informs about the element type which can be different for each element! Note that in this point an array of Firebase differs significantly from an array in Delphi.

In order to get the element of an array, you can use the method GetArrayValue(Index) that returns a TJSONValue. To access the value of such array element, use the same class helper functions as GetStringValue and GetIntegerValue without passing in a field name because all the elements in an array are nameless.

For sub-arrays, the TJSONObject class helper function GetArraySize returns the number of elements in an array. To access an element of the array, you can use the following class helper function:

function GetArrayItem(Ind: integer): TJSONObject;

Often in Firebase documents, data are structured within an array of maps. To access directly the elements of the maps the following method can be used:

function GetArrayMapValues(const FieldName: string): TJSONObjects;

Read Maps

For read maps (similar to records in Delphi), the IFirestoreDocument interface offers the following functions:

function GetMapSize(const FieldName: string): integer;
function GetMapType(const FieldName: string; Index: integer): TFirestoreFieldType;
function GetMapValue(const FieldName: string; Index: integer): TJSONValue; overload;
function GetMapValue(const FieldName, SubFieldName: string): TJSONObject; overload;
function GetMapValues(const FieldName: string): TJSONObjects;

For fast access of a variable in a map use the function GetMapValue(const FieldName, SubFieldName: string).

E.g. after creating a complex document in the DemoFB4D application, you can address the MapStr in MyMap with this new function as follows:

Doc.GetMapValue('MyMap', 'MapStr').GetStringValue

Alternatively, for sub-maps, you get the number of elements by the TJSONObject class helper method GetMapSize. To access an element of the map, you can use the following class helper function:

function GetMapItem(Ind: integer): TJSONPair;

Here you get a TJSONPair that contains the name of the element and the value as TJSONObject. Note that elements in maps are not nameless like elements of arrays.

Clone this wiki locally