Blog

How to Load Related Data Objects – The ‘Semi-Lazy’ Approach

by on April 1, 2020

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:

  1. Download the ZIP file which contains the definition for all tables from https://backendless.com/documentation/samples/restaurant-app-schema.zip
  2. Login to Backendless Console, select an app (it is recommended to create a new app for this) and click the Manage icon.
  3. Select the Import menu item.
  4. Click the single Zip file link located in the description text and browse to the ZIP file from step 1 above.

The restaurant table the sample works with looks as shown below:

Restaurant Table With Data

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']}");
        }
      }
    }


    The output produced by these methods:

    ============ 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!

    Leave a Reply