Skip to content

Commit 2b22e25

Browse files
Merge branch '194-in-memory' into 'dev'
Resolve "Add test for in-memory" See merge request objectbox/objectbox-java!127
2 parents 3601414 + c20ee44 commit 2b22e25

File tree

8 files changed

+305
-112
lines changed

8 files changed

+305
-112
lines changed

objectbox-java/src/main/java/io/objectbox/BoxStore.java

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2023 ObjectBox Ltd. All rights reserved.
2+
* Copyright 2017-2024 ObjectBox Ltd. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package io.objectbox;
1818

19+
import org.greenrobot.essentials.collections.LongHashMap;
20+
1921
import java.io.Closeable;
2022
import java.io.File;
2123
import java.io.IOException;
@@ -55,7 +57,6 @@
5557
import io.objectbox.reactive.DataPublisher;
5658
import io.objectbox.reactive.SubscriptionBuilder;
5759
import io.objectbox.sync.SyncClient;
58-
import org.greenrobot.essentials.collections.LongHashMap;
5960

6061
/**
6162
* An ObjectBox database that provides {@link Box Boxes} to put and get objects of specific entity classes
@@ -69,6 +70,9 @@ public class BoxStore implements Closeable {
6970
@Nullable private static Object context;
7071
@Nullable private static Object relinker;
7172

73+
/** Prefix supplied with database directory to signal a file-less and in-memory database should be used. */
74+
public static final String IN_MEMORY_PREFIX = "memory:";
75+
7276
/** Change so ReLinker will update native library when using workaround loading. */
7377
public static final String JNI_VERSION = "3.7.1";
7478

@@ -137,6 +141,12 @@ public static String getVersionNative() {
137141
return nativeGetVersion();
138142
}
139143

