Blog

How to Load Related Data Objects – The ‘One-Step/Dynamic’ Approach

by on April 1, 2020

In another article, we described how to load complex data objects from the persistent storage using the “auto-load” technique. Using that approach, a developer can statically identify specific (child) properties that should be returned along with the parent object(s) when a client app sends a request to load them.

That approach works well for object relations which must be unconditionally returned with the parent object. However, there are many scenarios when the client app must control which relations should be preloaded and returned along with the parent.  The API described below allows you to accomplish that.

Consider the following example which uses the client-side code generated by Backendless based on the database schema. In that schema, the Restaurant table has the “locations” column which represents a one-to-many relationship between the Restaurant and Locations table. This means that a restaurant may have several related locations as shown in the screenshots below.

The restaurant objects:

Restaurant Table Related Location Objects

The related location object for the “Cantina Laredo” restaurant:

Restaurant Table Related Locations

    private static void loadRelationsAsync() {
       Log.i(TAG, "============ Loading relations with the ASYNC API ============");
    
       final AsyncCallback<List> callback = new AsyncCallback<List>() {
           @Override
           public void handleResponse(List restaurants) {
               Log.i(TAG, "Loaded " + restaurants.size() + " restaurant objects");
    
               for (Restaurant restaurant : restaurants) {
                   Log.i(TAG, "Restaurant name = " + restaurant.getName());
                   printLocations(restaurant.getLocations());
               }
           }
    
           @Override
           public void handleFault(BackendlessFault fault) {
               Log.e(TAG, fault.getMessage());
           }
       };
    
       final DataQueryBuilder dataQuery = DataQueryBuilder.create();
       dataQuery.setRelated("locations");
    
       Backendless.Data.of(Restaurant.class).getObjectCount(new AsyncCallback() {
           @Override
           public void handleResponse(Integer count) {
               Log.i(TAG, "Total restaurants in the Backendless storage - " + count);
               Backendless.Data.of(Restaurant.class).find(dataQuery, callback);
           }
    
           @Override
           public void handleFault(BackendlessFault fault) {
               Log.e(TAG, fault.getMessage());
           }
       });
    }

    private fun loadRelationsAsync() {
       Log.i(TAG, "============ Loading relations with the ASYNC API ============")
    
       val callback = object : AsyncCallback<List> {
           override fun handleResponse(restaurants: List) {
               Log.i(TAG, "Loaded ${restaurants.size} restaurant objects")
    
               for (restaurant in restaurants) {
                   Log.i(TAG, "Restaurant name = ${restaurant.name}")
                   printLocations(restaurant.locations)
               }
           }
    
           override fun handleFault(fault: BackendlessFault) {
               Log.e(TAG, fault.message)
           }
       }
    
       val dataQuery = DataQueryBuilder.create()
       dataQuery.setRelated("locations")
    
       Backendless.Data.of(Restaurant::class.java).getObjectCount(object : AsyncCallback {
           override fun handleResponse(count: Int?) {
               Log.i(TAG, "Total restaurants in the Backendless storage - $count")
               Backendless.Data.of(Restaurant::class.java).find(dataQuery, callback)
           }
    
           override fun handleFault(fault: BackendlessFault) {
               Log.e(TAG, fault.message)
           }
       })
    }

    NSLog(@"============ Loading relations ============");
        
    DataQueryBuilder *queryBuilder = [DataQueryBuilder new];
    [queryBuilder addRelatedWithRelated:@"locations"];
        
    DataStoreFactory *dataStore = [Backendless.shared.data of:[Restaurant class]];
        
    [dataStore findWithQueryBuilder:queryBuilder responseHandler:^(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) {
                NSLog(@"\nRestaurant name = %@", restaurant.name);
                [self printLocations:restaurant.locations];
            }
        } errorHandler:^(Fault *fault) {
            NSLog(@"Error: %@", fault.message);
        }];
    
    } errorHandler:^(Fault *fault) {
        NSLog(@"Error: %@", fault.message);
    }];

    print("============ Loading relations ============")
            
    let queryBuilder = DataQueryBuilder()
    queryBuilder.addRelated(related: "locations")
            
    let dataStore = Backendless.shared.data.of(Restaurant.self)
            
    dataStore.find(queryBuilder: queryBuilder, responseHandler: { restaurants in
        print("Loaded \(restaurants.count) restaurantObjects")
                
        dataStore.getObjectCount(responseHandler: { totalRestaurants in
            print("Total restaurants in the Backendless storage - \(totalRestaurants)")
                    
            if let restaurants = restaurants as? [Restaurant] {
                for restaurant in restaurants {
                    print("\nRestaurant name = \(restaurant.name ?? "")")
                    self.printLocations(restaurant.locations)
                }
            }
                    
        }, errorHandler: { fault in
            print("Error: \(fault.message ?? "")")
        })
                
    }, errorHandler: { fault in
        print("Error: \(fault.message ?? "")")
    })

    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 = () => {
      const dataQuery = Backendless.DataQueryBuilder.create().setRelated('locations')
      
      return Backendless.Data.of('Restaurant').find(dataQuery)
    }
    
    const onSuccess = restaurants => {
      restaurants.forEach(restaurant => {
        printLocations(restaurant.locations)
      })
    }
    
    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(onSuccess)
      .catch(onError)
    

    static void _loadRelationsAsync() {
       print("============ Loading relations with the ASYNC API ============");
    
       DataQueryBuilder dataQuery = DataQueryBuilder()
         ..related = ["locations"];
    
       Backendless.Data.of("Restaurant").getObjectCount().then((count) {
         print("Total restaurants in the Backendless storage - $count");
    
         Backendless.Data.of("Restaurant").find(dataQuery).then((restaurants) {
           print("Loaded ${restaurants.length} restaurant objects");
           restaurants.forEach((restaurant) {
             print("Restaurant name = ${restaurant['name']}");
             _printLocations(restaurant['locations'].cast<Map>());
           });
         });
       });
     }
    

     

    Notice how the related locations column in set in the DataQueryBuilder class. Specifying the column name requests that the data objects referenced by the column must be initialized and returned with the every parent Restaurant object. The printLocations method displays the loaded Locations objects:

      private static void printLocations(List 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 printLocations(locations: List?) {
         when {
             locations == null -> Log.i(TAG, "Restaurant locations have not been loaded")
             locations.isEmpty() -> Log.i(TAG, "There are no related locations")
             else -> for (location in locations) {
                 Log.i(TAG, "Location: Street address - ${location.streetAddress}, City - ${location.city}")
             }
         }
      }

      - (void)printLocations:(NSArray *)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);
                  }
              }
          }
      }

      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 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 }`)
        })
      }
      

      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 code above produces the following output. Notice that the “Cantina Laredo” restaurant is fetched with the related location:

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

      See the documentation for more information on loading related objects with the “one-step” approach.

      Enjoy!

      Leave a Reply