The Remote Data Adapter and Data Tables

This article wive give you an in-depth look at the three the core classes in Data Abstract for Cocoa: DARemoteDataAdapter, DADataTable and DADataTableRow.

The DARemoteDataAdapter (frequently referred to as the “RDA”) acts as a central connection point for your client application to communicate with the server, to receive data tables and send updates and handle any other data-related communication tasks. Data received from the server is represented by DADataTables, which are essentially record sets that contain the individual rows of data and the necessary meta-data to work with them, and also keep track of local changes made to any of the values in the table. Each individual row within a table is represented by a DADataTableRow.

You will be working extensively with these three classes throughout your apps and in your day-to-day use of Data Abstract, so let’s have a look at them in more detail.

DARemoteDataAdapter

As stated above, the remote data adapter can be thought of as the central “hub” that lets your application communicate with the server. In most applications, you will have a single RDA instance that you configure to connect to your server and use to retrieve data and apply updates. The RDA gets the first part of its name because it is specifically designed to be used on client applications to get data from a remote server. When you dive into Business Rules Scripting, you will get to know a close relative of the RDA, the Local Data Adapter.

The remote data adapter itself contains two "remote services" (a class covered in more detail in our article about the Communication Infrastructure) called dataService and loginService, which are exposed via properties of the same names. Each remote service encapsulates two important pieces of information: the network address of your server and the name of a service (every server can, in theory, expose an unlimited number of services – be they Data Abstract data services or regular web services). The network address is encoded in form of a URL, and actually does not just contain the address, but also the form of communication (typically HTTP or TCP) and the data encoding used over the wire. This will also be looked at in more detail in the above-mentioned topic.

The most common way to set up a new RDA is to use the initWithTargetURL: method, or the corresponding adapterWithTargetURL: static method on the class. This initializes both remote services of the RDA with the passed URL, and the default names of “DataService” and “LoginService”, respectively.

When talking to Relativity Server, these default names will be correct. However, when talking to a custom Data Abstract server, you might need to specify different service names if the server is not using the default service names, or exposes multiple data services under different names. You can configure the service name by calling, for example:


[[rda dataService] setServiceName:@"MyDataService"];

rda.dataService.serviceName = "MyDataService"

rda.dataService.serviceName = "MyDataService";

rda.dataService.serviceName := 'MyDataService';

where “MyDataService” would be the name of the service your application wants to talk to.

For custom servers, it is fairly common to expose more than one data service and partition the data that these services expose logically (for example, there might be a "PayrollService" providing access to employee data, and an independent "FulfillmentService" that gives access to order fulfillment data). In this case, your application would pick the one service it needs to talk to, or contain multiple remote data adapters, each configured for a particular service.

Relativity Server always exposes data using the DataService name, but if your domain contains more than one schema, you can append the schema name to the service name to configure your RDA to talk to a specific schema, such as “DataService.Fulfillment”.

We have separate topics dedicated to Configuring Relativity Server and its domains and schemas.

The second important part of setting up a remote data adapter is to assign a delegate that implements the DARemoteDataAdapterDelegate protocol. The remote data adapter will try to call into the delegate to notify your application about error conditions or when it needs login. It is common to implement the delegate methods on the class that contains the RDA, like the DataAccess class we have seen in the previous chapter:


[rda setDelegate:self];

rda.delegate = self

rda.delegate = this;
rda.delegate := self;

With this setup out of the way, the remote data adapter is ready to be used for its main purpose: retrieving data from the server. For this, the RDA exposes a wide range of methods whose names start with either getDataTable* or beginGetDataTable*. The methods in the first group perform a synchronous data request, meaning that when you call them, your code will block until the request is complete and the method will return with the data you requested (or an error).

The methods in the second group perform an asynchronous request, meaning they do not block, but return right away, while your request for data is performed in the background. Once the request is completed (or has failed), your code will either receive a callback to a delegate method or to a block that you passed to beginGetTable*.

