Remote Data Adapter and Data Tables (Xcode)

From RemObjects Wiki
Jump to: navigation, search

This is an Article about Data Abstract for Xcode

(This topic is taken from Chapter 3 of Developing Database Applications for Mac and iOS)



Data Abstract is a sophisticated library with dozens of classes that work together to provide a state-of-the-art multi-tier client solution for iOS and Mac OS X, written in 100% native Objective-C code that is shared (with few #ifdefs) between the two sister platforms. There are a lot of things that go on under the hood to enable the seamless data access with all its features, such as keeping track of local changes, merging conflicts, robustly communicating with the server, and so on.

Luckily, Data Abstract “abstracts” most of this functionality away from the developer (although that is not where it got its name), letting you concentrate on what is important – creating a state-of-the-art iOS or Mac application that presents or edits data according to your needs – without having to worry about the internals of what is happening behind the scenes too much (although you can take interest and tweak DA down to the lowest levels, if the need should arise).

In the previous chapter, we saw three of Data Abstract’s classes (two we used directly, while a third one was referenced indirectly). These three classes are – with a couple of additions that we will look at in later chapters – the core classes that you will be interacting with in your day-to-day work with Data Abstract: DARemoteDataAdapter, DADataTable and DADataTableRow.

Per convention, type names in Cocoa frameworks usually start with two-letter prefixes such as NS* or UI*, to avoid name overlap. To be consistent with this system, classes in Data Abstract use the DA* prefix throughout the framework. In addition, you might also encounter classes with RO* prefixes, for example in call stacks during debugging. These classes come from “RemObjects SDK”, the underlying remoting framework that Data Abstract uses to communicate with the server. Developing a regular database application, you don’t usually have to care about how the communication is done, because Data Abstract encapsulates all that for you. But in some cases it will be helpful to know what is going on under the hood, so we will take a closer look at RemObjects SDK in Chapter 9 and Appendix A. When building iOS applications, RemObjects SDK is contained within libDataAbstract.a, the static library for Data Abstract.

The DARemoteDataAdapter (frequently referred to as the “RDA”, in short) 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 the book 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. In Chapter 12: 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 we will have a closer look at in Chapter 9 when we look at the underlying 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 Chapter 9.

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 expose multiple data services under different names. You can configure the service name by calling, for example:

[[rda dataService] setServiceName:@“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 FullfillmentService 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:

[[rda dataService] setServiceName:@“DataService.Fullfillment”];

You will learn more about configuring Relativity Server and its domains and schemas in Chapter 10. 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];

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*. Blocks are a fairly new concept in Objective-C, introduced 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

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, linked from Appendix E. {{Todo|link needed}

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 to your user, who expects a UI that is always responsive and reacts to their touches immediately.

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 after 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 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, 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:

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

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 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 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];

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 Objective-C. The common rule is that delegate objects are not retained, to avoid retain/release cycles. DAAsyncRequest behaves the same, in principle, with the slight variation that it will automatically retain 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. 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 – the most common reason for that being that you are writing code that still targets iOS version 3.x or older or is sharing code with a Mac application that targets Leopard – you can use callbacks to your delegate to be informed when your data requests have completed. 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:

- (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 check [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);
}

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 we will look at in more detail in Chapter 7: Advanced Data Access Scenarios.

A third set of overloads we will not cover in this book, as it exists largely for legacy reasons, allowing 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. 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 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 Objectve-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 sample domain (the same we used in Chapter 2) and inspect the table:

DADataTable *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);

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 run time (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]];

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:, 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 Chapter 6: Working With Data. 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:

All of these 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.

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 (if not more) of megabytes of data, it will be impractical to download the entire database to the device; 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 Chapter 7: Advanced Data Access Scenarios. Note that calling these 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 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;

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. Chapter 6: Working With Data will go into a lot of these topics and cover predicates and key paths in much more detail.

As of the Spring 2012 release, DADataTableRow also supports access to fields using the new dictionary subscript syntax intgroduced by Apple in the LLVM COmpiler 4.0: myRow[@fieldname].

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 BOOL 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 to 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*.

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. The next three chapters will be a little bit more hands-on, as we look at how to display and present data in your iOS application (the most iOS-specific chapter in this book), how to go about editing data and finally how to work more closely with the data contained in our data tables.