Working With Offline Data using Briefcases

Overview

As discussed Briefcases provide a means to persist client data in either a file or a folder on the local filesystem. This can be useful in different scenarios, for example:

  • A Briefcase can be used to store uncommitted data changes between client application restarts
  • A Briefcase can be used to store rarely changed dictionary and lookup data. This could be used to avoid reloading the same data from the server every time the client application is restarted
  • A Briefcase can be used to implement an "offline" mode for the client application that allows the user to continue working with their data.

There are two different models of briefcase files - single-file briefcases (DAFileBriefcase) and folder-based briefcases (DAFolderBriefcase) which are descended from a common DABriefcase class.

With a File briefcase, all data tables and custom properties are stored in one single file (usually with a .daBriefcase extension). This provides convenient storage and makes it easy for users to handle briefcase files if necessary and to share them between platforms, but imposes some restrictions on the flexibility with which individual data tables can be read from and written to the file.

A Folder briefcase uses a package to store the briefcase. This is simply a folder that typically uses the .briefcase path extension. Inside that package/folder each table is stored as an individual file briefcase with a separate .plist file used for custom properties. This allows more flexible access for reading and updating individual tables.

In both cases, the data is stored using the highly efficient binary format of the Bin2DataStreamer, which is also recommended for transferring data between client & server.

The main advantage of using a Briefcase, instead of some embedded database (for example SQLite) to cache user data or to implement your own offline mode, is that a Briefcase stores not only user data but also the state of that data (ie was some data row deleted, updated or inserted). This greatly simplifies further synchronization with a Data Abstract-based server.

Note: Briefcase methods are not thread-safe. Briefcase data tables are safe for multithreaded read operations, however any write operations must be synchronized.

Working with Briefcases

The intention is that you don't directly work with the DAFileBriefcase or DAFolderBriefcase. Instead you use the DABriefcase's briefcaseWithFile and briefcaseWithFolder methods to return an instance of those classes.

Initializing a Briefcase

 

DABriefcase *briefcase = [DABriefcase briefcaseWithFile:briefcaseFileName forReading:YES];

//or

DABriefcase *briefcase = [DABriefcase briefcaseWithFile:briefcaseFileName]; // assumes forReading:YES

 

let briefcase = DABriefcase.briefcaseWithFile(briefcaseFileName, forReading: true)

//or

let briefcase = DABriefcase.briefcaseWithFile(briefcaseFileName) // assumes forReading:true

 

var briefcase := DABriefcase.briefcaseWithFile(fBriefcaseFileName) forReading(true);

//or

var briefcase := DABriefcase.briefcaseWithFile(fBriefcaseFileName); // assumes forReading:true

 

var briefcase = DABriefcase.briefcaseWithFile(briefcaseFileName) forReading(true);

//or

var briefcase = DABriefcase.briefcaseWithFile(briefcaseFileName); //assumes forReading(true)

When the second constructor parameter is set to true, then the Briefcase will be opened and all of the data tables and properties it contains will be deserialized and loaded into memory. Passing false will only load the table names allowing you to lazily load the data as required. If you don't supply the second parameter then it defaults to true.

There is no need to add a .daBriefcase or .briefcase extension to the file name. If there is no extension in the provided name, then the appropriate extension will be automatically added; '.daBriefcase' for a file briefcase and '.briefcase' for a folder briefcase.

NOTE attempting to load a briefcase for reading that doesn't exist will generate a DA Exception. Either test for the files existence before opening it for reading, or wrap the call in exception handling.

 

DABriefcase *briefcase = [DABriefcase briefcaseWithFolder:briefcaseFileName];

 

let briefcase = DABriefcase.briefcaseWithFolder(briefcaseFileName)

 

var briefcase := DABriefcase.briefcaseWithFolder(fBriefcaseFileName);

 

var briefcase = DABriefcase.briefcaseWithFolder(briefcaseFileName);

Unlike briefcaseWithFile, briefcaseWithFolder will use lazy loading and only retrieve the table names from the briefcase folder, as well as all of the properties. When tableNamed: is used to get a table, that tables data will be read from disk. This allows memory consumption to be controlled so that large tables that might be rarely accessed aren't needlessly in memory.