By now, blocks are a fairly established concept in Cocoa, introduced back in Mac OS X Snow Leopard and iOS 4.0. Essentially, blocks allow you to pass a section of code that will be executed at a later time, similar to anonymous methods or closures in other languages. Blocks have full access to the surrounding scope, and thus provide a much more convenient way for defining callbacks than a delegate method or a separate function. For example, the following call to getDataTable:withBlock: might be used to retrieve a table without blocking the calling thread:


// Here we decide we need some data from the server
[rda beginGetDataTable:@"Customers" withBlock:^(DADataTable * customers){

  // Here, “customers” contains the data retrieved from the server

}];
 // Here our main thread keeps running, while data is being fetched

// Here we decide we need some data from the server
rda.beginGetDataTable("Customers") { customers: DADataTable! in

  // Here, “customers” contains the data retrieved from the server

}
 // Here our main thread keeps running, while data is being fetched

// Here we decide we need some data from the server
rda.beginGetDataTable("Customers") withBlock( (DADataTable customers) => {

  // Here, “customers” contains the data retrieved from the server

});
 // Here our main thread keeps running, while data is being fetched

// Here we decide we need some data from the server
rda.beginGetDataTable('Customers') withBlock( method (aCustomers: DADataTable) begin

  // Here, “aCustomers” contains the data retrieved from the server

end);
 // Here our main thread keeps running, while data is being fetched

The nice thing about this syntax is that the code which will respond to your data request being completed can be written within the same context as the original request, effectively making the asynchronous nature of the call more transparent and allowing you to see both parts of the code (what happens leading up to your data request, and what happens once the data is retrieved) in one single place.

If you are new to blocks, we recommend having a look at the thorough documentation for blocks and Grand Central Dispatch provided by Apple.

Asynchronous Calls

Your first instinct might be to conclude that synchronous access via the getDataTable* methods seems much more straight-forward and easier to do than bothering with asynchronous calls and callbacks. After all, your code can just ask for data, wait a bit, and get what it asked for.

And you’re right, synchronous calls are a lot easier, on the surface. But they have one decisive drawback, and that is speed. No matter how fast your server and how good your network connection (and let’s face it, when your application is running on an iOS device, network speed will often be unreliable, or the network might go down altogether, as your user drives into a tunnel), there will always be an unpredictable latency between the time you make a call to retrieve data and the time your request is honored and the data is received. Whether that delay is measured in milliseconds or in seconds, it will be too long to be acceptable for locking up your user interface.

When the user touches a button to drill into details for an order, they are probably perfectly willing to wait a couple of seconds while your application fetches those details across the network. Your users are reasonable people, after all. But they will rightfully expect your user interface to be responsive in the meantime. For example, it may be just fine to slide your UI to a new view that displays a “downloading” message and an activity indicator for a split second – but this new view should slide into view right away when the user touches the screen. The user will not tolerate your application seeming to freeze for 2 seconds while it gets data.

Asynchronous requests allow you to fire off that request for data and forget about it – letting your main thread get back to serving the user. Once the request completes, your code gets to take control again, and you can put that data into view. We will see this in action in the next chapter.

The way Data Abstract implements asynchronous requests, it is ensured that the callback to your block or delegate will always happen in the same thread or queue that initiated the original request. This means that when you call beginGetTable* on your main UI thread (for example in reaction to a table cell being tapped), your main thread will keep running and not block (for example, it might go on to push that new view onto the navigation controller, so your user sees something is happening). But once the request has completed and your data has been downloaded, your callback will be executed on the main thread, allowing you to work with your data and your UI (for example telling the table view to reload and show the data), without having to worry about thread synchronization.

If you call beginGetDataTable* in a background thread or on a queue, you can similarly be assured that your callbacks will happen on that same background thread (but if – for whatever reason – you prefer to receive the callback on a background thread even though you called beginGetDataTable* on the main thread, there are provisions for that, too, as we will see soon).