144+
/**
145+
* @return true if DB files did not exist or were successfully removed,
146+
* false if DB files exist that could not be removed.
147+
*/
148+
static native boolean nativeRemoveDbFiles(String directory, boolean removeDir);
149+
140150
/**
141151
* Creates a native BoxStore instance with FlatBuffer {@link FlatStoreOptions} {@code options}
142152
* and a {@link ModelBuilder} {@code model}. Returns the handle of the native store instance.
@@ -318,6 +328,12 @@ public static boolean isSyncServerAvailable() {
318328
}
319329

320330
static String getCanonicalPath(File directory) {
331+
// Skip directory check if in-memory prefix is used.
332+
if (directory.getPath().startsWith(IN_MEMORY_PREFIX)) {
333+
// Just return the path as is (e.g. "memory:data"), safe to use for string-based open check as well.
334+
return directory.getPath();
335+
}
336+
321337
if (directory.exists()) {
322338
if (!directory.isDirectory()) {
323339
throw new DbException("Is not a directory: " + directory.getAbsolutePath());
@@ -681,38 +697,30 @@ public boolean deleteAllFiles() {
681697
/**
682698
* Danger zone! This will delete all files in the given directory!
683699
* <p>
684-
* No {@link BoxStore} may be alive using the given directory.
700+
* No {@link BoxStore} may be alive using the given directory. E.g. call this before building a store. When calling
701+
* this after {@link #close() closing} a store, read the docs of that method carefully first!
685702
* <p>
686-
* If you did not use a custom name with BoxStoreBuilder, you can pass "new File({@link
687-
* BoxStoreBuilder#DEFAULT_NAME})".
703+
* If no {@link BoxStoreBuilder#name(String) name} was specified when building the store, use like:
704+
*
705+
* <pre>{@code
706+
* BoxStore.deleteAllFiles(new File(BoxStoreBuilder.DEFAULT_NAME));
707+
* }</pre>
708+
*
709+
* <p>For an {@link BoxStoreBuilder#inMemory(String) in-memory} database, this will just clean up the in-memory
710+
* database.
688711
*
689712
* @param objectStoreDirectory directory to be deleted; this is the value you previously provided to {@link
690713
* BoxStoreBuilder#directory(File)}
691714
* @return true if the directory 1) was deleted successfully OR 2) did not exist in the first place.
692715
* Note: If false is returned, any number of files may have been deleted before the failure happened.
693-
* @throws IllegalStateException if the given directory is still used by a open {@link BoxStore}.
716+
* @throws IllegalStateException if the given directory is still used by an open {@link BoxStore}.
694717
*/
695718
public static boolean deleteAllFiles(File objectStoreDirectory) {
696-
if (!objectStoreDirectory.exists()) {
697-
return true;
698-
}
699-
if (isFileOpen(getCanonicalPath(objectStoreDirectory))) {
719+
String canonicalPath = getCanonicalPath(objectStoreDirectory);
720+
if (isFileOpen(canonicalPath)) {
700721
throw new IllegalStateException("Cannot delete files: store is still open");
701722
}
702-
703-
File[] files = objectStoreDirectory.listFiles();
704-
if (files == null) {
705-
return false;
706-
}
707-
for (File file : files) {
708-
if (!file.delete()) {
709-
// OK if concurrently deleted. Fail fast otherwise.
710-
if (file.exists()) {
711-
return false;
712-
}
713-
}
714-
}
715-
return objectStoreDirectory.delete();
723+
return nativeRemoveDbFiles(canonicalPath, true);
716724
}
717725

718726
/**

objectbox-java/src/main/java/io/objectbox/BoxStoreBuilder.java

Lines changed: 95 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2023 ObjectBox Ltd. All rights reserved.
2+
* Copyright 2017-2024 ObjectBox Ltd. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package io.objectbox;
1818

19+
import org.greenrobot.essentials.io.IoUtils;
20+
1921
import java.io.BufferedInputStream;
2022
import java.io.BufferedOutputStream;
2123
import java.io.File;
@@ -43,7 +45,6 @@
4345
import io.objectbox.exception.DbMaxReadersExceededException;
4446
import io.objectbox.flatbuffers.FlatBufferBuilder;
4547
import io.objectbox.ideasonly.ModelUpdate;
46-
import org.greenrobot.essentials.io.IoUtils;
4748

4849
/**
4950
* Configures and builds a {@link BoxStore} with reasonable defaults. To get an instance use {@code MyObjectBox.builder()}.
@@ -77,6 +78,9 @@ public class BoxStoreBuilder {
7778
/** Ignored by BoxStore */
7879
private String name;
7980

81+
/** If non-null, using an in-memory database with this identifier. */
82+
private String inMemory;
83+
8084
/** Defaults to {@link #DEFAULT_MAX_DB_SIZE_KBYTE}. */
8185
long maxSizeInKByte = DEFAULT_MAX_DB_SIZE_KBYTE;
8286

@@ -92,8 +96,6 @@ public class BoxStoreBuilder {
9296

9397
int debugFlags;
9498

95-
private boolean android;
96-
9799
boolean debugRelations;
98100

99101
int fileMode;
@@ -134,6 +136,8 @@ private BoxStoreBuilder() {
134136
/** Called internally from the generated class "MyObjectBox". Check MyObjectBox.builder() to get an instance. */
135137
@Internal
136138
public BoxStoreBuilder(byte[] model) {
139+
// Note: annotations do not guarantee parameter is non-null.
140+
//noinspection ConstantValue
137141
if (model == null) {
138142
throw new IllegalArgumentException("Model may not be null");
139143
}
@@ -142,16 +146,15 @@ public BoxStoreBuilder(byte[] model) {
142146
}
143147

144148
/**
145-
* Name of the database, which will be used as a directory for DB files.
149+
* Name of the database, which will be used as a directory for database files.
146150
* You can also specify a base directory for this one using {@link #baseDirectory(File)}.
147-
* Cannot be used in combination with {@link #directory(File)}.
151+
* Cannot be used in combination with {@link #directory(File)} and {@link #inMemory(String)}.
148152
* <p>
149153
* Default: "objectbox", {@link #DEFAULT_NAME} (unless {@link #directory(File)} is used)
150154
*/
151155
public BoxStoreBuilder name(String name) {
152-
if (directory != null) {
153-
throw new IllegalArgumentException("Already has directory, cannot assign name");
154-
}
156+
checkIsNull(directory, "Already has directory, cannot assign name");
157+
checkIsNull(inMemory, "Already set to in-memory database, cannot assign name");
155158
if (name.contains("/") || name.contains("\\")) {
156159
throw new IllegalArgumentException("Name may not contain (back) slashes. " +
157160
"Use baseDirectory() or directory() to configure alternative directories");
@@ -161,65 +164,89 @@ public BoxStoreBuilder name(String name) {
161164
}
162165

163166
/**
164-
* The directory where all DB files should be placed in.
165-
* Cannot be used in combination with {@link #name(String)}/{@link #baseDirectory(File)}.
167+
* The directory where all database files should be placed in.
168+
* <p>
169+
* If the directory does not exist, it will be created. Make sure the process has permissions to write to this
170+
* directory.
171+
* <p>
172+
* To switch to an in-memory database, use a file path with {@link BoxStore#IN_MEMORY_PREFIX} and an identifier
173+
* instead:
174+
* <p>
175+
* <pre>{@code
176+
* BoxStore inMemoryStore = MyObjectBox.builder()
177+
* .directory(BoxStore.IN_MEMORY_PREFIX + "notes-db")
178+
* .build();
179+
* }</pre>
180+
* Alternatively, use {@link #inMemory(String)}.
181+
* <p>
182+
* Can not be used in combination with {@link #name(String)}, {@link #baseDirectory(File)}
183+
* or {@link #inMemory(String)}.
166184
*/
167185
public BoxStoreBuilder directory(File directory) {
168-
if (name != null) {
169-
throw new IllegalArgumentException("Already has name, cannot assign directory");
170-
}
171-
if (!android && baseDirectory != null) {
172-
throw new IllegalArgumentException("Already has base directory, cannot assign directory");
173-
}
186+
checkIsNull(name, "Already has name, cannot assign directory");
187+
checkIsNull(inMemory, "Already set to in-memory database, cannot assign directory");
188+
checkIsNull(baseDirectory, "Already has base directory, cannot assign directory");
174189
this.directory = directory;
175190
return this;
176191
}
177192

178193
/**
179194
* In combination with {@link #name(String)}, this lets you specify the location of where the DB files should be
180195
* stored.
181-
* Cannot be used in combination with {@link #directory(File)}.
196+
* Cannot be used in combination with {@link #directory(File)} or {@link #inMemory(String)}.
182197
*/
183198
public BoxStoreBuilder baseDirectory(File baseDirectory) {
184-
if (directory != null) {
185-
throw new IllegalArgumentException("Already has directory, cannot assign base directory");
186-
}
199+
checkIsNull(directory, "Already has directory, cannot assign base directory");
200+
checkIsNull(inMemory, "Already set to in-memory database, cannot assign base directory");
187201
this.baseDirectory = baseDirectory;
188202
return this;
189203
}
190204

191205
/**
192-
* On Android, you can pass a Context to set the base directory using this method.
193-
* This will conveniently configure the storage location to be in the files directory of your app.
206+
* Switches to an in-memory database using the given name as its identifier.
207+
* <p>
208+
* Can not be used in combination with {@link #name(String)}, {@link #directory(File)}
209+
* or {@link #baseDirectory(File)}.
210+
*/
211+
public BoxStoreBuilder inMemory(String identifier) {
212+
checkIsNull(name, "Already has name, cannot switch to in-memory database");
213+
checkIsNull(directory, "Already has directory, cannot switch to in-memory database");
214+
checkIsNull(baseDirectory, "Already has base directory, cannot switch to in-memory database");
215+
inMemory = identifier;
216+
return this;
217+
}
218+
219+
/**
220+
* Use to check conflicting properties are not set.
221+
* If not null, throws {@link IllegalStateException} with the given message.
222+
*/
223+
private static void checkIsNull(@Nullable Object value, String errorMessage) {
224+
if (value != null) {
225+
throw new IllegalStateException(errorMessage);
226+
}
227+
}
228+
229+
/**
230+
* Use on Android to pass a <a href="https://developer.android.com/reference/android/content/Context">Context</a>
231+
* for loading the native library and, if not an {@link #inMemory(String)} database, for creating the base
232+
* directory for database files in the
233+
* <a href="https://developer.android.com/reference/android/content/Context#getFilesDir()">files directory of the app</a>.
194234
* <p>
195-
* In more detail, this assigns the base directory (see {@link #baseDirectory}) to
235+
* In more detail, upon {@link #build()} assigns the base directory (see {@link #baseDirectory}) to
196236
* {@code context.getFilesDir() + "/objectbox/"}.
197-
* Thus, when using the default name (also "objectbox" unless overwritten using {@link #name(String)}), the default
198-
* location of DB files will be "objectbox/objectbox/" inside the app files directory.
199-
* If you specify a custom name, for example with {@code name("foobar")}, it would become
200-
* "objectbox/foobar/".
237+
* Thus, when using the default name (also "objectbox", unless overwritten using {@link #name(String)}), the default
238+
* location of database files will be "objectbox/objectbox/" inside the app's files directory.
239+
* If a custom name is specified, for example with {@code name("foobar")}, it would become "objectbox/foobar/".
201240
* <p>
202-
* Alternatively, you can also use {@link #baseDirectory} or {@link #directory(File)} instead.
241+
* Use {@link #baseDirectory(File)} or {@link #directory(File)} to specify a different directory for the database
242+
* files.
203243
*/
204244
public BoxStoreBuilder androidContext(Object context) {
205245
//noinspection ConstantConditions Annotation does not enforce non-null.
206246
if (context == null) {
207247
throw new NullPointerException("Context may not be null");
208248
}
209249
this.context = getApplicationContext(context);
210-
211-
File baseDir = getAndroidBaseDir(context);
212-
if (!baseDir.exists()) {
213-
baseDir.mkdir();
214-
if (!baseDir.exists()) { // check baseDir.exists() because of potential concurrent processes
215-
throw new RuntimeException("Could not init Android base dir at " + baseDir.getAbsolutePath());
216-
}
217-
}
218-
if (!baseDir.isDirectory()) {
219-
throw new RuntimeException("Android base dir is not a dir: " + baseDir.getAbsolutePath());
220-
}
221-
baseDirectory = baseDir;
222-
android = true;
223250
return this;
224251
}
225252

@@ -504,7 +531,7 @@ public BoxStoreBuilder debugRelations() {
504531
* {@link DbException} are thrown during query execution).
505532
*
506533
* @param queryAttempts number of attempts a query find operation will be executed before failing.
507-
* Recommended values are in the range of 2 to 5, e.g. a value of 3 as a starting point.
534+
* Recommended values are in the range of 2 to 5, e.g. a value of 3 as a starting point.
508535
*/
509536
@Experimental
510537
public BoxStoreBuilder queryAttempts(int queryAttempts) {
@@ -580,14 +607,36 @@ byte[] buildFlatStoreOptions(String canonicalPath) {
580607
}
581608

582609
/**
583-
* Builds a {@link BoxStore} using any given configuration.
610+
* Builds a {@link BoxStore} using the current configuration of this builder.
611+
*
612+
* <p>If {@link #androidContext(Object)} was called and no {@link #directory(File)} or {@link #baseDirectory(File)}
613+
* is configured, creates and sets {@link #baseDirectory(File)} as explained in {@link #androidContext(Object)}.
584614
*/
585615
public BoxStore build() {
616+
// If in-memory, use a special directory (it will never be created)
617+
if (inMemory != null) {
618+
directory = new File(BoxStore.IN_MEMORY_PREFIX + inMemory);
619+
}
620+
// On Android, create and set base directory if no directory is explicitly configured
621+
if (directory == null && baseDirectory == null && context != null) {
622+
File baseDir = getAndroidBaseDir(context);
623+
if (!baseDir.exists()) {
624+
baseDir.mkdir();
625+
if (!baseDir.exists()) { // check baseDir.exists() because of potential concurrent processes
626+
throw new RuntimeException("Could not init Android base dir at " + baseDir.getAbsolutePath());
627+
}
628+
}
629+
if (!baseDir.isDirectory()) {
630+
throw new RuntimeException("Android base dir is not a dir: " + baseDir.getAbsolutePath());
631+
}
632+
baseDirectory = baseDir;
633+
}
586634
if (directory == null) {
587-
name = dbName(name);
588635
directory = getDbDir(baseDirectory, name);
589636
}
590-
checkProvisionInitialDbFile();
637+
if (inMemory == null) {
638+
checkProvisionInitialDbFile();
639+
}
591640
return new BoxStore(this);
592641
}
593642

tests/objectbox-java-test/build.gradle.kts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,18 @@ dependencies {
7272
testImplementation("app.cash.turbine:turbine:0.5.2")
7373
}
7474

75-
tasks.test {
75+
val testInMemory by tasks.registering(Test::class) {
76+
group = "verification"
77+
description = "Run unit tests with in-memory database"
78+
systemProperty("obx.inMemory", true)
79+
}
80+
81+
// Run in-memory tests as part of regular check run
82+
tasks.check {
83+
dependsOn(testInMemory)
84+
}
85+
86+
tasks.withType<Test> {
7687
if (System.getenv("TEST_WITH_JAVA_X86") == "true") {
7788
// To run tests with 32-bit ObjectBox
7889
// Note: 32-bit JDK is only available on Windows

0 commit comments

Comments
 (0)