Skip to content

Commit 183aade

Browse files
Merge pull request #376 from objectbox/store-attach
Add Store.attach.
2 parents c548761 + 3d2f25e commit 183aade

File tree

4 files changed

+281
-80
lines changed

4 files changed

+281
-80
lines changed

objectbox/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
* Add an option to change code-generator's `output_dir` in `pubspec.yaml`. #341
44
* Support ObjectBox Admin for Android apps to browse the database, see our [docs](https://docs.objectbox.io/data-browser) to get started. #148
5+
* Add `Store.attach` to attach to a Store opened in a directory. This is an improved replacement for
6+
`Store.fromReference` to share a Store across isolates. It is no longer required to pass a
7+
Store reference and the underlying Store remains open until the last instance is closed. #376
58
* Update: [objectbox-c 0.15.2](https://github.com/objectbox/objectbox-c/releases/tag/v0.15.0).
69
* Update: [objectbox-android 3.1.2](https://github.com/objectbox/objectbox-java/releases/tag/V3.1.0).
710
* Update: [objectbox-swift 1.7.0](https://github.com/objectbox/objectbox-swift/releases/tag/v1.7.0).

objectbox/lib/src/native/store.dart

+116-33
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:typed_data';
99

1010
import 'package:ffi/ffi.dart';
1111
import 'package:meta/meta.dart';
12+
import 'package:objectbox/src/native/version.dart';
1213
import 'package:path/path.dart' as path;
1314

1415
import '../common.dart';
@@ -27,6 +28,13 @@ part 'observable.dart';
2728
/// Represents an ObjectBox database and works together with [Box] to allow
2829
/// getting and putting.
2930
class Store {
31+
/// Path of the default directory, currently 'objectbox'.
32+
static const String defaultDirectoryPath = 'objectbox';
33+
34+
/// Enables a couple of debug logs.
35+
/// This meant for tests only; do not enable for releases!
36+
static bool debugLogs = false;
37+
3038
late final Pointer<OBX_store> _cStore;
3139
HashMap<int, Type>? _entityTypeById;
3240
final _boxes = HashMap<Type, Box>();
@@ -36,8 +44,8 @@ class Store {
3644
final _reader = ReaderWithCBuffer();
3745
Transaction? _tx;
3846

39-
/// absolute path to the database directory
40-
final String _dbDir;
47+
/// Absolute path to the database directory, used for open check.
48+
final String _absoluteDirectoryPath;
4149

4250
late final ByteData _reference;
4351

@@ -51,8 +59,12 @@ class Store {
5159
/// Default value for string query conditions [caseSensitive] argument.
5260
final bool _queriesCaseSensitiveDefault;
5361

62+
static String _safeDirectoryPath(String? path) =>
63+
(path == null || path.isEmpty) ? defaultDirectoryPath : path;
64+
5465
/// Creates a BoxStore using the model definition from your
55-
/// `objectbox.g.dart` file.
66+
/// `objectbox.g.dart` file in the given [directory] path
67+
/// (or if null the [defaultDirectoryPath]).
5668
///
5769
/// For example in a Flutter app:
5870
/// ```dart
@@ -76,10 +88,8 @@ class Store {
7688
String? macosApplicationGroup})
7789
: _weak = false,
7890
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
79-
_dbDir = path.context.canonicalize(
80-
(directory == null || directory.isEmpty)
81-
? 'objectbox'
82-
: directory) {
91+
_absoluteDirectoryPath =
92+
path.context.canonicalize(_safeDirectoryPath(directory)) {
8393
try {
8494
if (Platform.isMacOS && macosApplicationGroup != null) {
8595
if (!macosApplicationGroup.endsWith('/')) {
@@ -96,12 +106,7 @@ class Store {
96106
malloc.free(cStr);
97107
}
98108
}
99-
if (_openStoreDirectories.contains(_dbDir)) {
100-
throw UnsupportedError(
101-
'Cannot create multiple Store instances for the same directory. '
102-
'Please use a single Store or close() the previous instance before '
103-
'opening another one.');
104-
}
109+
_checkStoreDirectoryNotOpen();
105110
final model = Model(_defs.model);
106111

107112
final opt = C.opt();
@@ -130,25 +135,14 @@ class Store {
130135
C.opt_free(opt);
131136
rethrow;
132137
}
138+
if (debugLogs) {
139+
print('Opening store (C lib V${libraryVersion()})... path=$directory'
140+
' isOpen=${isOpen(directory)}');
141+
}
142+
133143
_cStore = C.store_open(opt);
134144

135-
try {
136-
checkObxPtr(_cStore, 'failed to create store');
137-
} on ObjectBoxException catch (e) {
138-
// Recognize common problems when trying to open/create a database
139-
// 10199 = OBX_ERROR_STORAGE_GENERAL
140-
// 13 = permissions denied, 30 = read-only filesystem
141-
if (e.message.contains(OBX_ERROR_STORAGE_GENERAL.toString()) &&
142-
e.message.contains('Dir does not exist') &&
143-
(e.message.endsWith(' (13)') || e.message.endsWith(' (30)'))) {
144-
throw ObjectBoxException(e.message +
145-
' - this usually indicates a problem with permissions; '
146-
"if you're using Flutter you may need to use "
147-
'getApplicationDocumentsDirectory() from the path_provider '
148-
'package, see example/README.md');
149-
}
150-
rethrow;
151-
}
145+
_checkStorePointer(_cStore);
152146

153147
// Always create _reference, so it can be non-nullable.
154148
// Ensure we only try to access the store created in the same process.
@@ -157,7 +151,7 @@ class Store {
157151
_reference.setUint64(0 * _int64Size, pid);
158152
_reference.setUint64(1 * _int64Size, _ptr.address);
159153

160-
_openStoreDirectories.add(_dbDir);
154+
_openStoreDirectories.add(_absoluteDirectoryPath);
161155
} catch (e) {
162156
_reader.clear();
163157
rethrow;
@@ -205,7 +199,7 @@ class Store {
205199
{bool queriesCaseSensitiveDefault = true})
206200
// must not close the same native store twice so [_weak]=true
207201
: _weak = true,
208-
_dbDir = '',
202+
_absoluteDirectoryPath = '',
209203
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault {
210204
// see [reference] for serialization order
211205
final readPid = _reference.getUint64(0 * _int64Size);
@@ -221,6 +215,95 @@ class Store {
221215
}
222216
}
223217

218+
/// Attach to a store opened in the [directoryPath]
219+
/// (or if null the [defaultDirectoryPath]).
220+
///
221+
/// Use this to access an open store from other isolates.
222+
/// This results in each isolate having access to the same underlying native
223+
/// store.
224+
///
225+
/// The returned store is a new instance (e.g. different pointer value) with
226+
/// its own lifetime and must also be closed (e.g. before an isolate exits).
227+
/// The actual underlying store is only closed when the last store instance
228+
/// is closed (e.g. when the app exits).
229+
Store.attach(this._defs, String? directoryPath,
230+
{bool queriesCaseSensitiveDefault = true})
231+
// _weak = false so store can be closed.
232+
: _weak = false,
233+
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
234+
_absoluteDirectoryPath =
235+
path.context.canonicalize(_safeDirectoryPath(directoryPath)) {
236+
try {
237+
// Do not allow attaching to a store that is already open in the current
238+
// isolate. While technically possible this is not the intended usage
239+
// and e.g. transactions would have to be carefully managed to not
240+
// overlap.
241+
_checkStoreDirectoryNotOpen();
242+
243+
final path = _safeDirectoryPath(directoryPath);
244+
final pathCStr = path.toNativeUtf8();
245+
try {
246+
if (debugLogs) {
247+
final isOpen = C.store_is_open(pathCStr.cast());
248+
print('Attaching to store... path=$path isOpen=$isOpen');
249+
}
250+
_cStore = C.store_attach(pathCStr.cast());
251+
} finally {
252+
malloc.free(pathCStr);
253+
}
254+
255+
checkObxPtr(_cStore,
256+
'could not attach to the store at given path - please ensure it was opened before');
257+
258+
// Not setting _reference as this is a replacement for obtaining a store
259+
// via reference.
260+
} catch (e) {
261+
_reader.clear();
262+
rethrow;
263+
}
264+
}
265+
266+
void _checkStoreDirectoryNotOpen() {
267+
if (_openStoreDirectories.contains(_absoluteDirectoryPath)) {
268+
throw UnsupportedError(
269+
'Cannot create multiple Store instances for the same directory in the same isolate. '
270+
'Please use a single Store, close() the previous instance before '
271+
'opening another one or attach to it in another isolate.');
272+
}
273+
}
274+
275+
void _checkStorePointer(Pointer cStore) {
276+
try {
277+
checkObxPtr(cStore, 'failed to create store');
278+
} on ObjectBoxException catch (e) {
279+
// Recognize common problems when trying to open/create a database
280+
// 10199 = OBX_ERROR_STORAGE_GENERAL
281+
// 13 = permissions denied, 30 = read-only filesystem
282+
if (e.message.contains(OBX_ERROR_STORAGE_GENERAL.toString()) &&
283+
e.message.contains('Dir does not exist') &&
284+
(e.message.endsWith(' (13)') || e.message.endsWith(' (30)'))) {
285+
throw ObjectBoxException(e.message +
286+
' - this usually indicates a problem with permissions; '
287+
"if you're using Flutter you may need to use "
288+
'getApplicationDocumentsDirectory() from the path_provider '
289+
'package, see example/README.md');
290+
}
291+
rethrow;
292+
}
293+
}
294+
295+
/// Returns if an open store (i.e. opened before and not yet closed) was found
296+
/// for the given [directoryPath] (or if null the [defaultDirectoryPath]).
297+
static bool isOpen(String? directoryPath) {
298+
final path = _safeDirectoryPath(directoryPath);
299+
final cStr = path.toNativeUtf8();
300+
try {
301+
return C.store_is_open(cStr.cast());
302+
} finally {
303+
malloc.free(cStr);
304+
}
305+
}
306+
224307
/// Returns a store reference you can use to create a new store instance with
225308
/// a single underlying native store. See [Store.fromReference] for more details.
226309
ByteData get reference => _reference;
@@ -243,7 +326,7 @@ class Store {
243326
_reader.clear();
244327

245328
if (!_weak) {
246-
_openStoreDirectories.remove(_dbDir);
329+
_openStoreDirectories.remove(_absoluteDirectoryPath);
247330
checkObx(C.store_close(_cStore));
248331
}
249332
}

objectbox/test/basics_test.dart

+82
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:ffi' as ffi;
22
import 'dart:io';
3+
import 'dart:isolate';
34

5+
import 'package:async/async.dart';
46
import 'package:objectbox/internal.dart';
57
import 'package:objectbox/src/native/bindings/bindings.dart';
68
import 'package:objectbox/src/native/bindings/helpers.dart';
@@ -63,6 +65,58 @@ void main() {
6365
env.closeAndDelete();
6466
});
6567

68+
test('store attach fails if same isolate', () {
69+
final env = TestEnv('basics');
70+
expect(
71+
() => Store.attach(getObjectBoxModel(), env.dir.path),
72+
throwsA(predicate((UnsupportedError e) =>
73+
e.message!.contains('Cannot create multiple Store instances'))));
74+
env.closeAndDelete();
75+
});
76+
77+
test('store attach remains open if main store closed', () async {
78+
final env = TestEnv('basics');
79+
final store1 = env.store;
80+
final receivePort = ReceivePort();
81+
final received = StreamQueue<dynamic>(receivePort);
82+
await Isolate.spawn(storeAttachIsolate,
83+
StoreAttachIsolateInit(receivePort.sendPort, env.dir.path));
84+
final commandPort = await received.next as SendPort;
85+
86+
// Check native instance pointer is different.
87+
final store2Address = await received.next as int;
88+
expect(InternalStoreAccess.ptr(store1).address, isNot(store2Address));
89+
90+
final id = store1.box<TestEntity>().put(TestEntity(tString: 'foo'));
91+
expect(id, 1);
92+
// Close original store to test store remains open until all refs closed.
93+
store1.close();
94+
expect(true, Store.isOpen('testdata-basics'));
95+
96+
// Read data with attached store.
97+
commandPort.send(id);
98+
final readtString = await received.next as String?;
99+
expect(readtString, isNotNull);
100+
expect(readtString, 'foo');
101+
102+
// Close attached store, should close store completely.
103+
commandPort.send(null);
104+
await received.next;
105+
expect(false, Store.isOpen('testdata-basics'));
106+
107+
// Dispose StreamQueue.
108+
await received.cancel();
109+
});
110+
111+
test('store is open', () {
112+
expect(false, Store.isOpen(''));
113+
expect(false, Store.isOpen('testdata-basics'));
114+
final env = TestEnv('basics');
115+
expect(true, Store.isOpen('testdata-basics'));
116+
env.closeAndDelete();
117+
expect(false, Store.isOpen('testdata-basics'));
118+
});
119+
66120
test('transactions', () {
67121
final env = TestEnv('basics');
68122
expect(TxMode.values.length, 2);
@@ -139,3 +193,31 @@ void main() {
139193
Directory('basics').deleteSync(recursive: true);
140194
});
141195
}
196+
197+
class StoreAttachIsolateInit {
198+
SendPort sendPort;
199+
String path;
200+
201+
StoreAttachIsolateInit(this.sendPort, this.path);
202+
}
203+
204+
void storeAttachIsolate(StoreAttachIsolateInit init) async {
205+
final store2 = Store.attach(getObjectBoxModel(), init.path);
206+
207+
final commandPort = ReceivePort();
208+
init.sendPort.send(commandPort.sendPort);
209+
init.sendPort.send(InternalStoreAccess.ptr(store2).address);
210+
211+
await for (final message in commandPort) {
212+
if (message is int) {
213+
final read = store2.box<TestEntity>().get(message);
214+
init.sendPort.send(read?.tString);
215+
} else if (message == null) {
216+
store2.close();
217+
init.sendPort.send(null);
218+
break;
219+
}
220+
}
221+
222+
print('Store attach isolate finished');
223+
}

0 commit comments

Comments
 (0)