Also if the briefcase folder does not exist, then briefcaseWithFolder will create it for you. No exception will be raised unless there was a problem creating the folder.

Adding, Retrieving and Removing tables from a Briefcase

Operations to access the tables in a briefcase are the same regardless of whether you are using a file- or folder- based briefcases. Remember when working with the briefcase any changes are only made in memory until the briefcase is written back to disk.

  • To add a table to the briefcase use the addTable method. Note that if a table of that name already exists, it will be replaced:

 

[briefcase addTable:dataTable];

 

briefcase.addTable(dataTable)

 

briefcase.addTable(dataTable);

 

briefcase.addTable(dataTable);

To add multiple tables at one, you can use the addTables] method which takes an array of DADataTables.

  • To retrieve a table from the briefcase by its name you use tableNamed:

 

[briefcase tableNamed:@"TableName"];

 

briefcase.tableNamed("TableName")

 

briefcase.tableNamed("TableName");

 

briefcase.tableNamed("TableName");

Note: this method returns a copy of the instance being stored in the briefcase, so changing data table acquired via the tableNamed method won't affect the data stored in the briefcase.

 

[briefcase removeTableNamed:@"TableName"];

 

briefcase.removeTableNamed("TableName")

 

briefcase.removeTableNamed("TableName");

 

briefcase.removeTableNamed("TableName");

 

[briefcase removeAllTables];

 

briefcase.removeAllTables()

 

briefcase.removeAllTables();

 

briefcase.removeAllTables();

Using custom data properties in a Briefcase

A briefcase can contain multiple string properties that you define. The intention is that it could be used to store the version number of the app so that when an update occurs that changes the data format, then the briefcase can be ignored. It is not recommended or intended that the briefcase is used for general application settings.

  • Add a custom property to the briefcase

 

briefcase.properties[@"PropertyName"] = @"PropertyValue";

 

briefcase.properties["PropertyName"] = "PropertyValue"

 

briefcase.properties["PropertyName"] := "PropertyValue";

 

briefcase.properties["PropertyName"] = "PropertyValue";
  • Accessing the custom property by name

 

propertyValue = briefcase.properties[@"PropertyName"];

 

propertyValue = briefcase.properties["PropertyName"]

 

propertyValue := briefcase.properties["PropertyName"];

 

propertyValue = briefcase.properties["PropertyName"];
  • Removing a custom property specified by name

 

[briefcase.properties removeObjectForKey:@"PropertyName"];

 

briefcase.properties.removeObjectForKey("PropertyName")

 

briefcase.properties.removeObjectForKey("PropertyName");

 

briefcase.properties.removeObjectForKey("PropertyName");
  • Removing all properties from a briefcase

 

[briefcase.properties removeAllObjects];

 

briefcase.properties.removeAllObjects()

 

briefcase.properties.removeAllObjects();

 

briefcase.properties.removeAllObjects();
  • Getting a count of the available properties

 

propertiesCount = [briefcase.properties count];

 

propertiesCount = briefcase.properties.count

 

propertiesCount := briefcase.properties.count;

 

propertiesCount = briefcase.properties.count;

Writing the Briefcase to disk

As noted above, changes to a briefcase are only stored on disk when you write the briefcase.

 

[briefcase writeBriefcase];

 

briefcase.writeBriefcase()

 

briefcase.writeBriefcase();

 

briefcase.writeBriefcase();

 

[briefcaseFolder writeTable:productsTable]; // write single DADataTable
[briefcaseFolder writeTables:dataTables]; // write an array of DADataTables

 

briefcaseFolder.writeTable(productsTable) // write single DADataTable
briefcaseFolder.writeTables(dataTables) // write an array of DADataTables

 

briefcaseFolder.writeTable(productsTable); // write single DADataTable
briefcaseFolder.writeTables(dataTables); // write an array of DADataTables

 

briefcaseFolder.writeTable(productsTable); // write single DADataTable
briefcaseFolder.writeTables(dataTables); // write an array of DADataTables

 

[briefcaseFolder writeProperties]; // writes all properties

 

briefcaseFolder.writeProperties() // writes all properties

 

briefcaseFolder.writeProperties(); // writes all properties

 

briefcaseFolder.writeProperties(); // writes all properties

See also