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?