DAAsyncRequest

If you look at the header definitions for the beginGetDataTable* methods, such as those depicted below, you will notice that all the asynchronous methods return a special class called :


- (DAAsyncRequest *) beginGetDataTable:(NSString *)tableName start:(BOOL)start;

public func beginGetDataTable(tableName: String, # start: Boolean) -> DAAsyncRequest?

public DAAsyncRequest beginGetDataTable(NSString tableName) start(Bool start);

method beginGetDataTable(tableName: String; start: Boolean):DAAsyncRequest;

While in many cases you can just discard the returned object (especially when using blocks), DAAsyncRequest does give you more control over the execution of the background request, if you need it.

For one, you see that some versions of the beginGetDataTable* methods take an optional start parameter. By passing NO for the start parameter, you are returned a DAAsyncRequest object that represents the data request you made, but that request has not actually been started (i.e. sent to the server) yet and is awaiting your command to do so. This gives you the opportunity to manually call start or startInBackground at a later time. While the actual data request will of course always happen in the background (else it would not be an asynchronous request after all), calling startInBackground will cause your callback to be triggered in a background thread rather than on the thread that initiated the call. Calling plain start will cause your callback to be hit on the same thread that called start, as discussed above.

Calling start manually also gives you a chance to perform further setup on the DAAsyncRequest before it starts. For example, you can assign a delegate object to receive callbacks when the request succeeds or fails or the server requests authentication (if no delegate is assigned, the request automatically inherits the delegate from the RDA). You can also assign an arbitrary object to the context property, which will later allow you to identify the particular request object – this is useful, for example, if you start several independent requests with the same delegate.


DAAsyncRequest *ar = [rda beginGetDataTable:@"Customers" start:NO];
[ar setDelegate:self];
[ar setContext:@"Retrieving Customers"];
[ar start];

let ar = rda.beginGetDataTable("Customers", start: false)
ar.delegate = self
ar.context = "Retrieving Customers"
ar.start()

DAAsyncRequest ar = rda.beginGetDataTable("Customers") start(false);
ar.delegate = this;
ar.context = "Retrieving Customers";
ar.start();

var ar := rda.beginGetDataTable('Customers') start(false);
ar.delegate := self;
ar.context := 'Retrieving Customers';
ar.start();

When using blocks, you can also assign a second failureBlock to the asynchronous request, which will be called (in addition to the regular block) if the request fails.

It is worth pointing out that DAAsyncRequest slightly varies from the standard delegate patterns in Cocoa. The common rule is that delegate objects are not retained (or are stored weak, in ARC parlance), to avoid retain cycles. DAAsyncRequest behaves the same, in principle, with the slight variation that it will automatically retain (or strongly store) the delegate when the request is started, and release it again once the request is completed.

The end result is that DAAsyncRequest’s delegate behaves pretty much like you would expect, except that you don’t need to worry about your delegate going out of scope while a request is executing.

A common scenario we found when developing iOS applications with Data Abstract was that the application would push in a new view and that view would trigger some data requests, with itself as delegate. This was fine, except for when the user navigated back, popping and releasing the view, before the request completed. When the background request would later complete, its delegate was no longer valid. The only solution was to meticulously keep track of all DAAsyncRequests and reset their delegate reference to nil as your view dealloc’ed.

This was a lot of extra coding overhead, and a nightmare to maintain and debug. The improved “smart retention” of DAAsyncRequest’s delegate reference alleviates the need for this: you can just assign a delegate and be sure the delegate will be around at least as long as the request is running.

Asynchronous Requests with Delegate Callbacks

If you do not want to use blocks, you can use callbacks to your delegate to be informed when your data requests have completed. But as blocks are now supported since iOS 4.0 and OS X 10.6, there is really no good reason not to use them, and the APIs discussed below are provided mainly for backwards compatibility.

As seen before, you can either manually assign a delegate to the DAAsyncRequest itself (if you do so, make sure to assign that before starting your request to avoid concurrency issues and the highly unlikely event that your call completes before your assignment does) or implement these methods on your RDA’s delegate.

There are three delegate methods that are most relevant to this task:


[[rda dataService] setServiceName:@"MyDataService"];
 - (void)asyncRequest:(DAAsyncRequest *)request
      didReceiveTable:(DADataTable *)table;
 - (void)asyncRequest:(DAAsyncRequest *)request
     didReceiveTables:(NSDictionary *)tables;
 - (void)asyncRequest:(ROAsyncRequest *)request
 didFailWithException:(NSException *)exception;

The first two methods are called on your delegate once the data request has completed. As the names indicate, asyncRequest:didReceiveTable: is called for each individual table; if you requested more than one table in one go, for example by calling beginGetDataTables:, this delegate method will be called multiple times. In contrast, asyncRequest:didReceiveTables: will be called once per request, passing a dictionary with all the tables you requested, keyed by name. In most cases, you would implement one, but not both of these methods, depending on how you prefer to handle the tables in your code. For example, implementing asyncRequest:didReceiveTables: might make sense if you want to work with several tables at once, while asyncRequest:didReceiveTable: makes sense if you plan to perform independent actions for each table (you can use [table name] to distinguish which table you received with each call).

The third method will be called if any error occurred during your request – for example the server might have been unreachable, or denied access to the table. You should make sure to always implement this method, lest you’d be wondering why your requests never seem to return.


 // Somewhere in your code
 [rda setDelegate:self];
 [rda getDataTables:[NSArray arrayWithObjects:@“Customers”, @“Orders”, nil];

 - (void)asyncRequest:(DAAsyncRequest *)request
      didReceiveTable:(DADataTable *)table
 {
   if ([[table name] isEqualToString:@“Customers”])
   {
     // Do work with Customers table here
   }
   …
 }

 - (void)asyncRequest:(ROAsyncRequest *)request didFailWithException:(NSException *)exception
 {
   // Show error to the user
 }

Another delegate method you might want to implement, either for your DAAsyncRequest delegate or more globally on the RDA’s delegate, is asyncRequest:didReceiveDataSize:ofExpected:, which will be called repeatedly as data is transferred from the server to the client. If you are downloading larger tables and expect transfers to take more than a few seconds, it allows you to present an accurate progress indicator to let the user know how the data retrieval is progressing.


 - (void)asyncRequest:(ROAsyncRequest *)request didReceiveDataSize:(int)size
                                                ofExpected:(int)totalSize
 {
   NSLog(@“Downloaded %d%%”, size*100/totalSize);
 }

func asyncRequest(request: ROAsyncRequest!, didReceiveDataSize size: Int,
                  ofExpected totalSize: Int) {
  NSLog("Downloaded %d%%", size*100/totalSize);
}

void asyncRequest(ROAsyncRequest request) didReceiveDataSize(int size)
                                          ofExpected(int: totalSize)
{
  NSLog(@“Downloaded %d%%”, size*100/totalSize);
}

method asyncRequest(request: ROAsyncRequest) didReceiveDataSize(aSize: Integer)
                                             ofExpected(aTotalSize: Integer);
begin
  NSLog('Downloaded %d%%', aSize*100/aTotalSize);
end;

Different Ways to Request Data

We’ve seen that the getData* methods on the remote data adapter come in different flavors, synchronous vs. asynchronous, with or without blocks and with optional control to not start the request. These are four different choices as to how your request is handled. In addition, there are also different ways you can decide what data to get.

In its simplest form, the getData* methods accept the name of a single data table and fetch this one table from the server. There is also an overload that accepts an NSArray with table names and can retrieve several tables at once. These are the getDataTable: and getDataTables: methods, respectively.

There are also overloads that accept an SQL statement (or several SQL statements) to allow you to more dynamically query data and fetch specific rows and columns, rather than whole tables. These overloads are getDataTable:withSQL: and getDataTables:withSQL:, respectively. Because enforcement of business rules is an important aspect of a proper multi-tier architecture, these SQL statements will not be passed straight through to the database, but will instead be processed and executed within the middle tier – a technology called DA SQL that is covered in more detail in the DA SQL topic. DA SQL is a core part of what makes Data Abstract so powerful.

A third set of overloads, which we will not cover in this topic as it exists largely for legacy reasons, allows you to limit the scope of the data you are requesting by specifying a list of fields and an XML query that expresses a filter condition called “Dynamic Where”. In just about any scenario, using DA SQL is the better choice than Dynamic Where when talking to modern Data Abstract Servers or Relativity Server.

All in all, this might seem like an overwhelming number of methods to fetch data (24 to be exact), but if you think of them as a 3-dimensional cube of options (3 choices to specify what to fetch, 4 ways to execute the request, and for either a version for single or multiple tables) they become very manageable, and the different options prove to be very convenient.

No matter how you fetch your data though, one thing remains true: you will receive it in form of one or more DADataTables. So let’s leave the remote data adapter behind us and take a closer look at the DADataTable class.

DADataTable

A “data table”, implemented by the DADataTable class in the Data Abstract framework, represents a set of rows or records containing data adhering to a common structure as defined by the table’s fields. Each row of the table will contain the same fields (although not each value might have a definite value for each field and might instead contain NULL – a placeholder that is common in database systems to represent the absence of a proper value. NULL is roughly comparable, although not completely identical, to nil in Objective-C, Swift and Oxygene, and null in C#).

The structure, that is, the list of fields, contained within a data table might be an exact representation of a table that is exposed by the server; it might also be a subset or a superset if you used DA SQL to request a more complex query.

We can use the following code to retrieve a table from our "PCTrade", a sample domain that comes with Relativity Server, and inspect the table:


DADataTable *clientsTable = [rda getDataTable:@"Clients"];
NSLog(@"Clients Table: %@", clientsTable);

let clientsTable = rda.getDataTable("Clients")
NSLog("Clients Table: %@", clientsTable)

DADataTable clientsTable = rda.getDataTable("Clients");
NSLog("Clients Table: %@", clientsTable);

var clientsTable := rda.getDataTable('Clients');
NSLog('Clients Table: %@', clientsTable);

This will produce something like the following output:

Clients Table: <DADataTable Clients>
  <DAFieldDefinition Clients.ClientId, 0x4ca8850 PK>
  <DAFieldDefinition Clients.ClientName, 0x4ca8fb0>
  <DAFieldDefinition Clients.ContactPhone, 0x4ca9700>
  <DAFieldDefinition Clients.ContactAddress, 0x4ca9e50>
  <DAFieldDefinition Clients.AdditionalInfo, 0x4caa5a0>
  <DAFieldDefinition Clients.ClientDiscount, 0x4caacf0>

As you can see, our data table contains six fields, which is how it was exposed on the server. Alternatively, we could use the following code to retrieve only a subset of the fields:


DADataTable *clientsTable = [rda getDataTable:@"Clients"
                                       withSQL:@"SELECT ClientId, ClientName FROM Clients"];
 NSLog(@“Clients Table: %@“, clientsTable);

let clientsTable = rda.getDataTable("Clients",
                       withSQL: "SELECT ClientId, ClientName FROM Clients")
NSLog("Clients Table: %@", clientsTable)

DADataTable clientsTable = rda.getDataTable("Clients")
                               withSQL("SELECT ClientId, ClientName FROM Clients");
NSLog("Clients Table: %@", clientsTable);

var clientsTable := rda.getDataTable('Clients')
                        withSQL('SELECT ClientId, ClientName FROM Clients');
NSLog('Clients Table: %@', clientsTable);

which would yield the following:

Clients Table: <DADataTable Clients>
  <DAFieldDefinition Clients.ClientId, 0x4ca8850 PK>
  <DAFieldDefinition Clients.ClientName, 0x4ca8fb0>

Since we only asked the server for two fields, this is all our local DADataTable knows about, and our client application cannot use this table to access, say, the “ContactPhone” field.

The data table contains a read-only, non-mutable fields property, which is an array that gives you access to all the fields defined on the table, for all the rows. You can use this array and the contained DAFieldDefinition objects to inspect meta-data about the data table at runtime (for example, you could find out what fields are required, what data type they have, or what size of string is allowed for a given string field).

More interesting than the information about fields, though, is the content of the data table. It can most easily be accessed by the rows property:


NSArray *rows = [clientsTable rows];
NSLog(@"the table contains %d rows", [rows count]);

let rows = clientsTable.rows!
NSLog("the table contains %d rows", rows.count)

NSArray rows = clientsTable.rows;
NSLog("the table contains %d rows", rows.count);

var rows = clientsTable.rows;
NSLog('the table contains %d rows', rows.count);

Since the rows are a regular NSArray, you can use all the standard operations that work on arrays, including iterating over the elements, accessing individual rows by objectAtIndex: or via subscripts, and so on. You can also use any of the standard mechanisms such as NSSortDescriptors or NSPredicates to sort or filter rows – a topic that we will look at in more detail in the Working With Data topic.

DADataTable also provides a number of helper methods that make it easy to access sorted or filtered rows from the data table, such as the following:


 - rowsFilteredUsingPredicate:
 - rowsFilteredUsingPredicate:sortedByField:ascending:
 - rowsFilteredUsingPredicate:localizedCaseInsensitivelySortedByField:ascending:
 - rowsSortedByField:ascending:
 - rowsLocalizedCaseInsensitivelySortedByField:ascending:
 - rowsFinderStyleSortedByField:ascending:

func rowsFilteredUsingPredicate() -> [DADataTableRow]
func rowsFilteredUsingPredicate(,sortedByField:ascending:) -> [DADataTableRow]
func rowsFilteredUsingPredicate(,localizedCaseInsensitivelySortedByField:ascending:) -> [DADataTableRow]
func rowsSortedByField(,ascending:) -> [DADataTableRow]
func rowsLocalizedCaseInsensitivelySortedByField(,ascending:) -> [DADataTableRow]
func rowsFinderStyleSortedByField(,ascending:) -> [DADataTableRow]

NSArray rowsFilteredUsingPredicate()
NSArray rowsFilteredUsingPredicate() sortedByField() ascending()
NSArray rowsFilteredUsingPredicate()localizedCaseInsensitivelySortedByField()ascending()
NSArray rowsSortedByField()ascending()
NSArray rowsLocalizedCaseInsensitivelySortedByField()ascending()
NSArray rowsFinderStyleSortedByField()ascending()

```oxygene

method rowsFilteredUsingPredicate(): NSArray method rowsFilteredUsingPredicate() sortedByField() ascending(): NSArray method rowsFilteredUsingPredicate()localizedCaseInsensitivelySortedByField()ascending(): NSArray method rowsSortedByField()ascending(): NSArray method rowsLocalizedCaseInsensitivelySortedByField()ascending(): NSArray method rowsFinderStyleSortedByField()ascending(): NSArray ```    

All of these methods return NSArrays of rows, similar to the rows property itself, with the difference that the returned array is sorted, filtered, or both. These methods make it really easy to work with a local subset of the data without running an extra request against the server.

A case can be made for both approaches to data access: Your application can download an entire table (or a subset of a table), and then work locally by applying additional filtering using the methods above; or you could choose to request the data you need directly from the server each time, filtered on the server via DA SQL.

Which approach to choose largely depends on how you are planning to work with the data, and the size of your dataset. If your original data table has a manageable size, it might make sense to fetch it once, and then work with the data locally. One of the applications we have developed in-house, a bug tracker called, fittingly, Bugs, uses this approach, keeping the few thousand open issues it knows about on the device, and just using rowsFilteredUsingPredicate: to filter for local display.

If on the other hand you have a huge database containing millions of records, and hundreds of (if not more) megabytes of data, it will be impractical to download the entire database to the client; instead, you will design your application to only download the data it needs, when it needs it, using beginGetDataTable:withSQL: or a similar method.

This will be covered in more detail in the Advanced Data Access Scenarios topic.

Note that calling these filtering methods does not affect the actual content of the DADataTable itself. You could make three calls to different methods of these and obtain three different and independent arrays. The arrays would be sorted differently and possibly contain different subsets of the same rows. The individual row objects within those arrays would be the same, so that each row in the actual data table only exists once, and any changes made to a row would immediately reflect everywhere that row is accessible.

Each of these row objects will be of type DADataTableRow, which we will take a look at next:

DADataTableRow

DADataTableRow is a fairly simple class, and basically just provides a range of values for the fields in a given row, accessible by name. DADataTableRow has been modeled after the common NSDictionary class and is designed to be compliant with “Key Value Coding”, a common Cocoa design pattern.

This means, basically, that it exposes two standard methods called valueForKey: and setValue:forKey: that can be used to read and write values of the different fields contained in a row. These methods are defined as:


 - (id)valueForKey:(id)key;
 - (void)setValue:(id)value forKey:(id)key;

public func valueForKey(key: AnyObject) -> AnyObject
public func setValue(value: AnyObject, forKey key: AnyObject)

public id valueForKey(id key);
public void setValue(id value) forKey(id key);

method valueForKey(key: id): id;
method setValue(value: id) forKey(key: id);

Not only will this pattern be familiar to most Cocoa developers who have used NSDictionary or other KVC-compliant classes in the past; this pattern also integrates deeply with core framework technologies such as Cocoa Binding (currently available on the Mac only, not on iOS), key paths, predicates and so on. You can think of valueForKey: and setValue:forKey: as a standard way for Cocoa objects to make named values available, and DADataTableRow can participate any place this pattern is used. The Working With Data article will go into a lot of these topics and cover predicates and key paths in much more detail.

Of course DADataTableRow also supports access to fields using the new dictionary subscript syntax introduced by Apple in the LLVM Compiler 4.0: myRow[fieldname].

Making Changes

Another important aspect of multi-tier client data access is the ability to make changes to the local data, keep track of these changes, and send them back to the server at a later time. DADataTableRow takes care of this aspect as well. When your code makes changes to a row by calling setValue:forKey:, DADataTableRow will keep a backup of the original row, so it can keep track of what has been changed.

After one or more changes have been made, valueForKey: will return the new, changed values for the fields, as you would expect. But DADataTableRow also exposes a third method called originalValueForKey:, which allows you to access the old value of the field as it was originally obtained from the server. DADataTableRow also exposes a Boolean property called changed that you can use to determine if any changes have been made to the row or not.

Finally, DADataTableRow provides two methods called cancel and cancelChangeForKey: that let you undo all the changes made to a row, or just the change made to one specific field. This essentially allows you to always restore a row to its original state and discard any changes your client application might have made in it.

DADataTable also provides a method called cancelAllChanges that lets you discard all the changes made to any row in the entire table at once.

Applying Changes

DADataTableRow's keeping track of any changes made by your application is also what enables you to eventually send those changes back to the server, essentially “committing” them to the main database. This brings us back to the remote data adapter, which provides the methods applyChangesForTable: and applyChangesForTables: for this, letting you send back changes for either a single table or a list of tables. Of course, asynchronous versions of these methods are also provided, their names starting with beginApplyChangesForTable*.

Summary

This chapter should have given you a good fundamental understanding of three of the core classes behind Data Abstract, as well as a peek at some of their helper classes, like DAAsyncRequest. We learned how to work with the remote data adapter to retrieve data and send changes back to the server, and we took a first look at how data tables and data table rows provide local representations of that data within your application.

You are now ready to fetch and work with data.