In other articles, we’ve discussed how to load object relations using the auto-load and the one-step approach. Both of these approaches return a complex hierarchy of data where the parent object includes child entities at the time when it is retrieved from the server.
Quite often these approaches are less than desirable for the reason that they may result in large payloads of data which may slow down data transfer and result in a suboptimal user experience. Additionally, an entity stored in the Backendless Database may have a lot of related properties and it may be desirable to fetch a specific relation after the parent object has been loaded by the client.
To enable loading of relations after the client has retrieved the parent object, Backendless provides API to load related objects with a two-step approach. The “two-step” term describes the fact that the parent entity is loaded with the first call and the second call/step is to fetch specific relations. The sample code below demonstrates retrieval of the “locations” property for the Restaurant objects. This example is based on the Restaurant-to-Go application schema.
Please follow the steps below to configure your Backendless backend with the schema and data for the application:
The restaurant table the sample works with looks as shown below:
Notice the “Cantina Laredo” restaurant has related objects in the “locations” property. The code below retrieves a collection of the Restaurant objects (first step). Then the code makes a request to load the specific relation for each restaurant object:
private static void loadRelationsAsync() { Log.i(TAG, "============ Loading relations with the ASYNC API ============"); final LoadRelationsQueryBuilder<Location> relationsQueryBuilder = LoadRelationsQueryBuilder.of(Location.class); relationsQueryBuilder.setRelationName("locations"); final AsyncCallback<List<Restaurant>> callback = new AsyncCallback<List<Restaurant>>() { @Override public void handleResponse(List<Restaurant> restaurants) { Log.i(TAG, "Loaded " + restaurants.size() + " restaurant objects"); for (final Restaurant restaurant : restaurants) { Backendless.Data.of(Restaurant.class).loadRelations(restaurant.getObjectId(), relationsQueryBuilder, new AsyncCallback<List<Location>>() { @Override public void handleResponse(List<Location> locations) { Log.i(TAG, "Restaurant name = " + restaurant.getName()); printLocations(locations); } @Override public void handleFault(BackendlessFault fault) { Log.e(TAG, fault.getMessage()); } }); } } @Override public void handleFault(BackendlessFault fault) { Log.e(TAG, fault.getMessage()); } }; Backendless.Data.of(Restaurant.class).getObjectCount(new AsyncCallback<Integer>() { @Override public void handleResponse(Integer count) { Log.i(TAG, "Total restaurants in the Backendless storage - " + count); Backendless.Data.of(Restaurant.class).find(callback); } @Override public void handleFault(BackendlessFault fault) { Log.e(TAG, fault.getMessage()); } }); }
The printLocations method used to print out the details of the related locations is:
private static void printLocations(List<Location> locations) { if (locations == null) { Log.i(TAG, "Restaurant locations have not been loaded"); } else if (locations.size() == 0) { Log.i(TAG, "There are no related locations"); } else { for (Location location : locations) { Log.i(TAG, "Location: Street address - " + location.getStreetAddress() + ", City - " + location.getCity()); } } }
private fun loadRelationsAsync() { Log.i(TAG, "============ Loading relations with the ASYNC API ============") val relationsQueryBuilder = LoadRelationsQueryBuilder.of(Location::class.java) relationsQueryBuilder.setRelationName("locations") val callback = object : AsyncCallback<List<Restaurant>> { override fun handleResponse(restaurants: List<Restaurant>) { Log.i(TAG, "Loaded ${restaurants.size} restaurant objects") for (restaurant in restaurants) { Backendless.Data.of(Restaurant::class.java) .loadRelations(restaurant.objectId, relationsQueryBuilder, object : AsyncCallback<List<Restaurant>> { override fun handleResponse(locations: List<Location>) { Log.i(TAG, "Restaurant name = ${restaurant.name}") printLocations(locations) } override fun handleFault(fault: BackendlessFault) { Log.e(TAG, fault.message) } }) } } override fun handleFault(fault: BackendlessFault) { Log.e(TAG, fault.message) } } Backendless.Data.of(Restaurant::class.java).getObjectCount(object : AsyncCallback<Int> { override fun handleResponse(count: Int?) { Log.i(TAG, "Total restaurants in the Backendless storage - $count") Backendless.Data.of(Restaurant::class.java).find(callback) } override fun handleFault(fault: BackendlessFault) { Log.e(TAG, fault.message) } }) }
The printLocations method used to print out the details of the related locations is:
private fun printLocations(locations: List<Location>?) { when { locations == null -> Log.i(TAG, "Restaurant locations have not been loaded") locations.size == 0 -> Log.i(TAG, "There are no related locations") else -> for (location in locations) { Log.i(TAG, "Location: Street address - ${location.streetAddress}, City - ${location.city}") } } }
DataStoreFactory *dataStore = [Backendless.shared.data of:[Restaurant class]]; LoadRelationsQueryBuilder *relationsQueryBuilder = [[LoadRelationsQueryBuilder alloc] initWithEntityClass:[Location class]]; [relationsQueryBuilder setRelationNameWithRelationName:@"locations"]; NSLog(@"============ Loading relations ============"); [dataStore findWithResponseHandler:^(NSArray *restaurants) { NSLog(@"Loaded %lu restaurant objects", (unsigned long)restaurants.count); [dataStore getObjectCountWithResponseHandler:^(NSInteger totalRestaurants) { NSLog(@"Total restaurants in the Backendless storage - %li",(long)totalRestaurants); for (Restaurant *restaurant in restaurants) { [dataStore loadRelationsWithObjectId:restaurant.objectId queryBuilder:relationsQueryBuilder responseHandler:^(NSArray *locations) { NSLog(@"\nRestaurant name = %@", restaurant.name); [self printLocations:locations]; } errorHandler:^(Fault *fault) { NSLog(@"Error: %@", fault.message); }]; } } errorHandler:^(Fault *fault) { NSLog(@"Error: %@", fault.message); }]; } errorHandler:^(Fault *fault) { NSLog(@"Error: %@", fault.message); }];
The printLocations method used to print out the details of the related locations is:
- (void)printLocations:(NSArray<Location *> *)locations { if (!locations) { NSLog(@"Restaurant locations have not been loaded"); } else { if (locations.count == 0) { NSLog(@"There are no related locations"); } else { for (Location *location in locations) { NSLog(@"Location: Street address - %@, City - %@", location.street, location.city); } } } }
let dataStore = Backendless.shared.data.of(Restaurant.self) let relationsQueryBuilder = LoadRelationsQueryBuilder(entityClass: Location.self) relationsQueryBuilder.setRelationName(relationName: "locations") print("============ Loading relations ============") dataStore.find(responseHandler: { restaurants in print("Loaded \(restaurants.count) restaurant objects") dataStore.getObjectCount(responseHandler: { totalRestaurants in print("Total restaurants in the Backendless storage - \(totalRestaurants)") if let restaurants = restaurants as? [Restaurant] { for restaurant in restaurants { if let restaurantId = restaurant.objectId { dataStore.loadRelations(objectId: restaurantId, queryBuilder: relationsQueryBuilder, responseHandler: { locations in print("\nRestaurant name = \(restaurant.name ?? "")") if let locations = locations as? [Location] { self.printLocations(locations) } }, errorHandler: { fault in print("Error: \(fault.message ?? "")") }) } } } }, errorHandler: { fault in print("Error: \(fault.message ?? "")") }) }, errorHandler: { fault in print("Error: \(fault.message ?? "")") })
The printLocations method used to print out the details of the related locations is:
func printLocations(_ locations: [Location]?) { if locations == nil { print("Restaurant locations have not been loaded") } else if let locations = locations { if locations.count == 0 { print("There are no related locations") } else { for location in locations { if let city = location.city, let street = location.street { print("Location: Street address - \(street), City - \(city)") } } } } }
const Backendless = require('backendless') /* Or use `import Backendless from 'backendless'` for client side. If you don't use npm or yarn to install modules, you can add the following line <script src="//api.backendless.com/sdk/js/latest/backendless.min.js"></script> to your index.html file and use the global Backendless variable. */ Backendless.initApp('YOUR_APP_ID', 'YOUR_JS_API_KEY') const loadRestaurants = () => { return Backendless.Data.of('Restaurant').find() } const onRestaurantsLoaded = restaurants => { console.log(`Loaded ${ restaurants.length } restaurant objects`) const requests = [] restaurants.forEach(restaurant => { requests.push(loadRestaurantLocations(restaurant.objectId)) }) return Promise.all(requests) } const loadRestaurantLocations = restaurantId => { const loadRelationsQueryBuilder = Backendless.LoadRelationsQueryBuilder.create().setRelationName('locations') return Backendless.Data.of('Restaurant').loadRelations(restaurantId, loadRelationsQueryBuilder) } const onRestaurantsLocationsLoaded = restaurantsLocations => { restaurantsLocations.forEach(printLocations) } const printLocations = locations => { if (!locations) { return console.log('Restaurant locations have not been loaded') } if (!locations.length) { return console.log('There are no related locations') } locations.forEach(location => { console.log(`Location: Street address - ${ location.streetAddress }, City - ${ location.city }`) }) } const onError = error => { console.error('Server reported an error: ', error.message) console.error('error code: ', error.code) console.error('http status: ', error.status) } Promise.resolve() .then(loadRestaurants) .then(onRestaurantsLoaded) .then(onRestaurantsLocationsLoaded) .catch(onError)
static void _loadRelationsAsync() { print("============ Loading relations with the ASYNC API ============"); LoadRelationsQueryBuilder relationsQueryBuilder = LoadRelationsQueryBuilder.ofMap(); relationsQueryBuilder.relationName = "locations"; Backendless.Data.of("Restaurant").getObjectCount().then((count) { print("Total restaurants in the Backendless storage - $count"); Backendless.Data.of("Restaurant").find().then((restaurants) { print("Loaded ${restaurants.length} restaurant objects"); restaurants.forEach((restaurant) { Backendless.Data.of("Restaurant").loadRelations(restaurant['objectId'], relationsQueryBuilder).then((locations) { print("Restaurant name = ${restaurant['name']}"); _printLocations(locations.cast<Map>()); }); }); }); }); }
The printLocations method used to print out the details of the related locations is:
static void _printLocations(List<Map> locations) { if (locations == null) { print("Restaurant locations have not been loaded"); } else if (locations.isEmpty) { print("There are no related locations"); } else { for (Map location in locations) { print("Location: Street address - ${location['streetAddress']}, City - ${location['city']}"); } } }
============ Loading relations with the SYNC API ============ Loaded 4 restaurant objects Total restaurants in the Backendless storage - 4 Restaurant name = McDonald's There are no related locations Restaurant name = Buca Di Bepo There are no related locations Restaurant name = Cantina Laredo Location: Street address - 123 Main St., City - Frisco Restaurant name = Endless Sweets There are no related locations ============ Loading relations with the ASYNC API ============ Loaded 4 restaurant objects Total restaurants in the Backendless storage - 4 Restaurant name = McDonald's There are no related locations Restaurant name = Endless Sweets There are no related locations Restaurant name = Buca Di Bepo There are no related locations Restaurant name = Cantina Laredo Location: Street address - 123 Main St., City - Frisco
See the documentation for more information about loading relations with the two-step approach.
Enjoy!