1

In the documentation for ObjectBox, they say to-many relations are resolved lazily on first access, and then cached in the source entity inside the ToMany object.

In my app, I want to use a ToMany relationship where one entity A will be linked with a very large number (potentially in the range of thousands) of another entity B. I don't want to load all B at once, because that might slow down the app.

To simulate this, I created a small "banking" Flutter application, with 2 entities Account and Transaction. Here, one account may have a relationship with thousands of transactions.

main.dart is where I initialize ObjectBox. I'm enabling some debug flags because I want to see in the logs when read operations are performed:

// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final store = Store(
    getObjectBoxModel(),
    directory: 'memory:obx',
    debugFlags: DebugFlags.logTransactionsRead,
  );
  runApp(MainApp(store: store));
}

My entities Account and Transaction are very simple, with only the ID and the relationship.

@Entity()
class Account {
  @Id()
  int id;

  @Backlink('account')
  final transactions = ToMany<Transaction>();

  Account({this.id = 0});
}

@Entity()
class Transaction {
  @Id()
  int id;

  final account = ToOne<Account>();

  Transaction({this.id = 0});
}

Finally, my App. It consists of a column of 3 buttons, one for clearing the database, one for creating data, one one for reading them.

// App.dart
class MainApp extends StatelessWidget {
  final Store store;
  const MainApp({required this.store, super.key});

  Box<Account> get accountBox => store.box<Account>();
  Box<Transaction> get transactionBox => store.box<Transaction>();

  _clear() {
    accountBox.removeAll();
    transactionBox.removeAll();
    print('Cleared all data');
  }

  _create() {
    final account = Account();
    account.transactions.add(Transaction());
    account.transactions.add(Transaction());
    account.transactions.add(Transaction());

    accountBox.put(account);
  }

  _readAccounts() async {
    final accounts = accountBox.getAll();

    for (var account in accounts) {
      print('Account ID: ${account.id}');

      await Future.delayed(const Duration(seconds: 1));
      for (var transaction in account.transactions) {
        print('Transaction ID: ${transaction.id}');
        await Future.delayed(const Duration(seconds: 1));
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              ElevatedButton(onPressed: _clear, child: Text('Clear')),
              SizedBox(height: 20),
              ElevatedButton(onPressed: _create, child: Text('Create')),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _readAccounts,
                child: Text('Read Accounts'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

The _create() function creates 1 account and 3 transactions.

When I execute _readAccounts(), I'm expecting an initial read operation on the Account entity, followed by 3 additional read operation on Transaction every 1 second. In practice, this is not what happens. Here are my logs:

1.  I/Box     (10542): TX #27 (read)
2.  I/Box     (10542): TX #27 to be destroyed on owner thread (last committed: TX #25)...
3.  I/Box     (10542): TX #27 destroyed
4.  I/Box     (10542): TX #28 (read)
5.  I/Box     (10542): TX #28 to be destroyed on owner thread (last committed: TX #25)...
6.  I/Box     (10542): TX #28 destroyed
7.  I/flutter (10542): Account ID: 1
8.  I/Box     (10542): TX #29 (read)
9.  I/Box     (10542): TX #29 to be destroyed on owner thread (last committed: TX #25)...
10. I/Box     (10542): TX #29 destroyed
11. I/Box     (10542): TX #30 (read)
12. I/Box     (10542): TX #30 to be destroyed on owner thread (last committed: TX #25)...
13. I/Box     (10542): TX #30 destroyed
14. I/flutter (10542): Transaction ID: 1
15. I/flutter (10542): Transaction ID: 2
16. I/flutter (10542): Transaction ID: 3

What happens:

  • Transactions 27 & 28 (lines 1-6) correspond to the accountBox.getAll() operation. I'm not sure why it needs 2 transactions to do that.
  • Then, as soon as I enter the second for loop for (var transaction in account.transactions) { ... }, transactions 29 & 30 (lines 8-13) are executed.
  • Finally, lines 14-16 are printed one by one at a 1 second interval.

So yes, the relations are resolved lazily, because objectbox does not read anything until I access account.transactions. But when I do, it loads all relations at once.

Is there any way to resolve them one by one, when/if I need them?

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.