Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
ObjectBox is an Android database designed for Edge Computing and Offline First experiences. Here is the How-to and troubleshooting guides for using ObjectBox on Android with Java or Kotlin.
Learn all about Local Unit Tests, LiveData, Paging, and App Bundle. Please let us know, if our developer resources are helpful for you: The smiley at the bottom of the page already gives us an indication.
Explore the on-device database and SQLite alternative for object and vector data. This is the official ObjectBox documentation for Java/Kotlin, Dart/Flutter, and Python.
🚀 Our mission is to revolutionize mobile app development by making it fast and efficient with our intuitive and user-friendly native-language APIs. With ObjectBox, you can easily store and synchronize objects in various programming languages such as Python, Java, C, Go, Kotlin, Dart, C++, and Swift. It works seamlessly across different platforms like Android, iOS, Windows, and Linux, and you can even use it with Docker.
💚 Please share your feedback! Community feedback is very important for us as we strive to make ObjectBox better for our users. Our feedback form is anonymous and only takes 2 minutes to fill in. Every response is highly appreciated. Thank you in advance! 🙏
👍 To rate this documentation, use the "Was this page helpful?" smiley 🙃 at the end of each page.
🚩 Feel free to open an issue on GitHub (for Java & Kotlin or Dart/Flutter) or send us your comments to contact[at]objectbox.io - Thank you! - and if you like what you see, we also appreciate a shout out :) or GitHub star ⭐
Guides and documentation for advanced use cases of ObjectBox.
Explanation of Object IDs and how they are used and assigned in ObjectBox.
Objects must have an ID property of type long
. You are free to use the wrapper type java.lang.Long
, but we advise against it in most cases. long
IDs are enforced to make ObjectBox very efficient internally.
If your application requires other ID types (such as a string UID given by a server), you can model them as standard properties and use queries to look up entities by your application-specific ID.
When you create new entity objects (on the language level), they are not persisted yet and their ID is (zero). Once an entity is put (persisted), ObjectBox will assign an ID to the entity. You can access the ID property right after the call to put()
.
Those are also applied the other way round: ObjectBox uses the ID as a state indication of whether an entity is new (zero) or already persisted (non-zero). This is used internally, e.g. for relations that heavily rely on IDs.
Object IDs may be any long
value, with two exceptions:
0 (zero): Objects with an ID of zero (and null
if the ID is of type Long
) are considered new (not persisted before). Putting such an object will always insert a new object and assign an unused ID to it.
0xFFFFFFFFFFFFFFFF (-1 in Java): This value is reserved for internal use by ObjectBox and may not be used by the app.
By default, object IDs are assigned by ObjectBox. For each new object, ObjectBox will assign an unused ID that is above the current highest ID value used in a box. For example, if there are two objects with ID 1 and ID 100 in a box the next object that is put will be assigned ID 101.
Also, note that this will mean in some circumstances IDs from deleted objects may be reused. So it is best to not rely on a specific ID getting assigned.
By default, only ObjectBox may assign IDs. If you try to put an object with an ID greater than the currently highest ID, ObjectBox will throw an error.
If your code needs to assign IDs by itself you can change the @Id
annotation to:
@Id(assignable = true)
long id;
@Id(assignable: true)
int id = 0;
This will allow putting an entity with any valid ID (see Special Object IDs). If the @Id
field is writable, it can still be set to zero to let ObjectBox auto-assign a new ID.
Warning: manually assigning IDs breaks automatic state detection (new vs. persisted entity based on the ID). Therefore, entities with manually assigned IDs should be put immediately and the Box may have to be attached manually, especially when working with relations.
For details see the documentation about updating relations.
Check this issue on Github for status.
Troubleshoot or avoid crashes when using ObjectBox db and Android App Bundle or due to buggy devices. Google-certified devices prevent this crash.
Your app might observe crashes due to UnsatisfiedLinkError
or (since ObjectBox 2.3.4) LinkageError
on some devices. This has mainly two reasons:
If your app uses the format, the legacy split APK feature or the native library can't be found.
Or if your app's minimum SDK level is below API 23 (Marshmallow), there are known bugs in Android's native library loading code.
Let us know if the below suggestions do not resolve your crashes in .
When using an App Bundle or split APKs Google Play only delivers the split APKs required for each user's device configuration, including its architecture (ABI). If users bypass Google Play to install your app ("sideloading") they might not install all of the required split APKs. If the split APK containing the ObjectBox native library required for the device ABI is missing, your app will crash with LinkageError
when building BoxStore.
Add the to the dependencies block:
In the Application
class add the missing split APKs check before calling super.onCreate()
:
If a broken installation is detected, users will see a message and Reinstall button asking them to re-install the app from Google Play.
See to use the Play Core library detection.
If the Play Core library should not be used, there are two alternatives:
You can guard the MyObjectBox build call and for example display an activity with an info message (e.g. direct users to reinstall the app from Google Play, send you an error report, ...):
As an example see .
The simplest solution is to always include native libraries for all supported ABIs. However, this will increase the download size of your app for all users.
Source:
On some devices and if your minimum SDK is below API 23 (Android 6.0 Marshmallow), loading the native library may fail with LinkageError
due to known bugs in Android's native library loading code. To counter this ObjectBox includes support for the tool which will try to extract the native library manually if loading it normally fails.
To enable this, just add ReLinker to your dependencies:
ObjectBox is calling ReLinker via reflection. If you are using ProGuard or Multidex, make sure to add keep rules so that ReLinker code is not stripped from the final app or is not in the primary dex file.
For ProGuard add this line:
For Multidex add a multiDexKeepProguard file to your build file:
And in the multidex-config.pro file add the same rule as above:
To enable debug logs for ReLinker you can pass a custom ReLinkerInstance
when building BoxStore
:
implementation 'com.google.android.play:core:1.7.3'
public class App extends Application {
@Override
public void onCreate() {
if (MissingSplitsManagerFactory.create(this).disableAppIfMissingRequiredSplits()) {
return; // Skip app initialization.
}
super.onCreate();
// ...
}
}
// guard the build call and set some flag (here setting the boxStore field null)
try {
boxStore = MyObjectBox.builder()
.androidContext(context.getApplicationContext())
.build();
} catch (LinkageError e) {
boxStore = null;
Log.e(App.TAG, "Failed to load ObjectBox: " + e.getMessage());
}
// then for example in the main activity check the flag in onCreate and
// direct to an info/error message without the app crashing:
if (ObjectBox.get() == null) {
startActivity(new Intent(this, ErrorActivity.class));
finish();
return;
}
android {
bundle {
abi {
// This property is set to true by default.
enableSplit = false
}
}
}
// https://github.com/KeepSafe/ReLinker/releases
implementation 'com.getkeepsafe.relinker:relinker:1.4.1'
-keep class com.getkeepsafe.relinker.** { *; }
android {
buildTypes {
release {
multiDexKeepProguard file('multidex-config.pro')
}
}
}
-keep class com.getkeepsafe.relinker.** { *; }
boxStore = MyObjectBox.builder()
.androidContext(App.this)
.androidReLinker(ReLinker.log(new ReLinker.Logger() {
@Override
public void log(String message) { Log.d(TAG, message); }
}))
.build();
Answers to questions specific to ObjectBox for Java and Dart
ObjectBox comes with full Kotlin support including data classes. And yes, it supports RxJava and reactive queries without RxJava.
Yes. ObjectBox comes with strong relation support and offers features like “eager loading” for optimal performance.
The ObjectBox Gradle plugin only looks for entities in the current module, it does not search library modules. However, you can have a separate database (MyObjectBox
file) for each module. Just make sure to pass different database names when building your BoxStore.
It depends. Internally and in the C API, ObjectBox does zero-copy reads. Java objects require a single copy only. However, copying data is only a minor factor in overall performance. In ObjectBox, objects are POJOs (plain objects), and all properties will be properly initialized. Thus, there is no run time penalty for accessing properties and values do not change in unexpected ways when the database updates.
No. The objects you get from ObjectBox are POJOs (plain objects). You are safe to pass them around in threads.
It depends. In most cases no IO operations (which is what ObjectBox does) should be run on the main thread. This avoids (even rare) hangs of your app.
However, in some cases it might be alright. While ObjectBox and the underlying OS and file system can give no hard guarantees, reading (e.g. Box.get(id)) small amounts of data is typically very fast and should have no notable impact on observed performance of your app. This is because in ObjectBox reads, unlike writes, are not blocked by other operations.
ObjectBox supports Android 5.0 (API level 21) or newer and works on most device architectures (armeabi-v7a, arm64-v8a, x86 and x86_64). An Android library is available for Java (also Kotlin) and Flutter projects.
ObjectBox supports iOS 12 or newer on 64-bit devices only. An iOS library is available for Flutter or Swift projects.
ObjectBox also runs on Linux (x86_64, arm64, armv7), Windows (x86_64) and macOS 10.15 or newer (x86_64, Apple M1) with support for Kotlin, Java, Dart, Flutter, Go, C, Swift and Python.
Yes, you can ObjectBox on the desktop/server side. Contact us for details if you are interested in running ObjectBox in client/server mode or containerized!
Yes. You can run the ObjectBox database on any IoT device that runs Linux. We also offer Go and C APIs. Check our cross-platform tutorial and see how easy it is to sync data across platforms in real time with ObjectBox.
If you only do a rename on the language level, ObjectBox will by default remove the old and add a new entity/property. To do a rename, you must specify the UID.
The Google Play download size increases by around 2.3 MB (checked for ObjectBox 3.0.0) as a native library for each supported architecture is packaged. If you build multiple APKs split by ABI or use Android App Bundle it only increases around 0.6 MB.
When building with minimum API level 23 (Android 6.0), the raw file (APK or AAB) size increases more, by around 6.1 MB. This is because the Android Plugin adds extractNativeLibs="false"
to your AndroidManifest.xml
as recommended by Google. This turns off compression. However, this allows Google Play to optimally compress APKs before downloading them to each device (see download size above) and reduces the size of your app updates (on Android 6.0 or newer). Read this Android developers post for details. It also avoids issues that might occur when extracting the libraries.
If you rather have a smaller APK/App Bundle instead of smaller app downloads and updates (e.g. when distributing in other stores) you can override the flag in your AndroidManifest.xml
:
<application
...
// not recommended for non-Flutter apps, increases app update size
android:extractNativeLibs="true"
tools:replace="android:extractNativeLibs"
...
</applicaton>
More importantly, ObjectBox adds little to the APK method count since it’s mostly written in native code.
Yes. ObjectBox stores all data in a single database file. Thus, you just need to prepare a database file and copy it to the correct location on the first start of your app (before you touch ObjectBox’s API).
The database file is called data.mdb
and is typically located in a subdirectory called objectbox
(or any name you passed to BoxStoreBuilder). On Android, the DB file is located inside the app’s files directory inside objectbox/objectbox/
. Or objectbox/<yourname>
if you assigned the custom name <yourname>
using BoxStoreBuilder.
In most cases, ObjectBox uses disk space quite optimally. Only once you add more data, the database file grows as required. When you delete data, file areas are marked as unused internally and will be reused by ObjectBox. Note that re-using existing file areas is much more efficient than shrinking and growing the file. In practice, once used file storage will be used again in the future; especially considering that stored data has the tendency to get more over time.
ObjectBox relies on multi-version concurrency storage based on "copy on write". This allows e.g. to read the previous state while a write transaction is active. A counter-intuitive consequence is that deleting data can actually increase disk usage because the old data is still referenced. But of course, forthcoming transactions can reuse the internally reclaimed space.
Non-standard use cases may require a temporary peak in data storage space that is followed by a permanent drop of storage space. To reclaim disk space for those cases, you need to delete the database files and restore them later; e.g. from the cloud or from a second store, which you set up to put the objects you want to keep.
Deleting the database files deletes the contained data permanently. If you want to restore old data, it's your responsibility to backup and restore.
While we don't recommend deleting the entire database, the API offers some methods to do so: first, close()
the BoxStore
and then delete the database files using BoxStore.deleteAllFiles(objectBoxDirectory)
. To avoid having to close BoxStore
delete files before building it, e.g. during app start-up.
// If BoxStore is in use, close it first.
store.close();
BoxStore.deleteAllFiles(new File(BoxStoreBuilder.DEFAULT_NAME));
// TODO Build a new BoxStore instance.
Questions that apply to all supported platforms and languages are answered in the general ObjectBox FAQ.
If you believe to have found a bug or missing feature, please create an issue.
For Java/Kotlin: https://github.com/objectbox/objectbox-java/issues
For Flutter/Dart: https://github.com/objectbox/objectbox-dart/issues
If you have a usage question regarding ObjectBox, please post on Stack Overflow. https://stackoverflow.com/questions/tagged/objectbox
How to inherit properties from entity super classes.
ObjectBox allows entity inheritance to share persisted properties in super classes. The base class can be an entity or non-entity class. For this purpose the @Entity
annotation is complemented by the @BaseEntity
annotation. There are three types of super classes, which are defined via annotations:
No annotation: The base class and its properties are not considered for persistence.
@BaseEntity: Properties are considered for persistence in sub classes, but the base class itself cannot be persisted.
@Entity: Properties are considered for persistence in sub classes, and the base class itself is a normally persisted entity.
For example:
// Note: Kotlin data classes do not support inheritance,
// so this example uses regular Kotlin classes.
// Superclass:
@BaseEntity
abstract class Base {
@Id
var id: Long = 0
var baseString: String? = null
constructor()
constructor(id: Long, baseString: String?) {
this.id = id
this.baseString = baseString
}
}
// Subclass:
@Entity
class Sub : Base {
var subString: String? = null
constructor()
constructor(id: Long,
baseString: String?,
subString: String?) : super(id, baseString) {
this.subString = subString
}
}
The model for Sub, Sub_, will now include all properties: id
, baseString
and subString
.
It is also possible to inherit properties from another entity:
// Entities inherit properties from super entities.
@Entity
public class SubSub extends Sub {
String subSubString;
public SubSub() {
}
public SubSub(long id, String baseString,
String subString, String subSubString) {
super(id, baseString, subString);
this.subSubString = subSubString;
}
}
// Entities inherit properties from super entities.
@Entity
class SubSub : Sub {
var subSubString: String? = null
constructor()
constructor(id: Long,
baseString: String?,
subString: String?,
subSubString: String?) : super(id, baseString, subString) {
this.subSubString= subSubString
}
}
It is possible to have classes in the inheritance chain that are not annotated with @BaseEntity. Their properties will be ignored and will not become part of the entity model.
It is not generally recommend to have a base entity class consisting of an ID property only. E.g. Java imposes an additional overhead to construct objects with a sub class.
Depending on your use case using interfaces may be more straightforward.
Superclasses annotated with @BaseEntity can not be part of a library.
There are no polymorphic queries (e.g. you cannot query for a base class and expect results from sub classes).
Currently any superclass, whether it is an @Entity or @BaseEntity, can not have any relations (like a ToOne or ToMany property).
// THIS DOES NOT WORK
@BaseEntity
public abstract class Base {
@Id long id;
ToOne<OtherEntity> other;
ToMany<OtherEntity> others;
}
// THIS DOES NOT WORK
@BaseEntity
abstract class Base {
@Id
var id: Long = 0
lateinit var other: ToOne<OtherEntity>
lateinit var others: ToMany<OtherEntity>
}
How to create ObjectBox local unit tests for Android projects.
ObjectBox supports local unit tests. This gives you the full ObjectBox functionality for running super fast test directly on your development machine.
On Android, unit tests can either run on an Android device (or emulator), so called instrumented tests, or they can run on your local development machine. Running local unit tests is typically much faster.
To learn how local unit tests for Android work in general have a look at the Android developers documentation on Building Local Unit Tests. Read along to learn how to use ObjectBox in your local unit tests.
This page also applies to writing unit tests for desktop projects.
Add the native ObjectBox library for your operating system to your existing test dependencies in your app’s build.gradle
file:
dependencies {
// Required -- JUnit 4 framework
testImplementation("junit:junit:4.12")
// Optional -- manually add native ObjectBox library to override auto-detection
testImplementation("io.objectbox:objectbox-linux:$objectboxVersion")
testImplementation("io.objectbox:objectbox-macos:$objectboxVersion")
testImplementation("io.objectbox:objectbox-windows:$objectboxVersion")
// Not added automatically:
// Since 2.9.0 we also provide ARM support for the Linux library
testImplementation("io.objectbox:objectbox-linux-arm64:$objectboxVersion")
testImplementation("io.objectbox:objectbox-linux-armv7:$objectboxVersion")
}
You create your local unit test class as usual under module-name/src/test/java/
. To use ObjectBox in your test methods you need to build a BoxStore instance using the generated MyObjectBox class of your project. You can use the directory(File) method on the BoxStore builder to ensure the test database is stored in a specific folder on your machine. To start with a clean database for each test you can delete the existing database using BoxStore.deleteAllFiles(File).
The following example shows how you could implement a local unit test class that uses ObjectBox:
public class NoteTest {
private static final File TEST_DIRECTORY = new File("objectbox-example/test-db");
private BoxStore store;
@Before
public void setUp() throws Exception {
// Delete any files in the test directory before each test to start with a clean database.
BoxStore.deleteAllFiles(TEST_DIRECTORY);
store = MyObjectBox.builder()
// Use a custom directory to store the database files in.
.directory(TEST_DIRECTORY)
// Optional: add debug flags for more detailed ObjectBox log output.
.debugFlags(DebugFlags.LOG_QUERIES | DebugFlags.LOG_QUERY_PARAMETERS)
.build();
}
@After
public void tearDown() throws Exception {
if (store != null) {
store.close();
store = null;
}
BoxStore.deleteAllFiles(TEST_DIRECTORY);
}
@Test
public void exampleTest() {
// get a box and use ObjectBox as usual
Box<Note> noteBox = store.boxFor(Note.class);
assertEquals(...);
}
}
open class NoteTest {
private var _store: BoxStore? = null
protected val store: BoxStore
get() = _store!!
@Before
fun setUp() {
// Delete any files in the test directory before each test to start with a clean database.
BoxStore.deleteAllFiles(TEST_DIRECTORY)
_store = MyObjectBox.builder()
// Use a custom directory to store the database files in.
.directory(TEST_DIRECTORY)
// Optional: add debug flags for more detailed ObjectBox log output.
.debugFlags(DebugFlags.LOG_QUERIES or DebugFlags.LOG_QUERY_PARAMETERS)
.build()
}
@After
fun tearDown() {
_store?.close()
_store = null
BoxStore.deleteAllFiles(TEST_DIRECTORY)
}
@Test
fun exampleTest() {
// Get a box and use ObjectBox as usual
val noteBox = store.boxFor(Note::class.java)
assertEquals(...)
}
companion object {
private val TEST_DIRECTORY = File("objectbox-example/test-db")
}
}
It’s usually a good idea to extract the setup and tear down methods into a base class for your tests. E.g.:
public class AbstractObjectBoxTest {
private static final File TEST_DIRECTORY = new File("objectbox-example/test-db");
protected BoxStore store;
@Before
public void setUp() throws Exception {
// Delete any files in the test directory before each test to start with a clean database.
BoxStore.deleteAllFiles(TEST_DIRECTORY);
store = MyObjectBox.builder()
// Use a custom directory to store the database files in.
.directory(TEST_DIRECTORY)
// Optional: add debug flags for more detailed ObjectBox log output.
.debugFlags(DebugFlags.LOG_QUERIES | DebugFlags.LOG_QUERY_PARAMETERS)
.build();
}
@After
public void tearDown() throws Exception {
if (store != null) {
store.close();
store = null;
}
BoxStore.deleteAllFiles(TEST_DIRECTORY);
}
}
open class AbstractObjectBoxTest {
private var _store: BoxStore? = null
protected val store: BoxStore
get() = _store!!
@Before
fun setUp() {
// Delete any files in the test directory before each test to start with a clean database.
BoxStore.deleteAllFiles(TEST_DIRECTORY)
_store = MyObjectBox.builder()
// Use a custom directory to store the database files in.
.directory(TEST_DIRECTORY)
// Optional: add debug flags for more detailed ObjectBox log output.
.debugFlags(DebugFlags.LOG_QUERIES or DebugFlags.LOG_QUERY_PARAMETERS)
.build()
}
@After
fun tearDown() {
_store?.close()
_store = null
BoxStore.deleteAllFiles(TEST_DIRECTORY)
}
companion object {
private val TEST_DIRECTORY = File("objectbox-example/test-db")
}
}
To test entities that have relations, like ToOne or ToMany properties, on the local JVM you must initialize them and add a transient BoxStore field.
See the documentation about "initialization magic" for an example and what to look out for.
Background: the "initialization magic" is normally done by the ObjectBox plugin using the Android Gradle Plugin Transform API or a Gradle task running after the Java compiler which allows to modify byte-code. However, this does currently not work for Kotlin code in Kotlin desktop projects.
Explaining the ObjectBox meta model file and how to resolve model file conflicts.
Unlike relational databases like SQLite, ObjectBox does not require you to create a database schema. That does not mean ObjectBox is schema-less. For efficiency reasons, ObjectBox manages a meta model of the data stored. This meta model is actually ObjectBox’s equivalent of a schema. It includes known object types including all properties, indexes, etc. A key difference to relational schemas is that ObjectBox tries to manage its meta model automatically. In some cases it needs your help. That’s why we will look at some details.
In the ObjectBox meta model, everything has an ID and a UID. IDs are used internally in ObjectBox to reference entities, properties, and indexes. For example, you have an entity “User” with the properties “id” and “name”. In the meta model the entity (type) could have the ID 42, and the properties the IDs 1 and 2. Property IDs must only be unique within their entity.
ObjectBox assigns meta model IDs sequentially (1, 2, 3, 4, …) and keeps track of the last used ID to prevent ID collisions.
As a rule of thumb, for each meta model ID there’s a corresponding UID. They complement IDs and are often used in combination (e.g. in the JSON file). While IDs are assigned sequentially, UIDs are a random long value. The job of UIDs is detecting and resolving concurrent modifications of the meta model.
A UID is unique across entities, properties, indexes, etc. Thus unlike IDs, an UID already used for an entity may not be used for a property. As a precaution to avoid side effects, ObjectBox keeps track of “retired” UIDs to ensure previously used but now abandoned UIDs are not used for new artifacts.
ObjectBox stores a part of its meta model in a JSON file. This file should be available to every developer and thus checked into a source version control system (e.g. git). The main purpose of this JSON file is to ensure consistent IDs and UIDs in the meta model across devices.
This JSON file is stored in
objectbox-models/default.json
for Android or Java projects,
lib/objectbox-model.json
for Dart or Flutter projects.
For example, look at a file from the ObjectBox example project:
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:6645479796472661392",
"lastPropertyId": "5:8452815412120793251",
"name": "Note",
"properties": [
{
"id": "1:9211738071025439652",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:8804670454579230281",
"name": "text",
"type": 9
},
{
"id": "4:1260602348787983453",
"name": "date",
"indexId": "1:9194998581973594219",
"type": 10,
"flags": 8
},
{
"id": "5:8452815412120793251",
"name": "authorId",
"indexId": "2:5206397068544851461",
"type": 11,
"flags": 520,
"relationTarget": "Author"
}
],
"relations": []
},
{
"id": "2:8096888707432154961",
"lastPropertyId": "2:388290850215565763",
"name": "Author",
"properties": [
{
"id": "1:2456363380762264329",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:388290850215565763",
"name": "name",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "2:8096888707432154961",
"lastIndexId": "2:5206397068544851461",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
As you can see, the “id” attributes combine the ID and UID using a colon. This protects against faulty merges. When applying the meta model to the database, ObjectBox will check for consistent IDs and UIDs.
At build time, ObjectBox gathers meta model information from the entities (@Entity classes) and the JSON file. The complete meta model information is written into the generated class MyObjectBox
.
Then, at runtime, the meta model assembled in MyObjectBox is synchronized with the meta model inside the ObjectBox database (file). UIDs are the primary keys to synchronize the meta model with the database. The synchronization involves a couple of consistency checks that may fail when you try to apply illegal meta data.
At some point you may want to rename an entity class or just a property. Without further information, ObjectBox will remove the old entity/property and add a new one with the new name. This is actually a valid scenario by itself: removing one property and adding another. To tell ObjectBox it should do a rename instead, you need to supply the property's previous UID.
Add an @Uid
annotation without any value to the entity or property you want to rename and trigger a project build. The build will fail with a message containing the UID you need to apply to the @Uid
annotation.
Also check out this how-to guide for hands-on information on renaming and resetting.
In the section on UIDs, we already hinted at the possibility of meta model conflicts. This means the meta model defined in the objectbox-models/default.json
file conflicts with the model of the existing database file.
When creating a Store the meta model is passed and verified against the database file. If a conflict exists an exception or error is thrown, for example:
// If lastEntityId in the model file is incorrect:
io.objectbox.exception.DbSchemaException:
DB's last entity ID 4 is higher than 3 from model
// If the UID of an entity in the model file has changed:
io.objectbox.exception.DbSchemaException:
Incoming entity ID 1:6645479796472661392 does not match existing UID 8096888707432154961
Such a conflict can be caused by
developers changing the meta model at the same time (e.g. in different version control system branches), typically by adding entity classes or properties.
the objectbox-models/default.json
model file getting accidentally deleted, it is then generated on the next build with a new set of UIDs.
a database file with a different model getting restored by Android Auto Backup or getting supplied as the initial database file.
There are basically two ways to resolve this:
Resolve conflicts in or reconstruct the default.json
meta model file to match the model of an existing database file.
Keep or create a fresh default.json
model file and delete the database file. However, this will lose all existing data.
The two options are explained in detail below.
Usually, it is preferred to edit the JSON file to resolve conflicts and fix the meta model. This involves the following steps:
Ensure IDs are unique: in the JSON file the id attribute has values in the format “ID:UID”. If you have duplicate IDs after a VCS merge, you should assign a new ID (keep the UID part!) to one of the two. Typically, the new ID would be “last used ID + 1”.
Update last ID values: for entities, update the attribute lastEntityId
; for properties, update the attribute lastPropertyId
of the enclosing entity back to the ID:UID with the ID indicated by the error message.
Check for other ID references: do a text search for the UID and check if the ID part is correct for all UID occurrences
To illustrate this with an example, let's assume the last assigned entity ID was 41. Thus the next entity ID will be 42. Now, the developers Alice and Bob add a new entity without knowing of each other. Alice adds a new entity “Ant” which is assigned the entity ID 42. At the same time, Bob adds the entity “Bear” which is also assigned the ID 42. After both developers committed their code, the ID 42 does not unique identify an entity type (“Ant” or “Bear”?). Furthermore, in Alice’s ObjectBox the entity ID 42 is already wired to “Ant” while Bob’s ObjectBox maps 42 to “Bear”. UIDs make this situation resolvable. Let’s say the UID is 12345 for “Ant” and 9876 for “Bear”. Now, when Bob pulls Alice’s changes, he is able to resolve the conflict. He manually assigns the entity ID 43 to “Bear” and updates the lastEntityId attribute accordingly to “43:9876” (ID:UID). After Bob commits his changes, both developers are able to continue with their ObjectBox files.
During initial development, it may be an option to just delete the meta model and all databases. This will cause a fresh start for the meta model, e.g. all UIDs will be regenerated. Follow these steps:
Delete the JSON file (objectbox-models/default.json)
Build the project to generate a new JSON file from scratch
Commit the recreated JSON file to your VCS (e.g. git)
Delete all previously created ObjectBox databases (e.g. for Android, delete the app’s data or uninstall the app)
While this is a simple approach, it has its obvious disadvantages. For example for a published app, all existing data would be lost.
// Superclass:
@BaseEntity
public abstract class Base {
@Id long id;
String baseString;
public Base() {
}
public Base(long id, String baseString) {
this.id = id;
this.baseString = baseString;
}
}
// Subclass:
@Entity
public class Sub extends Base {
String subString;
public Sub() {
}
public Sub(long id, String baseString, String subString) {
super(id, baseString);
this.subString = subString;
}
}
Learn how to build a simple note-taking app with ObjectBox.
This tutorial will walk you through a simple note-taking app explaining how to do basic operations with ObjectBox. To just integrate ObjectBox into your project, look at the page.
You can check out the example code from GitHub. This allows you to run the code and explore it in its entirety. It is a simple app for taking notes where you can add new notes by typing in some text and delete notes by clicking on an existing note.
To store notes there is an entity class called Note
(or Task
in Python). It defines the structure or model of the data persisted (saved) in the database for a note: its id, the note text and the creation date.
In general, an ObjectBox entity is an annotated class persisted in the database with its properties. In order to extend the note or to create new entities, you simply modify or create new plain classes and annotate them with @Entity
and @Id;
in Python opt-in uid
argument i.e. @Entity(uid=)
.
Go ahead and build the project, for example by using Build > Make project in Android Studio. This triggers ObjectBox to generate some classes, like MyObjectBox.java
, and some other classes used by ObjectBox internally.
Go ahead and build the project, for example by using Build > Make project in Android Studio. This triggers ObjectBox to generate some classes, like MyObjectBox.kt
, and some other classes used by ObjectBox internally.
Before running the app, run the ObjectBox code generator to create binding code for the entity classes: flutter pub run build_runner build
Also re-run this after changing the note class.
To see how new notes are added to the database, take a look at the following code fragments. The Box
provides database operations for Note
objects. A Box is the main interaction with object data.
When a user adds a note the method addNote()
is called. There, a new Note
object is created and put into the database using the Box
reference:
Note that the ID property (0 when creating the Note
object), is assigned by ObjectBox during a put.
When the user taps a note, it is deleted. The Box
provides remove()
to achieve this:
To query and display notes in a list a Query
instance is built once:
And then executed each time any notes change:
In addition to a result sort order, you can add various conditions to filter the results, like equality or less/greater than, when building a query.
What is not shown in the example, is how to update an existing (== the ID is not 0) note. Do so by just modifying any of its properties and then put it again with the changed object:
There are additional methods to put, find, query, count or remove entities. Check out the methods of the Box class in API docs (for or ) to learn more.
Now that you saw ObjectBox in action, how did we get that database (or store) instance? Typically you should set up a BoxStore or Store once for the whole app. This example uses a .
Remember: ObjectBox is a NoSQL database on its own and thus NOT based on SQL or SQLite. That’s why you do not need to set up “CREATE TABLE” statements during initialization.
🌿
ObjectBox is a fully transactional database satisfying ACID properties. ObjectBox database gives you an easy way to develop safe and efficient data applications; single or multi-threaded.
A transaction can group several operations into a single unit of work that either executes completely or not at all. If you are looking for a more detailed introduction to transactions in general, please consult other resources like Wikipedia on . For ObjectBox transactions continue reading:
You may not notice it, but almost all interactions with ObjectBox involve transactions. For example, if you call put
a write transaction is used. Also if you get
an object or query for objects, a read transaction is used. All of this is done under the hood and transparent to you. It may be fine to completely ignore transactions altogether in your app without running into any problems. With more complex apps however, it’s usually worth learning transaction basics to make your app more consistent and efficient.
Accessing data always happens inside an implicit transaction, the API hides this detail for convenience.
You should use an explicit transaction for non-trivial operations for better speed and atomicity.
Transactions manage multi-threading; e.g. a transaction is tied to a thread and vice versa.
Read(-only) transactions never get blocked or block a write transaction.
There can only be a single write transaction at any time; they run strictly one after the other (sequential).
Sequential execution simplifies user code that is run in write transactions and makes it safer.
Keep write transactions short to optimize throughput, e.g. prepare data before entering it.
We learned that all ObjectBox operations run in implicit transactions – unless an explicit transaction is in progress. In the latter case, multiple operations share the (explicit) transaction. In other words, with explicit transactions, you control the transaction boundary. Doing so can greatly improve efficiency and consistency in your app.
The advantage of explicit transactions over the bulk put operations is that you can perform any number of operations and use objects of multiple boxes. In addition, you get a consistent (transactional) view on your data while the transaction is in progress.
Example for a write transaction:
The class offers the following methods to perform explicit transactions:
runInTx: Runs the given runnable inside a transaction.
runInReadTx: Runs the given runnable inside a read(-only) transaction. Unlike write transactions, multiple read transactions can run at the same time.
runInTxAsync: Runs the given Runnable as a transaction in a separate thread. Once the transaction completes the given callback is called (callback may be null).
callInTx: Like runInTx(Runnable), but allows returning a value and throwing an exception.
The Store
class provides read_tx
and write_tx
methods for creating read/write transactions which should be called in a with
statement:
Understanding transactions is essential to master database performance. If you just remember one sentence on this topic, it should be this one: a write transaction has its price.
Committing a transaction involves syncing data to physical storage, which is a relatively expensive operation for databases. Only when the file system confirms that all data has been stored in a durable manner (not just memory cached), the transaction can be considered successful. This file sync required by a transaction may take a couple of milliseconds. Keep this in mind and try to group several operations (e.g.put
calls) in one transaction.
Consider this example:
Do you see what’s wrong with that code? There is an implicit transaction for each user which is very inefficient, especially for a high number of objects. It is much more efficient to use one of the put overloads to store all users at once:
Much better! If you have 1,000 users, the latter example uses a single transaction to store all users. The first code example uses 1,000 (!) implicit transactions, causing a massive slow down.
In ObjectBox, read transactions are cheap. In contrast to write transactions, there is no commit and thus no expensive sync to the file system. Operations like get
, count
, and queries run inside an implicit read transaction if they are not called when already inside an explicit transaction (read or write). Note that it is illegal to put
when inside a read transaction: an exception will be thrown.
While read transactions are much cheaper than write transactions, there is still some overhead to starting a read transaction. Thus, for a high number of reads (e.g. hundreds, in a loop), you can improve performance by grouping those reads in a single read transaction (see explicit transactions below).
ObjectBox gives developers semantics. This allows multiple concurrent readers (read transactions) which can execute immediately without blocking or waiting. This is guaranteed by storing multiple versions of (committed) data. Even if a write transaction is in progress, a read transaction can read the last consistent state immediately. Write transactions are executed sequentially to ensure a consistent state. Thus, it is advised to keep write transactions short to avoid blocking other pending write transactions. For example, it is usually a bad idea to do networking or complex calculations while inside a write transaction. Instead, do any expensive operation and prepare objects before entering a write transaction.
Note that you do not have to worry about making write transactions sequential yourself. If multiple threads want to write at the same time (e.g. via put
or runInTx
), one of the threads will be selected to go first, while the other threads have to wait. It works just like a lock or synchronized
in Java.
Avoid locking (e.g. via synchronized
or java.util.concurrent.locks
) when inside a write transaction when possible. Because write transactions run exclusively, they effectively acquire a write lock internally. As with all locks, you need to pay close attention when multiple locks are involved. Always obtain locks in the same order to avoid deadlocks. If you acquire a lock “X” inside a transaction, you must ensure that your code does not start another write transaction while having the lock “X”.
boxStore.runInTx(() -> {
for(User user: allUsers) {
if(modify(user)) box.put(user);
else box.remove(user);
}
});
with store.write_tx():
for user in allUsers:
if modify(user):
box.put(user)
else:
box.remove(user)
for(User user: allUsers) {
modify(user); // modifies properties of given user
box.put(user);
}
for(User user: allUsers) {
modify(user); // modifies properties of given user
}
box.put(allUsers);
@Entity()
class Task:
id = Id()
text = String()
date_created = Date(py_type=int)
date_finished = Date(py_type=int)
# Make sure to install objectbox >= 4.0.0
$ pip install --upgrade objectbox
$ ls example
ollama
tasks
vectorsearch-cities
$ cd tasks
$ python main.py
Welcome to the ObjectBox tasks-list app example. Type help or ? for a list of commands.
>
@Override
public void onCreate(Bundle savedInstanceState) {
...
notesBox = ObjectBox.get().boxFor(Note.class);
...
}
public override fun onCreate(savedInstanceState: Bundle?) {
...
notesBox = ObjectBox.boxStore.boxFor()
...
}
class ObjectBox {
late final Store _store;
late final Box<Note> _box;
ObjectBox._create(this._store) {
_noteBox = Box<Note>(_store);
...
class TasklistCmd(Cmd):
# ...
def __init__(self):
# ...
self._store = Store(directory="tasklist-db")
self._task_box = self._store.box(Task)
private void addNote() {
...
Note note = new Note();
note.setText(noteText);
note.setComment(comment);
note.setDate(new Date());
notesBox.put(note);
Log.d(App.TAG, "Inserted new note, ID: " + note.getId());
...
}
private fun addNote() {
...
val note = Note(text = noteText, comment = comment, date = Date())
notesBox.put(note)
Log.d(App.TAG, "Inserted new note, ID: " + note.id)
...
}
Future<void> addNote(String text) => _noteBox.putAsync(Note(text));
def add_task(self, text: str):
task = Task(text=text, date_created=now_ms())
self._task_box.put(task)
OnItemClickListener noteClickListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Note note = notesAdapter.getItem(position);
notesBox.remove(note);
Log.d(App.TAG, "Deleted note, ID: " + note.getId());
...
}
};
private val noteClickListener = OnItemClickListener { _, _, position, _ ->
notesAdapter.getItem(position)?.also {
notesBox.remove(it)
Log.d(App.TAG, "Deleted note, ID: " + it.id)
}
...
}
Future<void> removeNote(int id) => _noteBox.removeAsync(id);
def remove_task(self, task_id: int) -> bool:
is_removed = self._task_box.remove(task_id)
return is_removed
@Override
public void onCreate(Bundle savedInstanceState) {
...
// Query all notes, sorted a-z by their text.
notesQuery = notesBox.query().order(Note_.text).build();
...
}
public override fun onCreate(savedInstanceState: Bundle?) {
...
// Query all notes, sorted a-z by their text.
notesQuery = notesBox.query {
order(Note_.text)
}
...
}
Stream<List<Note>> getNotes() {
// Query for all notes, sorted by their date.
// https://docs.objectbox.io/queries
final builder = _noteBox.query().order(Note_.date, flags: Order.descending);
...
def __init__(self):
# ...
self._query = self._task_box.query().build()
private void updateNotes() {
List<Note> notes = notesQuery.find();
notesAdapter.setNotes(notes);
}
private fun updateNotes() {
val notes = notesQuery.find()
notesAdapter.setNotes(notes)
}
...
// Build and watch the query,
// set triggerImmediately to emit the query immediately on listen.
return builder
.watch(triggerImmediately: true)
// Map it to a list of notes to be used by a StreamBuilder.
.map((query) => query.find());
}
def find_tasks(self):
return self._query.find()
note.setText("This note has changed.");
notesBox.put(note);
note.text = "This note has changed."
notesBox.put(note)
note.text = "This note has changed.";
_noteBox.putAsync(note);
task.text = "This task has changed."
self._task_box.put(task)
@Entity
public class Note {
@Id
long id;
String text;
String comment;
Date date;
...
}
How to rename entities and properties, change property types in ObjectBox.
ObjectBox manages its data model (schema) mostly automatically. The data model is defined by the entity classes you define. When you add or remove entities or properties of your entities, ObjectBox takes care of those changes without any further action from you.
For other changes like renaming or changing the type, ObjectBox needs extra information to make things unambiguous. This is done by setting a unique ID (UIDs) as an annotation, as we will see below.
ObjectBox keeps track of entities and properties by assigning them unique IDs (UIDs). All those UIDs are stored in a file objectbox-models/default.json
(Java, Kotlin) or lib/objectbox-model.json
(Dart) which you should add to your version control system (e.g. git). If you are interested, we have in-depth documentation on UIDs and concepts. But let’s continue with how to rename entities or properties.
In short: To make UID-related changes, put an @Uid
annotation (Java, Kotlin) or @Entity(uid: 0)/@Property(uid: 0)
(Dart) on the entity or property and build the project to get further instructions. Repeat for each entity or property to change.
So why do we need that UID annotation? If you simply rename an entity class, ObjectBox only sees that the old entity is gone and a new entity is available. This can be interpreted in two ways:
The old entity is removed and a new entity should be added, the old data is discarded. This is the default behavior of ObjectBox.
The entity was renamed, the old data should be re-used.
So to tell ObjectBox to do a rename instead of discarding your old entity and data, you need to make sure it knows that this is the same entity and not a new one. You do that by attaching the internal UID to the entity.
The same is true for properties.
Now let’s walk through how to do that. The process works the same if you want to rename a property:
Step 1: Add an empty UID to the entity/property you want to rename:
@Entity
@Uid
public class MyName { ... }
@Entity(uid: 0)
class MyName { ... }
Step 2: Build the project (in Dart, run pub run build_runner build
). The build will fail with an error message that gives you the current UID of the entity/property:
error: [ObjectBox] UID operations for entity "MyName":
[Rename] apply the current UID using @Uid(6645479796472661392L) -
[Change/reset] apply a new UID using @Uid(4385203238808477712L)
@Entity(uid: 0) found on "MyName" - you can choose one of the following actions:
[Rename] apply the current UID using @Entity(uid: 6645479796472661392)
[Change/reset] apply a new UID using @Entity(uid: 4385203238808477712)
Step 3: Apply the UID from the [Rename] section of the error message to your entity/property:
@Entity
@Uid(6645479796472661392L)
public class MyName { ... }
@Entity(uid: 6645479796472661392)
class MyName { ... }
Step 4: The last thing to do is the actual rename on the language level (Java, Kotlin, etc.):
@Entity
@Uid(6645479796472661392L)
public class MyNewName { ... }
@Entity(uid: 6645479796472661392)
class MyNewName { ... }
Step 5: Build the project again, it should now succeed. You can now use your renamed entity/property as expected and all existing data will still be there.
Repeat the steps above to rename another entity or property.
Since in Python there isn't a build step, the steps for renaming an Entity are different. Say we want to rename the Entity MyName
to MyNewName
:
@Entity()
class MyName:
id = Id
some_property = Int
Step 1: Find out the UID of the entity MyName
:
print(MyName._uid)
# 6645479796472661392
Step 2: Rename MyName
to MyNewName
and explicitly specify the old UID:
@Entity(uid=6645479796472661392)
class MyNewName:
id = Id
some_property = Int
This makes possible for Objectbox to associate the old entity to the new one, and retain persisted data. If you don't specify the old UID, Objectbox will discard the old data and add a fresh new entity called MyNameName
to the schema.
ObjectBox does not support migrating existing property data to a new type. You will have to take care of this yourself, e.g. by keeping the old property and adding some migration logic.
There are two solutions to changing the type of a property:
Add a new property with a different name (this only works if the property has no @Uid annotation already):
// old:
String year;
// new:
int yearInt;
Set a new UID for the property so ObjectBox treats it as a new property. Let’s walk through how to do that:
Step 1: Add the @Uid
annotation to the property where you want to change the type:
@Uid
String year;
@Property(uid: 0)
String year;
Step 2: Build the project. The build will fail with an error message that gives you a newly created UID value:
error: [ObjectBox] UID operations for property "MyEntity.year":
[Rename] apply the current UID using @Uid(6707341922395832766L) -
[Change/reset] apply a new UID using @Uid(9204131405652381067L)
@Property(uid: 0) found on "year" - you can choose one of the following actions:
[Rename] apply the current UID using @Property(uid: 6707341922395832766)
[Change/reset] apply a new UID using @Property(uid: 9204131405652381067)
Step 3: Apply the UID from the [Change/reset] section to your property:
@Uid(9204131405652381067L)
int year;
@Property(uid: 9204131405652381067)
String year;
Step 4: Build the project again, it should now succeed. You can now use the property in your entity as if it were a new one.
Repeat the steps above to change the type of another property.
Use existing greenDAO code with ObjectBox and migrate from greenDAO to ObjectBox.
Do you have an existing app that already uses the greenDAO library? We created DaoCompat as a compatibility layer that gives you a greenDAO like API, but behind the scenes uses ObjectBox to store your app's data.
Check out the DaoCompat documentation on how to easily migrate your app to ObjectBox. There is also an example app available.
Feel free to ask for help on Stack Overflow, or if you encounter a bug with DaoCompat create an issue.
@Entity
data class Note(
@Id var id: Long = 0,
var text: String? = null,
var comment: String? = null,
var date: Date? = null
)
git clone https://github.com/objectbox/objectbox-examples.git
cd objectbox-examples/android-app
git clone https://github.com/objectbox/objectbox-examples.git
cd objectbox-examples/android-app-kotlin
git clone https://github.com/objectbox/objectbox-dart.git
cd objectbox-dart/objectbox/examples/flutter/objectbox_demo
git clone https://github.com/objectbox/objectbox-python.git
cd objectbox-python
@Entity()
class Note {
int id;
String text;
String? comment;
DateTime date;
...
}
Discover ObjectBox: The Lightning-Fast Mobile Database for Persistent Object Storage. Streamline Your Workflow, Eliminate Repetitive Tasks, and Enjoy a User-Friendly Data Interface.
ObjectBox tools and dependencies are available on the Maven Central repository.
To add ObjectBox to your Android project, follow these steps:
Open the Gradle build file of your root project (not the ones for your app or module subprojects) and add a global variable for the version and the ObjectBox Gradle plugin:
buildscript {
ext.objectboxVersion = "4.3.0" // For Groovy build scripts
// val objectboxVersion by extra("4.3.0") // For KTS build scripts
repositories {
mavenCentral()
}
dependencies {
// Android Gradle Plugin 8.0 or later supported
classpath("com.android.tools.build:gradle:8.0.2")
classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")
}
}
Open the Gradle build file for your app or module subproject and, after the com.android.application
plugin, apply the io.objectbox
plugin:
// Using plugins syntax:
plugins {
id("com.android.application")
id("kotlin-android") // Only for Kotlin projects
id("kotlin-kapt") // Only for Kotlin projects
id("io.objectbox") // Apply last
}
// Or using the old apply syntax:
apply plugin: "com.android.application"
apply plugin: "kotlin-android" // Only for Kotlin projects
apply plugin: "kotlin-kapt" // Only for Kotlin projects
apply plugin: "io.objectbox" // Apply last
Then do "Sync Project with Gradle Files" in Android Studio so the Gradle plugin automatically adds the required ObjectBox libraries and code generation tasks.
Your project can now use ObjectBox, continue by defining entity classes.
The ObjectBox Java SDK and runtime libraries support applications:
running on the JVM on Linux (x86_64, arm64, armv7), Windows (x86_64) and macOS 10.15 or newer (x86_64, Apple M1)
written in Java or Kotlin
targeting at least Java 8
built with Gradle or Maven
ObjectBox tools and dependencies are available on the Maven Central repository.
Maven projects
To set up a Maven project, see the README of the Java Maven example project.
Gradle projects
Open the Gradle build script of your root project and
add a global variable to store the common version of ObjectBox dependencies and
add the ObjectBox Gradle plugin:
buildscript {
ext.objectboxVersion = "4.3.0" // For Groovy build scripts
// val objectboxVersion by extra("4.3.0") // For KTS build scripts
repositories {
mavenCentral()
}
dependencies {
classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion")
}
}
Open the Gradle build file for your application subproject and, after other plugins, apply the io.objectbox
plugin:
// Using plugins syntax:
plugins {
id("java-library") // or org.jetbrains.kotlin.jvm for Kotlin projects.
id("io.objectbox") // Apply last.
}
// Or using the old apply syntax:
apply plugin: "java-library" // or org.jetbrains.kotlin.jvm for Kotlin projects.
apply plugin: "io.objectbox" // Apply last.
Optionally, add a runtime library for each platform that your application should run on and instead apply the Gradle plugin after the dependencies block:
dependencies {
// ObjectBox platform-specific runtime libraries
// Add or remove them as needed to match what your application supports
// Linux (x64)
implementation("io.objectbox:objectbox-linux:$objectboxVersion")
// macOS (Intel and Apple Silicon)
implementation("io.objectbox:objectbox-macos:$objectboxVersion")
// Windows (x64)
implementation("io.objectbox:objectbox-windows:$objectboxVersion")
// Additional ObjectBox runtime libraries
// Linux (32-bit ARM)
implementation("io.objectbox:objectbox-linux-arm64:$objectboxVersion")
// Linux (64-bit ARM)
implementation("io.objectbox:objectbox-linux-armv7:$objectboxVersion")
}
// When manually adding ObjectBox dependencies, the plugin must be
// applied after the dependencies block so it can detect them.
// Using Groovy build scripts
apply plugin: "io.objectbox"
// Using KTS build scripts
apply(plugin = "io.objectbox")
Your project can now use ObjectBox, continue by defining entity classes.
To add ObjectBox to your Flutter project:
Run these commands:
flutter pub add objectbox objectbox_flutter_libs:any
flutter pub add --dev build_runner objectbox_generator:any
Or to use ObjectBox Sync (requires access to the Sync feature) instead run:
flutter pub add objectbox objectbox_sync_flutter_libs:any
flutter pub add --dev build_runner objectbox_generator:any
This should add lines like this to your pubspec.yaml
:
dependencies:
objectbox: ^4.3.0
objectbox_flutter_libs: any
# If you run the command for ObjectBox Sync it should add instead:
# objectbox_sync_flutter_libs: any
dev_dependencies:
build_runner: ^2.4.11
objectbox_generator: any
If you added the above lines manually, then install the packages with flutter pub get
.
Run these commands:
dart pub add objectbox
dart pub add --dev build_runner objectbox_generator:any
This should add lines like this to your pubspec.yaml
:
dependencies:
objectbox: ^4.3.0
dev_dependencies:
build_runner: ^2.4.11
objectbox_generator: any
If you added the above lines manually, then install the packages with dart pub get
Install the ObjectBox C library for your system (on Windows you can use "Git Bash"):
bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh)
Or to use ObjectBox Sync (requires access to the Sync feature) instead run:
bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh) --sync
Deploying Dart Native projects
Natively compiled Dart applications that use ObjectBox Dart require a reference to the objectbox-c library. Hence, the shared library file downloaded with install.sh
needs to be shipped with the executable.
The install.sh
script downloads the library by default to the lib
subdirectory of the working directory. An executable using ObjectBox Dart looks for the library in this lib
directory.
If it is not found there, it falls back to using system directories (using Dart's DynamicLibrary.open
):
Windows: working directory and %WINDIR%\system32
.
macOS: /usr/local/lib
(and maybe others).
Linux: /lib
and /usr/lib
(again, possibly others).
ObjectBox for Python is available via PyPI: Stable Version (4.0.0):
pip install --upgrade objectbox
Define your data model by creating a class with at least an ID property, a so called entity.
A simple entity representing a user with an ID and a name property could look like this:
@Entity
public class User {
@Id
public long id;
public String name;
}
@Entity
data class User(
@Id
var id: Long = 0,
var name: String? = null
)
When using a data class, add default values for all parameters. This will ensure your data class will have a constructor that can be called by ObjectBox. (Technically this is only required if adding properties to the class body, like custom or transient properties or relations, but it's a good idea to do it always.)
Avoid naming properties like reserved Java keywords, like private
and default
. ObjectBox tooling works with the Java representation of your Kotlin code to be compatible with both Java and Kotlin. It will ignore such properties.
@Entity()
class User {
@Id()
int id = 0;
String? name;
}
You can have multiple entities in the same file (here models.dart
), or you can have them spread across multiple files in your package's lib
directory.
from objectbox import Entity, Id, String
@Entity()
class User:
id = Id
name = String
Important:
Entities must have exactly one 64-bit integer ID property (a Java long
, Kotlin Long
, Dart int
). If you need another type for the ID, like a string, see the @Id annotation docs for some tips. Also, the ID property must have non-private visibility (or non-private getter and setter methods).
Entities must also have a no-argument constructor, or for better performance, a constructor with all properties as arguments. In the above examples, a default, no-argument constructor is generated by the compiler.
Support for many property types is already built-in, but almost any type can be stored with a converter.
For more details about entities, like how to create an index or a relation, check the Entity Annotations page.
You can also learn more about the ObjectBox model.
Next, we generate some binding code based on the model defined in the previous step.
Build your project to generate the MyObjectBox
class and other classes required to use ObjectBox, for example using Build > Make Project in Android Studio.
To change the package of the MyObjectBox
class, see the annotation processor options on the Advanced Setup page.
To generate the binding code required to use ObjectBox run
dart run build_runner build
ObjectBox generator will look for all @Entity
annotations in your lib
folder and create
a single database definition lib/objectbox-model.json
and
supporting code in lib/objectbox.g.dart
.
To customize the directory where generated files are written see Advanced Setup.
You typically commit the generated code file objectbox.g.dart
to your version control system (e.g. git) to avoid having to re-run the generator unless there are changes.
Among other files ObjectBox generates a JSON model file, by default to
app/objectbox-models/default.json
for Android projects,
lib/objectbox-model.json
for Dart/Flutter projects, or
<user-module-dir>/objectbox-model.json
for Python projects
To change the model file path, see Advanced Setup.
This JSON file changes when you change your entity classes (or sometimes with a new version of ObjectBox).
Keep this JSON file, commit the changes to version control!
This file keeps track of unique IDs assigned to your entities and properties. This ensures that an older version of your database can be smoothly upgraded if your entities or properties change.
The model file also enables you to keep data when renaming entities or properties or to resolve conflicts when two of your developers make changes at the same time.
BoxStore (Java) or Store (Dart) is the entry point for using ObjectBox. It is the direct interface to the database and manages Boxes. Typically, you want to only have a single Store (single database) and keep it open while your app is running, not closing it explicitly.
Create it using the builder returned by the generated MyObjectBox
class, for example in a small helper class like this:
public class ObjectBox {
private static BoxStore store;
public static void init(Context context) {
store = MyObjectBox.builder()
.androidContext(context)
.build();
}
public static BoxStore get() { return store; }
}
If you encounter UnsatisfiedLinkError
or LinkageError
on the build call, see App Bundle, split APKs and Multidex for solutions.
The best time to initialize ObjectBox is when your app starts. We suggest to do it in the onCreate
method of your Application class:
public class ExampleApp extends Application {
@Override
public void onCreate() {
super.onCreate();
ObjectBox.init(this);
}
}
Create it using the builder returned by the generated MyObjectBox class, for example in a small helper class like this:
object ObjectBox {
lateinit var store: BoxStore
private set
fun init(context: Context) {
store = MyObjectBox.builder()
.androidContext(context)
.build()
}
}
If you encounter UnsatisfiedLinkError
or LinkageError
on the build call, see App Bundle, split APKs and Multidex for solutions.
The best time to initialize ObjectBox is when your app starts. We suggest to do it in the onCreate
method of your Application class:
class ExampleApp : Application() {
override fun onCreate() {
super.onCreate()
ObjectBox.init(this)
}
}
public class ObjectBox {
private static BoxStore store;
public static void init(Context context) {
store = MyObjectBox.builder()
.name("objectbox-notes-db")
.build();
}
public static BoxStore get() { return store; }
}
The best time to initialize ObjectBox is when your app starts. For a command line app this is typically inside the main method.
Create it using the generated openStore()
method, for example in a small helper class like this:
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'objectbox.g.dart'; // created by `flutter pub run build_runner build`
class ObjectBox {
/// The Store of this app.
late final Store store;
ObjectBox._create(this.store) {
// Add any additional setup code, e.g. build queries.
}
/// Create an instance of ObjectBox to use throughout the app.
static Future<ObjectBox> create() async {
final docsDir = await getApplicationDocumentsDirectory();
// Future<Store> openStore() {...} is defined in the generated objectbox.g.dart
final store = await openStore(directory: p.join(docsDir.path, "obx-example"));
return ObjectBox._create(store);
}
}
The best time to initialize ObjectBox is when your app starts. We suggest to do it in your app's main()
function:
/// Provides access to the ObjectBox Store throughout the app.
late ObjectBox objectbox;
Future<void> main() async {
// This is required so ObjectBox can get the application directory
// to store the database in.
WidgetsFlutterBinding.ensureInitialized();
objectbox = await ObjectBox.create();
runApp(MyApp());
}
Create it using the generated openStore()
method, for example like this:
import 'objectbox.g.dart'; // created by `dart pub run build_runner build`
void main() {
// Store openStore() {...} is defined in the generated objectbox.g.dart
final store = openStore();
// your app code ...
store.close(); // don't forget to close the store
}
The above minimal example omits the argument to (directory: )
, using the default - ./objectbox
- in the current working directory.
from objectbox import Store
store = Store()
It is possible to specify various options when building a store. Notably for testing or caching, to use an in-memory database that does not create any files:
BoxStore inMemoryStore = MyObjectBox.builder()
.androidContext(context)
.inMemory("test-db")
.build();
final inMemoryStore =
Store(getObjectBoxModel(), directory: "memory:test-db");
store = Store(directory="memory:testdata")
For more store configuration options: for Java see the BoxStoreBuilder and for Dart the Store documentation. (Python APIs will be published soon)
The Box class is likely the class you interact with most. A Box instance gives you access to objects of a particular type. For example, if you have User
and Order
entities, you need a Box object to interact with each:
Box<User> userBox = store.boxFor(User.class);
Box<Order> orderBox = store.boxFor(Order.class);
val userBox = store.boxFor(User::class)
val orderBox = store.boxFor(Order::class)
final userBox = store.box<User>();
final orderBox = store.box<Order>();
user_box = store.box(User)
order_box = store.box(Order)
These are some of the operations offered by the Box class:
put inserts a new object or updates an existing one (with the same ID). When inserting, an ID will be assigned to the just inserted object (this will be explained below) and returned. put
also supports putting multiple objects, which is more efficient.
User user = new User("Tina");
userBox.put(user);
List<User> users = getNewUsers();
userBox.put(users);
val user = User(name = "Tina")
userBox.put(user)
val users: List<User> = getNewUsers()
userBox.put(users)
final user = User(name: 'Tina');
userBox.put(user);
final users = getNewUsers();
userBox.putMany(users);
user = User(name="Tina")
user_box.put(user)
users = get_new_users()
user_box.put(*users)
get and getAll: Given an object’s ID, get
reads it from its box. To get all objects in the box use getAll
.
User user = userBox.get(userId);
List<User> users = userBox.getAll();
val user = userBox[userId]
val users = userBox.all
final user = userBox.get(userId);
final users = userBox.getMany(userIds);
final users = userBox.getAll();
user = user_box.get(user_id)
users = user_box.get_all()
query: Starts building a query to return objects from the box that match certain conditions. See queries for details.
Query<User> query = userBox
.query(User_.name.equal("Tom"))
.order(User_.name)
.build();
List<User> results = query.find();
query.close();
val query = userBox
.query(User_.name.equal("Tom"))
.order(User_.name)
.build()
val results = query.find()
query.close()
final query =
(userBox.query(User_.name.equals('Tom'))..order(User_.name)).build();
final results = query.find();
query.close();
query = user_box \
.query(User.name.equals('Tom')) \
.build()
results = query.find()
remove and removeAll: Remove a previously put object from its box (deletes it). remove
also supports removing multiple objects, which is more efficient. removeAll
removes (deletes) all objects in a box.
boolean isRemoved = userBox.remove(userId);
userBox.remove(users);
// alternatively:
userBox.removeByIds(userIds);
userBox.removeAll();
val isRemoved = userBox.remove(userId)
userBox.remove(users)
// alternatively:
userBox.removeByIds(userIds)
userBox.removeAll()
final isRemoved = userBox.remove(userId);
userBox.removeMany(userIds);
userBox.removeAll();
is_removed = user_box.remove(user_id)
user_box.remove_all()
count: Returns the number of objects stored in this box.
long userCount = userBox.count();
val userCount = userBox.count()
final userCount = userBox.count();
user_box.count()
For a complete list of methods available in the Box class, check the API reference documentation for Java or Dart.
ObjectBox has built-in support to run (typically multiple or larger) database operations asynchronously.
runInTxAsync and callInTxAsync: runs the given Runnable/Callable in a transaction on a background thread (the internal ObjectBox thread pool) and calls the given callback once done. In case of callInTxAsync the callback also receives the returned result.
store.callInTxAsync(() -> {
Box<User> box = store.boxFor(User.class);
String name = box.get(userId).name;
box.remove(userId);
return text;
}, (result, error) -> {
if (error != null) {
System.out.println("Failed to remove user with id " + userId);
} else {
System.out.println("Removed user with name: " + result);
}
});
awaitCallInTx (Kotlin Coroutines only): wraps callInTxAsync in a coroutine that suspends until the transaction has completed. Likewise, on success the return value of the given callable is returned, on failure an exception is thrown.
try {
val name = store.awaitCallInTx {
val box = store.boxFor(User::class.java)
val name = box.get(userId).name
box.remove(userId)
name
}
println("Removed user with name $name")
} catch (e: Exception) {
println("Failed to remove user with id $userId")
}
Most Box methods do have async versions which run the operation in a worker isolate.
For example putAsync: asynchronously inserts a new object or updates an existing one (with the same ID). The returned future completes when the object is successfully written to the database.
final user = User(name: 'Tina');
Future<int> idFuture = userBox.putAsync(user);
...
final id = await idFuture;
userBox.get(id); // after the future completed, the object is inserted
To run multiple operations, it is more efficient to wrap the synchronous calls in an asynchronous transaction with runInTransactionAsync (API reference): run a callback with multiple database operations within a write or read transaction in the background without blocking the user interface. Can return results.
// The callback must be a function that can be sent to an isolate:
// either a top-level function, static method or a closure that only
// captures objects that can be sent to an isolate.
String? readNameAndRemove(Store store, int objectId) {
var box = store.box<User>();
final nameOrNull = box.get(objectId)?.name;
box.remove(objectId);
return nameOrNull;
}
final nameOrNull =
await store.runInTransactionAsync(TxMode.write, readNameAndRemove, objectId);
There is also runAsync (API reference): like runInTransactionAsync but does not start a transaction, leaving that to your callback code. This allows to supply a callback that is an async function.
If it is necessary to call put many times in a row, take a look at putQueued: Schedules the given object to be put later on, by an asynchronous queue, returns the id immediately even though the object may not have been written yet. You can use Store's awaitQueueCompletion()
or awaitQueueSubmitted()
to wait for the async queue to finish.
for (int i = 0; i < 100; i++) {
userBox.putQueued(User(name: 'User $i'));
}
// Optional: wait until submitted items are processed.
store.awaitQueueSubmitted();
expect(userBox.count(), equals(100));
By default IDs for new objects are assigned by ObjectBox. When a new object is put, it will be assigned the next highest available ID:
User user = new User();
// user.id == 0
box.put(user);
// user.id != 0
long id = user.id;
val user = User()
// user.id == 0
box.put(user)
// user.id != 0
val id = user.id
final user = User();
// user.id == 0
box.put(user);
// user.id != 0
final id = user.id;
user = User()
box.put(user)
id: int = user.id
For example, if there is an object with ID 1 and another with ID 100 in a box, the next new object that is put will be assigned ID 101.
If you try to assign a new ID yourself and put the object, ObjectBox will throw an error.
Object IDs can not be:
0
(zero) or null
(if using java.lang.Long) As said above, when putting an object with ID zero it will be assigned an unused ID (not zero).
0xFFFFFFFFFFFFFFFF
(-1 in Java) Reserved for internal use.
For a detailed explanation see the page on Object IDs.
While ObjectBox offers powerful transactions, it is sufficient for many apps to consider just some basics guidelines about transactions:
A put
runs an implicit transaction.
Prefer put
bulk overloads for lists (like put(entities)
) when possible.
For a high number of DB interactions in loops, consider explicit transactions, such as using runInTx()
.
For more details check the separate transaction documentation.
DaoCompat is a compatibility layer that gives you a greenDAO like API for ObjectBox. It makes switching from greenDAO to ObjectBox simple. Have a look at the documentation and the example. Contact us if you have any questions!
Check out the ObjectBox example projects on GitHub.
Learn how to write unit tests.
To enable debug mode and for advanced use cases, see the Advanced Setup page.
The Android Paging Library helps you load and display small data chunks at a time. Learn to use ObjectBox database with the Paging library from Android Architecture Components.
ObjectBox supports integration with the that is part of Google's . To that end, the (objectbox-android
) provides the ObjectBoxDataSource
class. It is an implementation of the Paging library's .
Within your ViewModel
, similar to , you first . But then, you construct an ObjectBoxDataSource
factory with it instead. This factory is then passed to a LivePagedListBuilder
to build the actual LiveData
.
Here is an example of a ViewModel
class doing just that:
Note that the LiveData
holds your entity class, here Note
, wrapped inside a PagedList
. You observe the LiveData
as usual in your activity or fragment, then submit the PagedList
on changes to your PagedListAdapter
of the Paging library.
We will not duplicate how this works here, see the for details about this.
Have a look at the .
Check out .
Learn how to .
public class NotePagedViewModel extends ViewModel {
private LiveData<PagedList<Note>> noteLiveDataPaged;
public LiveData<PagedList<Note>> getNoteLiveDataPaged(Box<Note> notesBox) {
if (noteLiveDataPaged == null) {
// query all notes, sorted a-z by their text
Query<Note> query = notesBox.query().order(Note_.text).build();
// build LiveData
noteLiveDataPaged = new LivePagedListBuilder<>(
new ObjectBoxDataSource.Factory<>(query),
20 /* page size */
).build();
}
return noteLiveDataPaged;
}
}
LiveData is an observable data holder class. Learn to use ObjectBox database with LiveData from Android Architecture Components.
As an alternative to ObjectBox’ data observers and reactive queries, you can opt for the LiveData approach supplied by Android Architecture Components. ObjectBox comes with ObjectBoxLiveData
, a class that can be used inside your ViewModel classes.
A simple ViewModel implementation for our note example app includes the special ObjectBoxLiveData
that is constructed using a regular ObjectBox query:
public class NoteViewModel extends ViewModel {
private ObjectBoxLiveData<Note> noteLiveData;
public ObjectBoxLiveData<Note> getNoteLiveData(Box<Note> notesBox) {
if (noteLiveData == null) {
// query all notes, sorted a-z by their text
noteLiveData = new ObjectBoxLiveData<>(notesBox.query().order(Note_.text).build());
}
return noteLiveData;
}
}
Note that we did choose to pass the box to getNoteLiveData()
. Instead you could use AndroidViewModel
, which provides access to the Application
context, and then call ((App)getApplication()).getBoxStore().boxFor()
inside the ViewModel. However, the first approach has the advantage that our ViewModel has no reference to Android classes. This makes it easier to unit test.
Now, when creating the activity or fragment we get the ViewModel, access its LiveData and finally register to observe changes:
NoteViewModel model = ViewModelProviders.of(this).get(NoteViewModel.class);
model.getNoteLiveData(notesBox).observe(this, new Observer<List<Note>>() {
@Override
public void onChanged(@Nullable List<Note>; notes) {
notesAdapter.setNotes(notes);
}
});
The ObjectBoxLiveData will now subscribe to the query and notify observers when the results of the query change, if there is at least one observer. In this example the activity is notified if a note is added or removed. If all observers are destroyed, the LiveData will cancel the subscription to the query.
If you have used ObjectBox observers in the past this might sound familiar. Well, because it is! ObjectBoxLiveData just wraps a DataObserver on the query you give to it.
Solutions for common issues with ObjectBox for Java and Dart
MyObjectBox is a generated class, so make sure the project builds successfully. Resolve any other build errors.
Check that the project is configured correctly. E.g. for Kotlin, check that the kapt plugin is applied (apply plugin: 'kotlin-kapt'
) before the ObjectBox plugin.
android-apt
is known to cause problems, try to remove it.
If your team makes concurrent modifications to the data model (e.g. adding/removing entities or properties) it may clash with your changes. Read the meta model docs on how to resolve the conflicts.
Creating a Store throws a DbSchemaException
and a message like
Incoming entity ID does not match existing UID
Incoming property ID does not match existing UID
Incoming index ID does not match existing UID
This means there is a conflict between the data model defined in your code (using @Entity
classes) and the data model of the existing database file.
For example for an entity, this message indicates the unique identifier (UID) of an entity is not the same as before. Entity UIDs are auto-assigned by ObjectBox and stored in objectbox-models/default.json
for Java or lib/objectbox-model.json
for Dart. Look for the id
property which contains the UID in the second part, the first part is the ID (the format is ID:UID
).
Read the meta model docs on why this can happen and how to resolve such conflicts.
See below.
Creating a Store throws a DbSchemaException
and a message like
DB's last entity ID is higher than from model
DB's last property ID is higher than the incoming one
DB's last index ID is higher than from model
DB's last relation ID is higher than from model
This means, there is a conflict between the data model defined in your code (using @Entity
classes) and the data model of the existing database file.
For example for an entity, this message indicates that the database already contains one or more entities with IDs higher than the lowest one of the model in your code. Entity IDs are auto-assigned by ObjectBox and stored in objectbox-models/default.json
for Java or lib/objectbox-model.json
for Dart. Look for the lastEntityId
value, the highest used entity ID is contained in the first part, the second part is the UID (the format is ID:UID
).
Read the meta model docs on why this can happen and how to resolve such conflicts.
This is thrown when applying a transaction (e.g. putting an object) would exceed the maxSizeInKByte (Java)/maxDBSizeInKB (Dart) configured for the store.
By default, this is 1 GB, which should be sufficient for most applications. In general, a maximum size prevents the database from growing indefinitely when something goes wrong (for example data is put in an infinite loop).
This value can be changed, so increased or also decreased, each time when opening a store:
MyObjectBox.builder()
.androidContext(context)
.maxSizeInKByte(1024 * 1024 /* 1 GB */)
.build()
Store(getObjectBoxModel(), maxDBSizeInKB: 1024 * 1024 /* 1 GB */);
This can have various reasons. In general check your ABI filter setup or add one in your Gradle build file.
If your app explicitly ships code for "armeabi": For Android, ObjectBox comes with binaries for “armeabi-v7a” and “arm64-v8a” ABIs. We consider “armeabi” to be outdated and thus do not support it. Check if you have a Gradle config like abiFilters "armeabi", which is causing the problem (e.g. remove it or change it to “armeabi-v7a”).
If your app uses split APKs or App Bundle: some users might have sideloaded your APK that includes the library for a platform that is incompatible with the one of their device. See App Bundle, split APKs and Multidex for workarounds.
If you are doing Android instrumentation (especially with Espresso), you may get a warning like this:
Error:Conflict with dependency ‘com.google.code.findbugs:jsr305’ in project ‘:app’. Resolved versions for app (3.0.2) and test app (2.0.1) differ. See
http://g.co/androidstudio/app-test-app-conflict
for details.
You can easily resolve the version conflict by adding this Gradle dependency: androidTestCompile 'com.google.code.findbugs:jsr305:3.0.2'
Check the data model migration guide if you get an exception like:
io.objectbox.exception.DbException: Property […] is not compatible to its previous definition. Check its type.
or
Cannot change the following flags for Property
Only 64-bit iOS devices are supported. To resolve the build error, configure Architectures in your Xcode project like described in Getting Started for Flutter.
Sometimes you might get an error code along with a error message. Typically, you will find error codes in parenthesis. Example:
Could not prepare directory: objectbox (30)
This error code comes from the OS, and gives you additional information; e.g. 30 tells that you tried to init a database in a read-only location of the file system, which cannot work.
Here's the list of typical OS errors:
Error code
Errno
Description
1
EPERM
Operation not permitted
2
ENOENT
No such file or directory
3
ESRCH
No such process
4
EINTR
Interrupted system call
5
EIO
I/O error
6
ENXIO
No such device or address
7
E2BIG
Argument list too long
8
ENOEXEC
Exec format error
9
EBADF
Bad file number
10
ECHILD
No child processes
11
EAGAIN
Try again
12
ENOMEM
Out of memory
13
EACCES
Permission denied
14
EFAULT
Bad address
15
ENOTBLK
Block device required
16
EBUSY
Device or resource busy
17
EEXIST
File exists
18
EXDEV
Cross-device link
19
ENODEV
No such device
20
ENOTDIR
Not a directory
21
EISDIR
Is a directory
22
EINVAL
Invalid argument
23
ENFILE
File table overflow
24
EMFILE
Too many open files
25
ENOTTY
Not a typewriter
26
ETXTBSY
Text file busy
27
EFBIG
File too large
28
ENOSPC
No space left on device
29
ESPIPE
Illegal seek
30
EROFS
Read-only file system
31
EMLINK
Too many links
32
EPIPE
Broken pipe
33
EDOM
Math argument out of domain of func
34
ERANGE
Math result not representable
If you believe to have found a bug or missing feature, please create an issue.
For Java/Kotlin: https://github.com/objectbox/objectbox-java/issues
For Flutter/Dart: https://github.com/objectbox/objectbox-dart/issues
If you have a usage question regarding ObjectBox, please post on Stack Overflow. https://stackoverflow.com/questions/tagged/objectbox
ObjectBox fully supports Kotlin for Android. Learn what to look out for when using ObjectBox with Kotlin, how to use the built-in Kotlin extension functions.
ObjectBox comes with full Kotlin support for Android. This allows entities to be modeled in Kotlin classes (regular and data classes). With Kotlin support you can build faster apps even faster.
ObjectBox supports regular and data classes for entities. However, @Id
properties must be var (not val) because ObjectBox assigns the ID after putting a new entity. They also should be of non-null type Long
with the special value of zero for marking entities as new.
Can sealed classes be entities? Not directly. Sealed classes are abstract and can't be instantiated. But subclasses of a sealed class should work.
To learn how to create entities, look at these pages:
When defining relations in Kotlin, keep in mind that relation properties must be var
. Otherwise they can not be initialized as described in the relations docs. To avoid null checks use a lateinit modifier. When using a data class this requires the relation property to be moved to the body.
For non-Android projects, i.e. if you are using Kotlin for desktop apps, there's an additional setup for for entities necessary, please see https://docs.objectbox.io/relations#initialization-magic for details. In the future, we hope to eliminate this requirement.
See the Relations page for examples.
To simplify your code, you might want to use the Kotlin extension functions provided by ObjectBox. The library containing them is added automatically if the Gradle plugin detects a Kotlin project.
To add it manually, modify the dependencies section in your app's build.gradle
file:
dependencies {
implementation("io.objectbox:objectbox-kotlin:$objectboxVersion")
}
Now have a look at what is possible with the extensions compared to standard Kotlin idioms:
Get a box:
// Regular:
val box = store.boxFor(DataClassEntity::class.java)
// With extension:
val box: Box<DataClassEntity> = store.boxFor()
Build a query:
// Regular:
val query = box.query().run {
equal(property, value)
order(property)
build()
}
// With extension:
val query = box.query {
equal(property, value)
order(property)
}
Use the in filter of a query:
// Regular:
val query = box.query().`in`(property, array).build()
// With extension:
val query = box.query().inValues(property, array).build()
Modify a ToMany:
// Regular:
toMany.apply {
reset()
add(entity)
removeById(id)
applyChangesToDb()
}
// With extension:
toMany.applyChangesToDb(resetFirst = true) { // default is false
add(entity)
removeById(id)
}
Get a Flow from a Box or Query subscription (behind the scenes this is based on a Data Observer):
// Listen to all changes to a Box
val flow = store.subscribe(TestEntity::class.java).toFlow()
// Get the latest query results on any changes to a Box
val flow = box.query().subscribe().toFlow()
Something missing? Let us know what other extension functions you want us to add.
To run Box operations on a separate Dispatcher wrap them using withContext
:
suspend fun putNote(
note: Note,
dispatcher: CoroutineDispatcher
) = withContext(dispatcher) {
boxStore.boxFor(Note::class.java).put(note)
}
BoxStore provides an async API to run transactions. There is an extension function available that wraps it in a coroutine:
// Calls callInTxAsync behind the scenes.
val id = boxStore.awaitCallInTx {
box.put(Note("Hello", 1))
}
Check out the Kotlin example on GitHub.
Continue with Getting Started.
Besides Android apps, ObjectBox for Java supports desktop apps running on Linux, macOS and Windows written in Java or Kotlin. See how to build and test desktop apps using ObjectBox.
Just like on Android, ObjectBox stands for a super simple API and high performance. It’s designed for objects and outperforms other database and ORM solutions. Because it is an embedded database, ObjectBox runs in your apps’ process and needs no maintenance. Read on to learn how to create a Java project using ObjectBox. We believe it’s fairly easy. Please let us know your thoughts on it.
See the Getting Started page on how to set up your project, add entities and use the ObjectBox APIs.
There are example command line apps available in our examples repository.
The setup and writing tests is identical to writing unit tests that run on the local JVM for Android, see Android Local Unit Tests.
Which types are supported by default in ObjectBox, how to store types that are not, recommendations for storing enums.
With ObjectBox you can store pretty much any type (class), given that it can be converted to any of the built-in types.
ObjectBox can store the following built-in types without a converter:
boolean, Boolean
int, Integer
short, Short
long, Long
float, Float
double, Double
byte, Byte
char, Character
String
// Strings
String[]
List<String>
Map<String, String>
boolean[]
// integer arrays
byte[]
char[]
short[]
int[]
long[]
// floating point arrays
float[]
double[]
// Stored as time (long) with millisecond precision.
java.util.Date
// Stored as time (long) with nanosecond precision.
@Type(DatabaseType.DateNano) long, Long
// Flex properties, see important notes below
Object
Map<String, Object>
List<Object>
// The nullable variants are supported as well
Boolean
Int
Short
Long
Float
Double
Byte
Char
String
// Kotlin unsigned integer types
// https://kotlinlang.org/docs/unsigned-integer-types.html
// are stored as their signed Java equivalent.
// These are just Kotlin inline value classes, so to make their
// getter visible to Java, need to annotate them with @JvmName.
// https://kotlinlang.org/docs/inline-classes.html#calling-from-java-code
@get:JvmName("getUnsignedByte")
var unsignedByte: UByte = 0u
@get:JvmName("getUnsignedShort")
var unsignedShort: UShort = 0u
@get:JvmName("getUnsignedInt")
var unsignedInt: UInt = 0u
@get:JvmName("getUnsignedLong")
var unsignedLong: ULong = 0u
// The same applies for other Kotlin inline value classes
@JvmInline
value class Custom(val i: Long)
@get:JvmName("getCustom")
var custom: Custom = Custom(0)
// Strings
Array<String>
MutableList<String>
MutableMap<String, String>
BooleanArray
// integer arrays
ByteArray
CharArray
ShortArray
IntArray
LongArray
// floating point arrays
FloatArray
DoubleArray
// Stored as time (Long) with millisecond precision.
java.util.Date
// Stored as time (Long) with nanosecond precision.
@Type(DatabaseType.DateNano) Long?
// Flex properties, see important notes below
Any
MutableMap<String, Any>
MutableList<Any>
// all fields are supported as both nullable and non-nullable
bool
int // 64-bit, see below to store as smaller integer
double // 64-bit, see below to store as smaller floating-point
String
List<String>
// Time with millisecond precision.
// Note: always restored in default time zone.
@Property(type: PropertyType.date)
DateTime date;
// Time with millisecond precision restored in UTC time zone.
@Transient()
DateTime utcDate;
int get dbUtcDate => utcDate.millisecondsSinceEpoch;
set dbUtcDate(int value) {
utcDate = DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
}
// Time with nanosecond precision.
@Property(type: PropertyType.dateNano)
DateTime nanoDate;
// integer
@Property(type: PropertyType.byte)
int byte; // 8-bit
@Property(type: PropertyType.short)
int short; // 16-bit
@Property(type: PropertyType.char)
int char; // 16-bit unsigned
@Property(type: PropertyType.int)
int int32; // 32-bit
int int64; // 64-bit
// floating point
@Property(type: PropertyType.float)
double float; // 32-bit
double float64; // 64-bit
// 8-bit integer vector
@Property(type: PropertyType.byteVector)
List<int> byteList;
Int8List int8List;
Uint8List uint8List;
// 16-bit unsigned integer vector
@Property(type: PropertyType.charVector)
List<int>? charList;
// 16-bit integer vector
@Property(type: PropertyType.shortVector)
List<int>? shortList;
Int16List? int16List;
Uint16List? uint16List;
// 32-bit integer vector
@Property(type: PropertyType.intVector)
List<int>? intList;
Int32List? int32List;
Uint32List? uint32List;
// 64-bit integer vector
List<int>? longList;
Int64List? int64List;
Uint64List? uint64List;
// 32-bit floating point vector
@Property(type: PropertyType.floatVector)
List<double>? floatList;
Float32List? float32List;
// 64-bit floating point vector
List<double>? doubleList;
Float64List? float64List;
Bool
Int8
Int16
Int32
Int64
Float32
Float64
Bytes
String
BoolVector
Int8Vector
Int16Vector
Int32Vector
Int64Vector
Float32Vector
Float64Vector
CharVector
BoolList
Int8List
Int16List
Int32List
Int64List
Float32List
Float64List
CharList
Date
DateNano
Flex
ObjectBox supports properties where the type is not known at compile time using Object
in Java or Any?
in Kotlin. These "flex properties" can store types like integers, floating point values, strings and byte arrays. Or lists and maps (using string keys) of those. In the database these properties are stored as byte arrays.
Some important limitations apply, see the FlexObjectConverter class documentation for details.
@Entity
public class Customer {
@Id long id;
// Stores any supported type at runtime
@Nullable Object tag;
// Or explicitly use a String map
@Nullable Map<String, Object> stringMap;
// Or a list
@Nullable List<Object> flexList;
public Customer(Object tag) {
this.id = 0;
this.tag = tag;
}
public Customer() {} // For ObjectBox
// TODO getters and setters
}
Customer customerStrTag = new Customer("string-tag");
Customer customerIntTag = new Customer(1234);
box.put(customerStrTag, customerIntTag);
@Entity
data class Customer(
@Id var id: Long = 0,
// Stores any supported type at runtime
var tag: Any? = null,
// Or explicitly use a String map
var stringMap: MutableMap<String, Any?>? = null
// Or a list
var flexList: MutableList<Any?>? = null
)
val customerStrTag = Customer(tag = "string-tag")
val customerIntTag = Customer(tag = 1234)
box.put(customerStrTag, customerIntTag)
To override the default converter chosen by ObjectBox, use @Convert
. For example to use another built-in FlexObjectConverter
subclass:
// StringLongMapConverter restores any integers always as Long
@Convert(converter = StringLongMapConverter.class, dbType = byte[].class)
@Nullable Map<String, Object> stringMap;
// StringLongMapConverter restores any integers always as Long
@Convert(converter = StringLongMapConverter::class, dbType = ByteArray::class)
var stringMap: MutableMap<String, Any>? = null
You can also write a custom converter like shown below.
To add support for a custom type, you need to provide a conversion to one of the ObjectBox built-in types. For example, you could define a color in your entity using a custom Color
class and map it to an Integer
. Or you can map the popular org.joda.time.DateTime
from Joda Time to a Long
.
Here is an example mapping an enum
to an integer:
@Entity
public class User {
@Id
public long id;
@Convert(converter = RoleConverter.class, dbType = Integer.class)
public Role role;
public enum Role {
DEFAULT(0), AUTHOR(1), ADMIN(2);
final int id;
Role(int id) {
this.id = id;
}
}
public static class RoleConverter implements PropertyConverter<Role, Integer> {
@Override
public Role convertToEntityProperty(Integer databaseValue) {
if (databaseValue == null) {
return null;
}
for (Role role : Role.values()) {
if (role.id == databaseValue) {
return role;
}
}
return Role.DEFAULT;
}
@Override
public Integer convertToDatabaseValue(Role entityProperty) {
return entityProperty == null ? null : entityProperty.id;
}
}
}
@Entity
data class User(
@Id
var id: Long = 0,
@Convert(converter = RoleConverter::class, dbType = Int::class)
var role: Role? = null
)
enum class Role(val id: Int) {
DEFAULT(0), AUTHOR(1), ADMIN(2);
}
class RoleConverter : PropertyConverter<Role?, Int?> {
override fun convertToEntityProperty(databaseValue: Int?): Role? {
if (databaseValue == null) {
return null
}
for (role in Role.values()) {
if (role.id == databaseValue) {
return role
}
}
return Role.DEFAULT
}
override fun convertToDatabaseValue(entityProperty: Role?): Int? {
return entityProperty?.id
}
}
enum Role {
unknown,
author,
admin
}
@Entity()
class User {
int id;
// The Role type is not supported by ObjectBox.
// So ignore this field...
@Transient()
Role? role;
// ...and define a field with a supported type,
// that is backed by the role field.
int? get dbRole {
_ensureStableEnumValues();
return role?.index;
}
set dbRole(int? value) {
_ensureStableEnumValues();
if (value == null) {
role = null;
} else {
role = Role.values[value]; // throws a RangeError if not found
// or if you want to handle unknown values gracefully:
role = value >= 0 && value < Role.values.length
? Role.values[value]
: Role.unknown;
}
}
User(this.id);
void _ensureStableEnumValues() {
assert(Role.unknown.index == 0);
assert(Role.author.index == 1);
assert(Role.admin.index == 2);
}
}
If you define your custom type or converter inside a Java or Kotlin entity class, it must be static or respectively not an inner class.
Don’t forget to handle null values correctly – usually, you should return null if the input is null.
Database types in the sense of the converter are the primitive (built-in) types offered by ObjectBox, as mentioned in the beginning. It is recommended to use a primitive type that is easily convertible (int, long, byte array, String, …).
You must not interact with the database (such as using Box
or BoxStore
) inside the converter. The converter methods are called within a transaction, so for example, getting or putting entities to a box will fail.
You can use a converter with List types. For example, you could convert a List of Strings to a JSON array resulting in a single string for the database. At the moment it is not possible to use an array with converters (you can track this feature request).
Enums are popular with data objects like entities. When persisting enums, there are a couple of best practices:
Do not persist the enum’s ordinal or name: Both are unstable, and can easily change the next time you edit your enum definitions.
Use stable ids: Define a custom property (integer or string) in your enum that is guaranteed to be stable. Use this for your persistence mapping.
Prepare for the unknown: Define an UNKNOWN enum value. It can serve to handle null or unknown values. This will allow you to handle cases like an old enum value getting removed without crashing your app.
QueryBuilder
is unaware of custom types. You have to use the primitive DB type for queries.
So for the Role example above you would get users with the role of admin with the query condition .equal(UserProperties.Role, 2)
.
The ObjectBox Admin web app (formerly Data Browser) is an easy way to view what's happening in your database of your app using a web browser. Browse your app's data and gain insights.
The ObjectBox Admin web app allows you to
view data objects and schema of your database inside a regular web browser,
display additional information,
and download objects in JSON format.
Admin comes in two variants: as a standalone desktop app (Docker image) and embedded in the Android library.
The web app runs directly on your device or on your development machine. Behind the scenes, this is done by embedding a lightweight HTTP server into ObjectBox. It runs completely local (no Cloud whatsoever) and you can simply open the Admin in your Browser.
Latest changes (2025-07-05):
Add class and dependency diagrams to the schema page (view and download)
Improved data view for large vectors by displaying only the first elements and the full vector in a dialog
Detects images stored as bytes and shows them as such (PNG, GIF, JPEG, SVG, WEBP)
To run Admin on a desktop operating system, e.g. your development machine, you can launch ObjectBox Admin instantaneously using the official ObjectBox Admin Docker image objectboxio/admin
.
This requires a running Docker Engine or Docker Desktop. If not done, install and start Docker Engine (Linux, Windows/WSL2) or Docker Desktop (Windows, macOS) capable of running a Linux/x86_64 image.
There's a known issue on macOS and Windows with Docker and changes to the filesystem. Thus, to see changes made to the database, Admin needs to be restarted.
Recommended, but the below script needs to be run from a Linux distribution (for example Ubuntu through WSL2 on Windows) or macOS. To run from Windows PowerShell, see the "Run Docker manually" tab.
objectbox-admin.sh
Shell Front-End for ObjectBox Admin Docker ImageDownload the script objectbox-admin.sh
via the link above. Make it executable (e.g. chmod +x objectbox-admin.sh
). Copy it to some place (e.g./usr/local/bin
) and then run it.
Then you can have a quick look at the options of the script:
$ objectbox-admin.sh --help
usage: objectbox-admin.sh [options] [<database-directory>]
<database-directory> ( defaults to ./objectbox ) should contain an objectbox "data.mdb" file.
Available (optional) options:
[--port <port-number>] Mapped bind port to localhost (defaults to 8081)
Basically you can optionally select the path to an ObjectBox database and the mapping of the local HTTP port (e.g. to open multiple Admins to analyze multiple databases).
So to run the script, either change to the directory where the objectbox
directory with the database file data.mdb
exists:
cd objectbox-c/examples/cpp-gen
objectbox-admin.sh
Or pass the path to it as an argument:
objectbox-admin.sh objectbox-c/examples/cpp-gen
If you see the error failed: port is already allocated.
try to use a different local port. E.g. to use port 8082
:
objectbox-admin.sh --port 8082
Note: If you run the script for the first time Docker will download the ObjectBox Admin image automatically from Docker Hub. If run again the download is skipped as the image has been cached in your local image repository.
The Docker image is available at objectboxio/admin
on Docker Hub. We recommend to use the latest
tag.
Linux/macOS/Windows WSL2 command line
docker run --rm -it --volume /path/to/db:/db -u $(id -u):$(id -g) --publish 8081:8081 objectboxio/admin:latest
Replace /path/to/db
with the actual path to the directory containing the data.mdb
file.
If you need to use a different local port other than 8081, modify the first number of -p
accordingly. E.g. -p 8082:8081
lets you open the web-app at http://localhost:8082
.
Windows PowerShell/Command Prompt
Note: Make sure Docker Desktop runs with WSL2 instead of Hyper-V.
docker run --rm -it --volume C:\path\to\db:/db --publish 8081:8081 objectboxio/admin:latest
Replace C:\path\to\db
with the actual path to the directory containing the data.mdb
file. Note this uses Windows-style backslashes.
Note the user id (-u
) mapping is omitted on Windows. The port can be changed as written above.
Once Admin has started, open the local URL printed by the script (typically http://127.0.0.1:8081
) in your browser. You should see the Data page for an entity type.
We strongly recommend using Admin only for debug builds as it ships with additional resources and configuration not intended for production code.
Modify the app's Gradle build file to add the dependency and change the “io.objectbox” plugin to be applied after the dependencies block:
dependencies {
// Manually add objectbox-android-objectbrowser only for debug builds,
// and objectbox-android for release builds.
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
}
// Apply the plugin after the dependencies block so it picks up
// and does not add objectbox-android.
apply plugin: 'io.objectbox'
// Or using Kotlin DSL:
apply(plugin = "io.objectbox")
If the plugin is not applied afterwards, the build will fail with a duplicate files error (like Duplicate files copied in APK lib/armeabi-v7a/libobjectbox.so
) because the plugin fails to detect and adds the objectbox-android
library.
Modify the Gradle build file of the Flutter Android app to add the dependency:
// Tell Gradle to exclude the Android library (without Admin)
// that is added by the objectbox_flutter_libs package for debug builds.
configurations {
debugImplementation {
exclude group: 'io.objectbox', module: 'objectbox-android'
}
}
dependencies {
// Add the Android library with ObjectBox Admin only for debug builds.
// Note: when the objectbox package updates, check if the Android
// library below needs to be updated as well.
// TODO Replace <version> with the one noted in the release notes (https://github.com/objectbox/objectbox-dart/releases)
debugImplementation("io.objectbox:objectbox-android-objectbrowser:<version>")
}
To avoid a version mismatch on updates, we suggest to change the dependency on the objectbox Dart package from a range of versions to a concrete version:
dependencies:
# Note: when updating objectbox, check the release notes (https://github.com/objectbox/objectbox-dart/releases)
# if objectbox-android-objectbrowser in android/app/build.gradle has to be updated.
objectbox: x.y.z # TODO Replace with valid version
objectbox_flutter_libs: any
Finally, after creating the store, to start Admin:
Create an Admin
instance and call start
:
boxStore = MyObjectBox.builder().androidContext(this).build();
if (BuildConfig.DEBUG) {
boolean started = new Admin(boxStore).start(this);
Log.i("ObjectBoxAdmin", "Started: " + started);
}
Create an Admin
instance and keep a reference, optionally close it once done using the web app:
if (Admin.isAvailable()) {
// Keep a reference until no longer needed or manually closed.
admin = Admin(store);
}
// (Optional) Close at some later point.
admin.close();
When Admin is started it will print the URL where to access the web app to the logs, e.g. something like:
ObjectBox Admin running at URL: http://127.0.0.1:8090/index.html
The URL can be opened on the device or emulator. To open the web app on your dev machine, see the instructions below.
For ObjectBox for Java, the app also displays a notification to access Admin. (Don't see the notification on Android 13 or newer? Try to manually turn on notifications for the app!) Tapping it will launch a service to keep the app alive and opens the Admin web app in the web browser on the device.
Stop the keep-alive service from the notification.
To open the web app on your development machine find the Admin URL log message as noted above.
Then, on your dev machine, use the ADB command to forward the port (or whichever you like) to that port of your device. If the default port 8090 is used, the command looks like this:
adb forward tcp:8090 tcp:8090
Then open the web app URL in a web browser on your dev machine.
To download all objects of the currently viewed box tap the download all button at the very bottom. The exported data is in JSON format.
Discover how to use the Query API to create queries with ObjectBox DB. By utilizing these queries, you can retrieve stored objects that meet user-defined criteria.
Use box.query(condition)
and supply a condition
on one or more properties to start building a query.
Create a condition
by accessing a property via the underscore class of the entity, e.g. User_.firstName.equal("Joe")
.
Use build()
to create a re-usable query instance. To then retrieve all results use find()
on the query. More options on retrieving results are discussed later in Run a query.
Once done, close()
the query to clean up resources.
Here is a full example to query for all users with the first name “Joe”:
Query<User> query = userBox.query(User_.firstName.equal("Joe")).build();
List<User> joes = query.find();
query.close();
val query = userBox.query(User_.firstName.equal("Joe")).build()
val joes = query.find()
query.close()
Query<User> query = userBox.query(User_.firstName.equals('Joe')).build();
List<User> joes = query.find();
query.close();
To combine multiple conditions use and(condition)
and or(condition)
. This implicitly adds parentheses around the combined conditions, e.g. cond1.and(cond2)
is logically equivalent to (cond1 AND cond2)
.
For example to get users with the first name “Joe” that are born later than 1970 and whose last name starts with “O”:
Query<User> query = userBox.query(
User_.firstName.equal("Joe")
.and(User_.yearOfBirth.greater(1970))
.and(User_.lastName.startsWith("O")))
.build();
List<User> youngJoes = query.find();
query.close();
val query = userBox.query(
User_.firstName equal "Joe"
and (User_.yearOfBirth greater 1970)
and (User_.lastName startsWith "O")
.build()
val youngJoes = query.find()
query.close()
Query<User> query = userBox.query(
User_.firstName.equal('Joe')
.and(User_.yearOfBirth.greaterThan(1970))
.and(User_.lastName.startsWith('O')))
.build();
// or use operator overloads:
Query<User> query = userBox.query(
User_.firstName.equal('Joe') &
User_.yearOfBirth.greaterThan(1970) &
User_.lastName.startsWith('O'))
.build();
query = userBox.query(
User.firstName.equals("Joe") &
User.yearOfBirth.greater_than(1970) &
User.lastName.starts_with('O')
).build()
joes = query.find()
To nest conditions pass a combined condition to and()
or or()
:
// equal AND (less OR oneOf)
Query<User> query = box.query(
User_.firstName.equal("Joe")
.and(User_.age.less(12)
.or(User_.stamp.oneOf(new long[]{1012}))))
.order(User_.age)
.build();
// equal AND (less OR oneOf)
val query = box.query(
User_.firstName equal "Joe"
and (User_.age less 12
or (User_.stamp oneOf longArrayOf(1012))))
.order(User_.age)
.build()
Query<User> query = box.query(
User_.firstName.equal('Joe')
.and(User_.age.lessThan(12)
.or(User_.stamp.oneOf([1012]))))
.order(User_.age)
.build();
query = userBox.query(
(User.firstName.equals("Joe")
& User.yearOfBirth.greater_than(1970)) |
User.lastName.starts_with('O')
).build()
joes = query.find()
In Kotlin, instead of condition1.and(condition2)
you can write condition1
and
condition2
(similarly condition1
or
condition2
).
In Dart and Python, instead of condition1.and(condition2)
you can write condition1
&
condition2
(similarly condition1
|
conditon2
).
Use condition.alias(aliasName)
to set an alias for a condition
that can later be used to change the parameter value of the condition on the built query.
Apart from the standard conditions like equal()
, notEqual()
, greater()
and less()
there are also additional conditions available:
isNull()
and notNull()
,
between()
to filter for values that are between the given two,
oneOf()
and notOneOf()
to filter for values that match any in the given array,
startsWith()
, endsWith()
and contains()
for extended String filtering.
See the API for a full list:
Relation conditions: Java
A special condition is available for vector properties with an HNSW index. See the dedicated page for details:
In addition to specifying conditions, you can order the returned results using the order()
method. By default this sorts ASCII characters in alphabetical order while ignoring case and numbers in ascending order.
Query<User> query = userBox
.query(User_.firstName.equal("Joe"))
.order(User_.lastName) // in ascending order, ignoring case
.build();
val query = userBox
.query(User_.firstName.equal("Joe"))
.order(User_.lastName) // in ascending order, ignoring case
.build()
// in ascending order, ignoring case
final qBuilder = box.query(User_.firstName.equals('Joe')).order(User_.lastName);
final query = qBuilder.build();
You can also pass flags to order()
to sort in descending order, to sort case sensitive or to specially treat null values. For example to sort the above results in descending order and case sensitive instead:
.order(User_.lastName, QueryBuilder.DESCENDING | QueryBuilder.CASE_SENSITIVE)
.order(User_.lastName, QueryBuilder.DESCENDING or QueryBuilder.CASE_SENSITIVE)
.order(User_.lastName, flags: Order.descending | Order.caseSensitive)
Order directives can also be chained. Check the method documentation (Java) for details.
Queries are first created (and not yet executed) by calling build()
on the QueryBuilder
.
Query<User> query = builder.build();
Once the query is created, it allows various operations, which we will explore in the following sub sections.
There are a couple of find methods to retrieve objects matching the query:
// return all entities matching the query
List<User> joes = query.find();
// return only the first result or null if none
User joe = query.findFirst();
// return the only result or null if none, throw if more than one result
User joe = query.findUnique();
To return all entities matching the query simply call find()
.
To only return the first result, use findFirst()
.
If you expect a unique result, call findUnique()
instead. It will give you a single result or null, if no matching entity was found and throw an exception if there was more than one result.
To remove all objects matching a query, call query.remove()
.
If you frequently run the same query you should cache the Query
object and re-use it. To make a Query
more reusable you can change the values, or query parameters, of each condition you added even after the Query
is built. Let's see how.
Assume we want to find a list of User
with specific firstName
values. First, we build a regular Query
with an equal()
condition for firstName
. Because we have to pass an initial parameter value to equal()
but plan to override it before running the Query
later, we just pass an empty string:
// build a query
Query<User> query = userBox.query(User_.firstName.equal("")).build();
// build a query
val query = userBox.query(User_.firstName.equal("")).build()
// build a query
final query = userBox.query(User_.firstName.equals('')).build();
# build a query
query = userBox.query(User.firstName.equals('')).build();
Now at some later point, we want to run the Query
with an actual value for the equals
condition onfirstName
:
// Change firstName parameter to "Joe" and get results
List<User> joes = query.setParameter(User_.firstName, "Joe").find();
// Change firstName parameter to "Jake" and get results
List<User> jakes = query.setParameter(User_.firstName, "Jake").find();
// Change firstName parameter to "Joe" and get results
val joes = query.setParameter(User_.firstName, "Joe").find()
// Change firstName parameter to "Jake" and get results
val jakes = query.setParameter(User_.firstName, "Jake").find()
// Change firstName parameter to "Joe" and get results
query.param(User_.firstName).value = 'Joe';
final joes = query.find();
// Change firstName parameter to "Jake" and get results
final jakes = (query..param(User_.firstName).value = 'Jake').find();
# Change firstName parameter to "Joe" and get results
joes = query.set_parameter_string(User.firstName, "Joe").find()
# Change firstName parameter to "Jake" and get results
jakes = query.set_parameter_srting(User.firstName, "Jake").find()
You might already be wondering what happens if you have more than one condition using firstName
? For this purpose you can assign each parameter an alias while specifying the condition:
// assign alias "name" to the equal query parameter
Query<User> query = userBox
.query(User_.firstName.equal("").alias("name"));
// assign alias "name" to the equal query parameter
val query = userBox
.query(User_.firstName.equal("").alias("name"))
// assign alias "name" to the equals query parameter
final query = userBox.query(User_.firstName.equals('', alias: 'name')).build();
# Assign alias "name" to the equals query parameter
query = userBox.query(User.firstName.equals('').alias("name")).build();
Then, when setting a new parameter value pass the alias instead of the property:
// Change parameter with alias "name" to "Joe", get results
List<User> joes = query.setParameter("name", "Joe").find();
// Change parameter with alias "name" to "Joe" and get results
val joes = query.setParameter("name", "Joe").find()
// Change parameter with alias "name" to "Joe" and get results
final joes = (query..param(User_.firstName, alias: 'name').value = 'Joe').find();
# Change parameter with alias "name" to "Joe" and get results
joes = query.set_parameter_alias_string("name", "Joe").find()
Sometimes you only need a subset of a query, for example, the first 10 elements to display in your user interface. This is especially helpful (and resource-efficient) when you have a high number of entities and you cannot limit the result using query conditions only.
// offset by 10, limit to at most 5 results
List<User> joes = query.find(10, 5);
// offset by 10, limit to at most 5 results
val joes = query.find(10, 5)
// offset by 10, limit to at most 5 results
query
..offset = 10
..limit = 5;
List<User> joes = query.find();
# Offset by 10, limit to at most 5 results
joes = query \
.offset(10)
.limit(5)
.find()
offset:
The first offset
results are skipped.
limit:
At most limit
results are returned.
To avoid loading query results right away, Query offers findLazy()
and findLazyCached()
which return a LazyList
of the query results.
LazyList is a thread-safe, unmodifiable list that reads entities lazily only once they are accessed. Depending on the find method called, the lazy list will be cached or not. Cached lazy lists store the previously accessed objects to avoid loading entities more than once. Some features of the list are limited to cached lists (e.g. features that require the entire list). See the LazyList class documentation for more details.
Instead of reading the whole result (list of objects) using find()
you can stream it using stream()
:
Query<User> query = userBox.query().build();
Stream<User stream = query.stream();
await stream.forEach((User user) => print(user));
query.close();
To learn how to observe or listen to changes to the results of a query, see the data observers page:
If you only want to return the values of a particular property and not a list of full objects you can use a PropertyQuery. After building a query, simply call property(Property)
to define the property followed by the appropriate find method.
For example, instead of getting all User
s, to just get their email addresses:
String[] emails = userBox.query().build()
.property(User_.email)
.findStrings();
// or use .findString() to return just the first result
val emails = userBox.query().build()
.property(User_.email)
.findStrings()
// or use .findString() to return just the first result
final query = userBox.query().build();
List<String> emails = query.property(User_.email).find();
query.close();
By default, null values are not returned. However, you can specify a replacement value to return if a property is null:
// includes 'unknown' for each null email
String[] emails = userBox.query().build()
.property(User_.email)
.nullValue("unknown")
.findStrings();
// includes 'unknown' for each null email
val emails = userBox.query().build()
.property(User_.email)
.nullValue("unknown")
.findStrings()
final query = userBox.query().build();
// includes 'unknown' for each null email
List<String> emails = query.property(User_.email).find(replaceNullWith: 'unknown');
query.close();
The property query can also only return distinct values:
PropertyQuery pq = userBox.query().build().property(User_.firstName);
// returns ['joe'] because by default, the case of strings is ignored.
String[] names = pq.distinct().findStrings();
// returns ['Joe', 'joe', 'JOE']
String[] names = pq.distinct(StringOrder.CASE_SENSITIVE).findStrings();
// the query can be configured to throw there is more than one value
String[] names = pq.unique().findStrings();
val pq = userBox.query().build().property(User_.firstName)
// returns ['joe'] because by default, the case of strings is ignored.
val names = pq.distinct().findStrings()
// returns ['Joe', 'joe', 'JOE']
val names = pq.distinct(StringOrder.CASE_SENSITIVE).findStrings()
// the query can be configured to throw there is more than one value
val names = pq.unique().findStrings()
final query = userBox.query().build();
PropertyQuery<String> pq = query.property(User_.firstName);
pq.distinct = true;
// returns ['Joe', 'joe', 'JOE']
List<String> names = pq.find();
// returns ['joe']
pq.caseSensitive = false;
List<String> names = pq.find();
query.close();
Property queries (JavaDoc and Dart API docs) also offer aggregate functions to directly calculate the minimum, maximum, average, sum and count of all found values:
min()
/ minDouble()
: Finds the minimum value for the given property over all objects matching the query.
max()
/ maxDouble()
: Finds the maximum value.
sum()
/ sumDouble()
: Calculates the sum of all values. Note: the non-double version detects overflows and throws an exception in that case.
avg()
: Calculates the average (always a double) of all values.
count()
: returns the number of results. This is faster than finding and getting the length of the result array. Can be combined with distinct()
to count only the number of distinct values.
After creating a relation between entities, you might want to add a query condition for a property that only exists in the related entity. In SQL this is solved using JOINs. But as ObjectBox is not a SQL database we built something very similar: links. Links are based on Relations - see the doc page for the introduction.
Assume there is a Person
that can be associated with multiple Address
entities:
@Entity
public class Person {
@Id long id;
String name;
ToMany<Address> addresses;
}
@Entity
public class Address {
@Id long id;
String street;
String zip;
}
@Entity
class Person {
@Id
var id: Long = 0
var name: String? = null
lateinit var addresses: ToMany<Address>
}
@Entity
class Address {
@Id
var id: Long = 0
var street: String? = null
var zip: String? = null
}
@Entity()
class Person {
int id;
String name;
final addresses = ToMany<Address>();
}
@Entity()
class Address {
int id;
String street;
String zip;
}
To get a Person
with a certain name that also lives on a specific street, we need to query the associated Address
entities of a Person
. To do this, use the link()
method of the query builder to tell that the addresses
relation should be queried. Then add a condition for Address
:
// get all Person objects named "Elmo"...
QueryBuilder<Person> builder = personBox
.query(Person_.name.equal("Elmo"));
// ...which have an address on "Sesame Street"
builder.link(Person_.addresses)
.apply(Address_.street.equal("Sesame Street"));
List<Person> elmosOnSesameStreet = builder.build().find();
// get all Person objects named "Elmo"...
val builder = personBox
.query(Person_.name.equal("Elmo"))
// ...which have an address on "Sesame Street"
builder.link(Person_.addresses)
.apply(Address_.street.equal("Sesame Street"))
val elmosOnSesameStreet = builder.build().find()
// get all Person objects named "Elmo"...
QueryBuilder<Person> builder = personBox
.query(Person_.name.equals('Elmo'));
// ...which have an address on "Sesame Street"
builder.linkMany(Person_.addresses, Address_.street.equals('Sesame Street'));
Query<Person> query = builder.build();
List<Person> elmosOnSesameStreet = query.find();
query.close();
What if we want to get a list of Address
instead of Person
? If you know ObjectBox relations well, you would probably add a @Backlink
relation to Address
and build your query using it with link()
as shown above:
@Entity
public class Address {
// ...
@Backlink(to = "addresses")
ToMany<Person> persons;
}
// get all Address objects with street "Sesame Street"...
QueryBuilder<Address> builder = addressBox
.query(Address_.street.equal("Sesame Street"));
// ...which are linked from a Person named "Elmo"
builder.link(Address_.persons)
.apply(Person_.name.equal("Elmo"));
List<Address> sesameStreetsWithElmo = builder.build().find();
@Entity
class Address {
// ...
@Backlink(to = "addresses")
lateinit var persons: ToMany<Person>
}
// get all Address objects with street "Sesame Street"...
val builder = addressBox
.query(Address_.street.equal("Sesame Street"))
// ...which are linked from a Person named "Elmo"
builder.link(Address_.persons)
.apply(Person_.name.equal("Elmo")
val sesameStreetsWithElmo = builder.build().find()
@Entity()
class Address {
...
@Backlink()
final persons = ToMany<Person>();
}
// get all Address objects with street "Sesame Street"...
QueryBuilder<Address> builder =
addressBox.query(Address_.street.equals('Sesame Street'));
// ...which are linked from a Person named "Elmo"
builder.linkMany(Address_.persons, Person_.name.equals('Elmo'));
Query<Address> query = builder.build();
List<Address> sesameStreetsWithElmo = query.find();
query.close();
But actually, you do not have to modify the Address
entity (you still can if you need the @Backlink
elsewhere). Instead, we can use the backlink()
method to create a backlink to the addresses
relation from Person
just for that query:
// get all Address objects with street "Sesame Street"...
QueryBuilder<Address> builder = addressBox
.query(Address_.street.equal("Sesame Street"));
// ...which are linked from a Person named "Elmo"
builder.backlink(Person_.addresses)
.apply(Person_.name.equal("Elmo"));
List<Address> sesameStreetsWithElmo = builder.build().find();
// get all Address objects with street "Sesame Street"...
val builder = addressBox
.query(Address_.street.equal("Sesame Street"))
// ...which are linked from a Person named "Elmo"
builder.backlink(Person_.addresses)
.apply(Person_.name.equal("Elmo"))
val sesameStreetsWithElmo = builder.build().find()
// get all Address objects with street "Sesame Street"...
QueryBuilder<Address> builder =
addressBox.query(Address_.street.equals('Sesame Street'));
// ...which are linked from a Person named "Elmo"
builder.backlinkMany(Person_.addresses, Person_.name.equals('Elmo'));
Query<Address> query = builder.build();
List<Address> sesameStreetsWithElmo = query.find();
query.close();
By default relations are loaded lazily: when you first access a ToOne
or ToMany
property it will perform a database lookup to get its data. On each subsequent access it will use a cached version of that data.
List<Customer> customers = customerBox.query().build().find();
// Customer has a ToMany called orders.
// First access: this will cause a database lookup.
Order order = customers.get(0).orders.get(0);
val customers = customerBox.query().build().find()
// Customer has a ToMany called orders
val order = customers[0].orders[0] // first access: causes a database lookup
While this initial lookup is fast, you might want to prefetch ToOne
or ToMany
values before the query results are returned. To do this call the QueryBuilder.eager
method when building your query and pass the RelationInfo
objects associated with the ToOne
and ToMany
properties to prefetch:
List<Customer> customers = customerBox.query()
.eager(Customer_.orders) // Customer has a ToMany called orders.
.build()
.find();
// First access: this will cause a database lookup.
Order order = customers.get(0).orders.get(0);
val customers = customerBox.query()
.eager(Customer_.orders) // Customer has a ToMany called orders
.build()
.find()
customers[0].orders[0] // first access: this will NOT cause a database lookup
Eager loading only works one level deep. If you have nested relations and you want to prefetch relations of all children, you can instead add a query filter as described below. Use it to simply access all relation properties, which triggers them to lookup there values as described above.
Query filters come into play when you are looking for objects that need to match complex conditions, which cannot be fully expressed with the QueryBuilder class. Filters are written in Java and thus can express any complexity. Needless to say, that database conditions can be matched more efficiently than Java-based filters. Thus you will get the best results when you use both together:
Narrow down results using standard database conditions to a reasonable number (use QueryBuilder to get “candidates”)
Now filter those candidates using the QueryFilter Java interface to identify final results
A QueryFilter implementation looks at one candidate object at a time and returns true if the candidate is a result or false if not.
Example:
// Reduce object count to reasonable value.
songBox.query(Song_.bandId.equal(bandId))
// Filter is performed on candidate objects.
.filter((song) -> song.starCount * 2 > song.downloads);
Notes on performance: 1) ObjectBox creates objects very fast. 2) The virtual machine is tuned to garbage collect short-lived objects. Notes 1) and 2) combined makes a case for filtering because ObjectBox creates candidate objects of which some are not used and thus get garbage collected quickly after their creation.
The ToMany class offers additional methods that can be convenient in query filters:
hasA: returns true if one of the elements matches the given QueryFilter
hasAll: returns true if all of the elements match the given QueryFilter
getById: return the element with the given ID (value of the property with the @Id annotation)
To see what query is actually executed by ObjectBox:
// Set the LOG_QUERY_PARAMETERS debug flag
BoxStore store = MyObjectBox.builder()
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build();
// Execute a query
query.find();
// Set the LOG_QUERY_PARAMETERS debug flag
val store = MyObjectBox.builder()
.debugFlags(DebugFlags.LOG_QUERY_PARAMETERS)
.build()
// Execute a query
query.find()
print(query.describeParameters());
Then in your console (or logcat on Android) you will see log output like:
Parameters for query #2:
(firstName ==(i) "Joe"
AND age < 12)
Additional configuration options when creating an ObjectBox database.
This page contains:
This page assumes you have to your project.
To then change the default behavior of the ObjectBox plugin and processor read on for advanced setup options.
The ObjectBox Gradle plugin adds required libraries and the annotation processor to your projects dependencies automatically, but you can also add them manually.
Just make sure to apply the ObjectBox Gradle plugin after the dependencies block, so it does not replace manually added dependencies.
In your app's Gradle build script:
For JVM apps, by default, the ObjectBox Gradle plugin only adds the native (Linux, macOS or Windows) library required to run on your current system. If your app wants to support multiple platforms, manually add all of the required native libraries listed above when you distribute your app.
In your app’s Gradle build script, the following processor options, explained below, are available:
By default, the ObjectBox model file is stored in module-name/objectbox-models/default.json
. You can change the file path and name by passing the objectbox.modelPath
argument to the ObjectBox annotation processor.
By default, the MyObjectBox class is generated in the same or a parent package of your entity classes. You can define a specific package by passing the objectbox.myObjectBoxPackage
argument to the ObjectBox annotation processor.
You can enable debug output for the annotation processor if you encounter issues while setting up your project and entity classes.
In your app’s build.gradle
file, enable the objectbox.debug
option and then run Gradle with the --info
option to see the debug output.
To enable debug mode for the ObjectBox Gradle plugin:
ObjectBox can help you migrate from greenDAO by generating classes with a greenDAO-like API.
See the on how to enable and use this feature.
To customize the directory (relative to the package root) where the files generated by ObjectBox are written, add the following to your pubspec.yaml
:
dependencies {
// All below added automatically by the plugin:
// Java library
implementation("io.objectbox:objectbox-java:$objectboxVersion")
// Annotation processor
annotationProcessor("io.objectbox:objectbox-processor:$objectboxVersion")
// One of the native libraries required for your system
implementation("io.objectbox:objectbox-linux:$objectboxVersion")
implementation("io.objectbox:objectbox-macos:$objectboxVersion")
implementation("io.objectbox:objectbox-windows:$objectboxVersion")
// Not added automatically:
// Since 2.9.0 we also provide ARM support for the Linux library
implementation("io.objectbox:objectbox-linux-arm64:$objectboxVersion")
implementation("io.objectbox:objectbox-linux-armv7:$objectboxVersion")
}
// Apply plugin after dependencies block so they are not overwritten.
apply plugin: "io.objectbox"
// Or using Kotlin DSL:
apply(plugin = "io.objectbox")
dependencies {
// All below added automatically by the plugin:
// Java library
implementation("io.objectbox:objectbox-java:$objectboxVersion")
// Kotlin extension functions
implementation("io.objectbox:objectbox-kotlin:$objectboxVersion")
// Annotation processor
kapt("io.objectbox:objectbox-processor:$objectboxVersion")
// One of the native libraries required for your system
implementation("io.objectbox:objectbox-linux:$objectboxVersion")
implementation("io.objectbox:objectbox-macos:$objectboxVersion")
implementation("io.objectbox:objectbox-windows:$objectboxVersion")
// Not added automatically:
// Since 2.9.0 we also provide ARM support for the Linux library
implementation("io.objectbox:objectbox-linux-arm64:$objectboxVersion")
implementation("io.objectbox:objectbox-linux-armv7:$objectboxVersion")
}
// Apply plugin after dependencies block so they are not overwritten.
apply plugin: "io.objectbox"
// Or using Kotlin DSL:
apply(plugin = "io.objectbox")
kapt {
arguments {
arg("objectbox.modelPath", "$projectDir/schemas/objectbox.json")
arg("objectbox.myObjectBoxPackage", "com.example.custom")
arg("objectbox.debug", true)
}
}
// Groovy DSL (build.gradle)
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
"objectbox.modelPath" : "$projectDir/schemas/objectbox.json".toString(),
"objectbox.myObjectBoxPackage" : "com.example.custom",
"objectbox.debug" : "true"
]
}
}
}
}
// Kotlin DSL (build.gradle.kts)
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments.put("objectbox.modelPath", "$projectDir/schemas/objectbox.json")
arguments.put("objectbox.myObjectBoxPackage", "com.example.custom")
arguments.put("objectbox.debug", "true")
}
}
}
}
// Groovy DSL
tasks.withType(JavaCompile) {
options.compilerArgs += [ "-Aobjectbox.modelPath=$projectDir/schemas/objectbox.json" ]
options.compilerArgs += [ "-Aobjectbox.myObjectBoxPackage=com.example.custom" ]
options.compilerArgs += [ "-Aobjectbox.debug=true" ]
}
// Kotlin DSL
tasks.withType<JavaCompile>() {
options.compilerArgs.add("-Aobjectbox.modelPath=$projectDir/schemas/objectbox.json")
options.compilerArgs.add("-Aobjectbox.myObjectBoxPackage=com.example.custom")
options.compilerArgs.add("-Aobjectbox.debug=true")
}
kapt {
arguments {
arg("objectbox.modelPath", "$projectDir/schemas/objectbox.json")
arg("objectbox.myObjectBoxPackage", "com.example.custom")
arg("objectbox.debug", true)
}
}
// Enable debug output for the plugin
// Groovy DSL
objectbox {
debug = true
}
// Kotlin DSL
configure<io.objectbox.gradle.ObjectBoxPluginExtension> {
debug.set(true)
}
objectbox:
# Writes objectbox-model.json and objectbox.g.dart to lib/custom (and test/custom).
output_dir: custom
# Or optionally specify the lib and test output folder separately.
# output_dir:
# lib: custom
# test: other
dependencies {
// All below added automatically by the plugin:
// Java library
implementation("io.objectbox:objectbox-java:$objectboxVersion")
// Kotlin extension functions
implementation("io.objectbox:objectbox-kotlin:$objectboxVersion")
// Annotation processor
kapt("io.objectbox:objectbox-processor:$objectboxVersion")
// Native library for Android
implementation("io.objectbox:objectbox-android:$objectboxVersion")
}
// Apply plugin after dependencies block so they are not overwritten.
apply plugin: 'io.objectbox'
// Or using Kotlin DSL:
apply(plugin = "io.objectbox")
dependencies {
// All below added automatically by the plugin:
// Java library
implementation("io.objectbox:objectbox-java:$objectboxVersion")
// Annotation processor
annotationProcessor("io.objectbox:objectbox-processor:$objectboxVersion")
// Native library for Android
implementation("io.objectbox:objectbox-android:$objectboxVersion")
}
// Apply plugin after dependencies block so they are not overwritten.
apply plugin: 'io.objectbox'
// Or using Kotlin DSL:
apply(plugin = "io.objectbox")
How to observe box and query changes using ObjectBox with Java or Dart, how to integrate with RxJava.
On this page:
ObjectBox for Java makes it easy for your app to react to data changes by providing:
data observers,
reactive extensions,
and an optional library to work with RxJava.
This makes setting up data flows easy while taking care of threading details.
Let’s start with an example to demonstrate what you can do with reactive data observers:
// Keep a reference until the subscription is cancelled
// to avoid garbage collection.
private DataSubscription subscription;
// ...
// Build a regular query.
Query<Task> query = taskBox.query().equal(Task_.complete, false).build();
// Subscribe to its results.
subscription = query.subscribe()
.on(AndroidScheduler.mainThread())
.observer(data -> updateResultDisplay(data));
// Cancel to no longer receive updates (e.g. leaving screen).
subscription.cancel();
private void updateResultDisplay(List<Task> tasks) {
// Do something with the given tasks.
}
The first line creates a regular query to get Task
objects where task.complete == false
. The second line connects an observer to the query. This is what happens:
the query is executed in the background
once the query finishes the observer gets the result data
whenever changes are made to Task
objects in the future, the query will be executed again
once updated query results are in, they are propagated to the observer
the observer is called on Android’s main thread
Now, let’s dive into the details.
When objects change, ObjectBox notifies subscribed data observers. They can either subscribe to changes of certain object types (via BoxStore) or to query results. To create a data observer you need to implement the generic io.objectbox.reactive.DataObserver
interface:
public interface DataObserver<T> {
void onData(T data);
}
This observer will be called by ObjectBox when necessary: typically shortly after subscribing and when data changes.
BoxStore
allows a DataObserver
to subscribe to object types. Let’s say we have a to-do list app where Task
objects get added. To get notified when Task
objects are added in another place in our app we can do the following:
DataObserver<Class<Task>> observer = new DataObserver<Class<Task>>() {
@Override public void onData(Class<Task> data) {
// TODO Do something, e.g. run a query.
// Just observing a query can also be done
// more easily, read on!
List<Task> results = store.boxFor(Task.class)
.query(Task_.text.contains("TODO"))
.build()
.find(offset, limit);
}
};
// Keep the subscription while used
// to avoid garbage collection of the observer.
subscription = boxStore.subscribe(Task.class).observer(observer);
val observer = object : DataObserver<Class<Task>> {
override fun onData(data: Class<Task>) {
// TODO Do something, e.g. run a query.
// Just observing a query can also be done
// more easily, read on!
val results = store.boxFor(Task::class)
.query(Task_.text.contains("TODO"))
.build()
.find(offset, limit)
}
}
// Keep the subscription while used
// to avoid garbage collection of the observer.
subscription = store.subscribe(Task::class.java).observer(observer)
Here onData()
is not called with anything useful as data. If you need more than being notified, like to get a list of Task
objects following the above example, read on to learn how to observe queries.
ObjectBox let’s you build queries to find the objects matching certain criteria. Queries are an essential part of ObjectBox: whenever you need a specific set of data, you will probably use a query.
Combining queries and observers results in a convenient and powerful tool: query observers will automatically deliver fresh results whenever changes are made to entities in a box. Let’s say you display a list of to-do tasks in your app. You can use a DataObserver to get all tasks that are not yet completed and pass them to a method updateUi() (note that we are using lambda syntax here):
Query<Task> query = taskBox.query().equal(Task_.completed, false).build();
subscription = query.subscribe().observer(data -> updateResultDisplay(data));
So when is our observer lambda called? Immediately when an observer is subscribed, the query will be run in a separate thread. Once the query result is available, it will be passed to the observer. This is the first call to the observer.
Now let’s say a task gets changed and stored in ObjectBox. It doesn't matter where and how; it might be the user who marked a task as completed, or some backend thread putting additional tasks during synchronization with a server. In any case, the query will notify all observers with (potentially) updated query results.
Note that this pattern can greatly simplify your code: there is a single place where your data comes in to update your user interface. There is no separate initialization code, no wiring of events, no re-running queries, etc.
See the subscribe()-method documentation for more details.
When you call observer()
, it returns a subscription object implementing the io.objectbox.reactive.DataSubscription
interface:
public interface DataSubscription {
void cancel();
boolean isCanceled();
}
Keep a reference to the DataSubscription
for as long as results should be received, otherwise it can be garbage collected at any point. Also call cancel()
on it once the observer should not be notified anymore, e.g. when leaving the current screen:
// Keep a reference for as long as updates should be received.
DataSubscription subscription = boxStore.subscribe().observer(myObserver);
// To no longer receive updates (e.g. leaving screen):
subscription.cancel();
If you have more than one query subscription, you might find it useful to create a DataSubscriptionList
instance instead to keep track of multiple DataSubscription
objects. Pass the list to the query.subscribe(subList)
overload. A basic example goes like this:
private DataSubscriptionList subscriptions = new DataSubscriptionList();
protected void onStart() {
super.onStart();
Query<A> queryA = boxA.query().build();
queryA.subscribe(subscriptions).observe(someObserverForAs);
Query<B> queryB = boxB.query.build();
queryB.subscribe(subscriptions).observe(someObserverForBs);
}
protected void onStop() {
super.onStop();
// Cancels both subscriptions (for A and B).
subscriptions.cancel();
}
Observer notifications occur after a transaction is committed. For some scenarios it is especially important to know transaction bounds. If you call box.put()
or remove()
individually, an implicit transaction is started and committed. For example, this code fragment would trigger data observers on User.class
twice:
box.put(friendUser);
box.put(myUser);
// Log:
// User friendUser put.
// Observers of User called.
// User myUser put.
// Observers of User called.
There are several ways to combine several operations into one transaction, for example using one of the runInTx()
or callInTx()
methods in the BoxStore class. For our simple example, we can simply use an overload of put()
accepting multiple objects:
box.put(friendUser, myUser);
// Log:
// Users friendUser and myUser put.
// Observers of User called.
This results in a single transaction and thus in a single DataObserver
notification.
In the first part you saw how data observers can help you keep your app state up to date. But there is more: ObjectBox comes with simple and convenient reactive extensions for typical tasks. While most of these are inspired by RxJava, they are not actually based on RxJava. ObjectBox brings its own features because not all developers are familiar with RxJava (for the RxJava ObjectBox library see below). We do not want to impose the complexity (Rx is almost like a new language to learn) and size of RxJava (~10k methods) on everyone. So, let’s keep it simple and neat for now.
On Android, UI updates must occur on the main thread only. Luckily, ObjectBox allows to switch the observer from a background thread over to the main thread. Let’s take a look on a revised version of the to-do task example from above:
Query<Task> query = taskBox.query().equal(Task_.complete, false).build();
// updateResultDisplay is always called on the Android main thread.
subscription = query.subscribe()
.on(AndroidScheduler.mainThread())
.observer(data -> updateResultDisplay(data));
Where is the difference? The additional on()
call is all that is needed to tell where we want our observer to be called. AndroidScheduler.mainThread()
is a built-in scheduler implementation. Alternatively, you can create an AndroidScheduler
using a custom Looper
, or build a fully custom scheduler by implementing the io.objectbox.reactive.Scheduler
interface.
Maybe you want to transform the data before you hand it over to an observer. Let’s say, you want to keep track of the count of all stored objects for each type. The BoxStore subscription gives you the classes of the objects, and this example shows you how to transform them into actual object counts:
subscription = boxStore.subscribe()
.transform(clazz -> return boxStore.boxFor(clazz).count())
.observer(count -> updateCount(count));
Note that the transform operation takes a Class
object and returns a Long
number. Thus the DataObserver
receives the object count as a Long
parameter in onData()
.
While the lambda syntax is nice and brief, let’s look at the io.objectbox.reactive.Transformer
interface for clarification of what the transform()
method expects as a parameter:
public interface DataTransformer<FROM, TO> {
TO transform(FROM source) throws Exception;
}
Some additional notes on transformers:
Transforms are not required to actually “transform” any data. Technically, it is fine to return the same data that is received and just do some processing with (or without) it.
Transformers are always executed asynchronously. It is fine to perform long lasting operations.
Maybe you noticed that a transformer may throw any type of exception. Also, a DataObserver
might throw a RuntimeException
. In both cases, you can provide an ErrorObserver
to be notified about an exception that occurred. The io.objectbox.reactive.ErrorObserver
is straight-forward:
public interface ErrorObserver {
void onError(Throwable th);
}
To specify your ErrorObserver
, simply call the onError()
method after subscribe()
.
When you subscribe to a query, the DataObserver
gets both of the following by default:
Initial query results (right after subscribing)
Updated query results (underlying data was changed)
Sometimes you may by interested in only one of those. This is what the methods single()
and onlyChanges()
are for (call them after subscribe()
). Single subscriptions are special in the way that they are cancelled automatically once the observer is notified. You can still cancel them manually to ensure no call to the observer is made at a certain point.
Sometimes it may be nice to have a weak reference to a data observer. Note that for the sake of a deterministic flow, it is advisable to cancel subscriptions explicitly whenever possible. If that does not scare you off, use weak()
after subscribe()
.
To summarize threading as discussed earlier:
Query execution runs on a background thread (exclusive for this task)
DataTransformer
runs on a background thread (exclusive for this task)
DataObserver
and ErrorObserver
run on a background thread unless a scheduler is specified via the on()
method.
By design, there are zero dependencies on any Rx libraries in the core of ObjectBox. As you saw before ObjectBox gives you simple means to transform data, asynchronous processing, thread scheduling, and one time (single) notifications. However, you still might want to integrate with the mighty RxJava 2 (we have no plans to support RxJava 1). For this purpose we created the ObjectBox RxJava extension library:
implementation "io.objectbox:objectbox-rxjava:$objectboxVersion"
It provides the classes RxQuery
and RxBoxStore
. Both offer static methods to subscribe using RxJava means.
For general object changes, you can use RxBoxStore
to create an Observable
. RxQuery
allows to subscribe to query objects using:
Flowable
Observable
Single
Example usage:
// Keep a reference until the disposable is disposed
// to avoid garbage collection.
private Disposable disposable;
// ...
Query query = box.query().build();
disposable = RxQuery.observable(query).subscribe(this);
The extension library is open-source and available GitHub.
You can build a Stream from a query to get notified any time there is a change to the box of any of the queried entities:
Stream<Query<Note>> watchedQuery = box.query(condition).watch();
final sub1 = watchedQuery.listen((Query<Note> query) {
// This gets triggered any time a box of any of the queried entities
// has changes, e.g. objects are put or removed.
// Call any query method here, for example:
print(query.count());
print(query.find());
});
...
sub1.cancel(); // Cancel the subscription after your code is done.
For a Flutter app you typically want to get the latest results immediately when listening to the stream, and also get a list of objects instead of a query instance:
// Build and watch the query,
// set triggerImmediately to emit the query immediately on listen.
return box.query(condition)
.watch(triggerImmediately: true)
// Map it to a list of objects to be used by a StreamBuilder.
.map((query) => query.find());
query = userBox.query(User.firstName.equals("Joe")).build()
joes = query.find()
Local on-device approximate nearest neighbor (ANN) search on high-dimensional vector properties
Vector search is the task of searching for objects whose vector is near to a given input query vector. Applications include semantic/similarity search (often performs better than full text search (FTS)), multi-modal search (text, images, video), recommendation engines and various use cases in AI.
You can use ObjectBox as a plain vector database and store only vectors and IDs. Or, you can have your entire data model stored in ObjectBox since it is also a full database. Choose anything in between that best suits your needs.
To use ObjectBox as a vector database follow these 3 simple steps:
Define your data model along with a vector index,
Insert your data/vectors,
Search for nearest neighbors.
To illustrate these steps, we will use a simplified example using cities throughout the next sections. Each city has a location expressed as latitude and longitude. And thus, we can search cities that are close to a certain point.
In the diagram above, let's look at which are the closest cities to the red point, which is located at latitude 53.0 and longitude 15.0. One can intuitively see that Berlin is closest, followed by Copenhagen and Vienna. In search terms, Berlin, Copenhagen and Vienna are the 3 nearest neighbors to the given point.
A city's location has only 2 dimensions, which is easy to grasp. While even 3D space can still be quite intuitive, we humans typically have a hard time when dimensions increase beyond that. But this is exactly where vector databases shine: they handle high-dimensional data with typically hundreds or thousands of dimensions.
In order to enable efficient vector search, a vector database needs to "index" the data. This is true for ordinary data too, just that a vector index is a "special" index. ObjectBox relies on Hierarchical Navigable Small Worlds (HNSW), a state-of-the-art algorithm for approximate nearest neighbor (ANN) search that performs very fast and is very scalable.
Defining the data model in ObjectBox is straight-forward: in one of the supported programming languages, declare a City class and "annotate" (tag) it as an ObjectBox "Entity" (a persistable object type; check the Getting started guide for details). The class shall have the following members (properties): an obligatory ID, a name, and a location. The latter is expressed as a vector; after all we want to demonstrate vector search. This vector only has 2 dimensions: latitude and longitude.
So, this is what a City data class with an HNSW index definition can look like:
@Entity()
class City:
id = Id
name = String
location = Float32Vector(index=HnswIndex(
dimensions=2,
distance_type=VectorDistanceType.EUCLIDEAN
))
@Entity()
class City {
@Id()
int id = 0;
String? name;
@HnswIndex(dimensions: 2, distanceType: VectorDistanceType.geo)
@Property(type: PropertyType.floatVector)
List<double>? location;
City(this.name, this.location);
}
@Entity
public class City {
@Id
long id = 0;
@Nullable
String name;
@HnswIndex(dimensions = 2, distanceType = VectorDistanceType.GEO)
float[] location;
public City(@Nullable String name, float[] location) {
this.name = name;
this.location = location;
}
}
@Entity
data class City(
@Id var id: Long = 0,
var name: String? = null,
@HnswIndex(dimensions = 2, distanceType = VectorDistanceType.GEO)
var location: FloatArray? = null
)
// objectbox: entity
class City {
var id: Id = 0
var name: String?
// objectbox:hnswIndex: dimensions=2, distanceType="geo"
var location: [Float]?
}
// The syntax for all supported options is:
// objectbox:hnswIndex: dimensions=2, neighborsPerNode=30, indexingSearchCount=100, flags="debugLogs", distanceType="euclidean", reparationBacklinkProbability=0.95, vectorCacheHintSizeKB=2097152
// flags may be a comma-separated list of debugLogs, debugLogsDetailed, reparationLimitCandidates, vectorCacheSimdPaddingOff
// distanceType may be one of euclidean, geo, cosine, dotProduct, dotProductNonNormalized
For C++, you define the data model using FlatBuffer schema files (see the getting started guide for details):
table City {
id: ulong;
name: string;
/// objectbox: index=hnsw, hnsw-dimensions=2
/// objectbox: hnsw-distance-type=Euclidean
location: [float];
}
Once the ObjectBox Generator was run, it creates a City struct like this:
struct City {
obx_id id;
std::string name;
std::vector<float> location;
}
As a starting point the index configuration only needs the number of dimensions. To optimize the index, you can supply additional options via the annotation later once you got things up and running:
dimensions (required): how many dimensions of the vector to use for indexing. This is a fixed value that depends on your specific use case (e.g. on your embedding model) and you will typically only use vectors of that exact dimension. For special use cases, you can insert vectors with a higher dimension. However, if the vector of an inserted object has less dimensions, it is completely ignored for indexing (it cannot be found).
distanceType: the algorithm used to determine the distance between two vectors. By default, (squared) Euclidean distance is used: d(v, w) = length(v - w)
Other algorithms, based on cosine, Haversine distance and dot product, are available.
neighborsPerNode (aka "M" in HNSW terms): the maximum number of connections per node (default: 30). A higher number increases the graph connectivity which can lead to better results, but higher resources usage. Try e.g. 16 for faster but less accurate results, or 64 for more accurate results.
indexingSearchCount (aka "efConstruction" in HNSW terms): the number of neighbors searched for while indexing (default: 100). The default value serves as a starting point that can likely be optimized for specific datasets and use cases. The higher the value, the more accurate the search, but the longer the indexing will take. If indexing time is not a major concern, a value of at least 200 is recommended to improve search quality.
There are also some advanced options available:
flags to turn on debug log output, to turn off SIMD padding, or to limit graph reparation when nodes are removed.
reparationBacklinkProbability: when a node is removed, its neighborhood is repaired. Use this to configure the probability of adding backlinks between repaired nodes (defaults to 1.0, which is always).
vectorCacheHintSizeKB: a non-binding hint for the maximum size of the vector cache (default: 2 GB). Note: memory is only allocated for caching as needed. E.g. smaller data sets will reserve less memory.
Vector objects are inserted like any other data objects in ObjectBox (the indexing is done automatically behind the scenes):
store = Store()
box = store.box(City)
box.put(City(name="Barcelona", location=[41.385063, 2.173404]))
box.put(City(name="Nairobi", location=[-1.292066, 36.821945]))
box.put(City(name="Salzburg", location=np.array([47.809490, 13.055010])))
In Python, vector values can be plain Python lists or numpy arrays (the property type must be compatible with numpy array dtype
).
final box = store.box<City>();
box.putMany([
City("Barcelona", [41.385063, 2.173404]),
City("Nairobi", [-1.292066, 36.821945]),
City("Salzburg", [47.809490, 13.055010]),
]);
final Box<City> box = store.boxFor(City.class);
box.put(
new City("Barcelona", new float[]{41.385063F, 2.173404F}),
new City("Nairobi", new float[]{-1.292066F, 36.821945F}),
new City("Salzburg", new float[]{47.809490F, 13.055010F})
);
val box = store.boxFor(City::class)
box.put(
City(name = "Barcelona", location = floatArrayOf(41.385063f, 2.173404f)),
City(name = "Nairobi", location = floatArrayOf(-1.292066f, 36.821945f)),
City(name = "Salzburg", location = floatArrayOf(47.809490f, 13.055010f))
)
let box: Box<City> = store.box()
try box.put([
City("Barcelona", [41.385063, 2.173404]),
City("Nairobi", [-1.292066, 36.821945]),
City("Salzburg", [47.809490, 13.055010]),
])
cityBox.put({
City{0, "Barcelona", {41.385063F, 2.173404F}},
City{0, "Nairobi", {-1.292066F, 36.821945F}},
City{0, "Salzburg", {47.809490F, 13.055010F}}
});
(Approximate) nearest neighbor search is part of the standard ObjectBox Query API. For vector properties with an HNSW index, a special "nearest neighbor" query condition is available when building a query. It accepts a query vector to search neighbors for, and the maximum number of results to return. See the method documentation for additional details.
Once the query is built, one of its "find" methods is called to execute it. For vector search, a special set of "find with scores" methods is available, that also return the distance (the "score") to the queried vector. Additionally, only the "find with scores" methods will return the results ordered by the distance (nearest first). And as with other find methods, you can either only retrieve result IDs or complete objects.
# Query vector
madrid = [40.416775, -3.703790]
# Prepare a Query object to search for the 2 closest neighbors:
query = box.query(City.location.nearest_neighbor(madrid, 2)).build()
# Retrieve IDs
results = query.find_ids_with_scores()
for id_, score in results:
print(f"City ID: {id_}, distance: {score}")
# Retrieve objects
results = query.find_with_scores()
for object_, score in results:
print(f"City: {object_.name}, distance: {score}")
final madrid = [40.416775, -3.703790]; // query vector
// Prepare a Query object to search for the 2 closest neighbors:
final query = box
.query(City_.location.nearestNeighborsF32(madrid, 2))
.build();
// Combine with other conditions as usual
final query = box
.query(City_.location.nearestNeighborsF32(madrid, 2)
.and(City_.name.startsWith("B")))
.build();
// Retrieve IDs
final results = query.findIdsWithScores();
for (final result in results) {
print("City ID: ${result.id}, distance: ${result.score}");
}
// Retrieve objects
final results = query.findWithScores();
for (final result in results) {
print("City: ${result.object.name}, distance: ${result.score}");
}
final float[] madrid = {40.416775F, -3.703790F}; // query vector
// Prepare a Query object to search for the 2 closest neighbors:
final Query<City> query = box
.query(City_.location.nearestNeighbors(madrid, 2))
.build();
// Combine with other conditions as usual
final Query<City> query = box
.query(City_.location.nearestNeighbors(madrid, 2)
.and(City_.name.startsWith("B")))
.build();
// Retrieve IDs
final List<IdWithScore> results = query.findIdsWithScores();
for (IdWithScore result : results) {
System.out.printf("City ID: %d, distance: %f%n", result.getId(), result.getScore());
}
// Retrieve objects
final List<ObjectWithScore<City>> results = query.findWithScores();
for (ObjectWithScore<City> result : results) {
System.out.printf("City: %s, distance: %f%n", result.get().name, result.getScore());
}
val madrid = floatArrayOf(40.416775f, -3.703790f) // query vector
// Prepare a Query object to search for the 2 closest neighbors:
val query = box
.query(City_.location.nearestNeighbors(madrid, 2))
.build()
// Combine with other conditions as usual
val query = box
.query(
City_.location.nearestNeighbors(madrid, 2)
.and(City_.name.startsWith("B"))
)
.build()
// Retrieve IDs
val results = query.findIdsWithScores()
for (result in results) {
println("City ID: ${result.id}, distance: ${result.score}")
}
// Retrieve objects
val results = query.findWithScores()
for (result in results) {
println("City: ${result.get().id}, distance: ${result.score}")
}
let madrid = [40.416775, -3.703790] // query vector
// Prepare a Query object to search for the 2 closest neighbors:
let query = try box
.query { City.location.nearestNeighbors(queryVector: madrid, maxCount: 2) }
.build()
// Combine with other conditions as usual
final query = box
.query { City.location.nearestNeighbors(queryVector: madrid, maxCount: 2)
&& City.name.startsWith("B") }
.build()
// Retrieve IDs
let results = try query.findIdsWithScores()
for result in results {
print("City ID: \(result.id), distance: \(result.score)")
}
// Retrieve objects
let results = try query.findWithScores()
for result in results {
print("City: \(result.object.name), distance: \(result.score)")
}
float madrid[] = {40.416775f, -3.703790f};
obx::Query<City> query = cityBox
.query(City_::location.nearestNeighbors(madrid, 1))
.build();
std::vector<std::pair<City, double>> citiesWithScores =
queryCityByLocation_.findWithScores();
// Print the results
printf("%3s %-18s %-19s %-10s\n", "ID", "Name", "Location", "Score");
for (const auto& pair : citiesWithScores) {
const City& city = pair.first;
printf("%3" PRIu64 " %-18s %-9.2f %-9.2f %5.2f\n", city.id, city.name.c_str(),
city.location[0], city.location[1], pair.second);
}
And that's it! You have successfully performed your first vector search with ObjectBox. 🎉
Vector search is a central component in AI applications. In this context you will often be confronted with the term "embeddings". So, let's have a closer look:
To make complex data (e.g. texts, images, videos, ...) accessible, it is transformed into an N-dimensional vector representation. This is called an embedding. An embedding has a certain meaning (or semantics) based on a given model and is associated with its source document. For example with multi-modal models, the word "cat" would be transformed to a vector that is nearby a vector that originated from a picture of a cat. This is the power of embeddings: they capture the essence of the data they represent. For AI Large Language Models (LLMs) these vector embeddings also mimic some "understanding" and semantics become "computable".
Note that ObjectBox works with vectors, so it is your responsibility to create these vectors. E.g. you need access to some model that transforms data into vectors (embeddings). These models can be online or offline.
Typically, you will also use the model to transform the query. Let's say you have associated all your pictures with a vector by a multi-modal model and stored all vectors into ObjectBox. Now using the same model, you create a search vector to find similar pictures. The input to get this vector can be a single word like "cat", a sentence like "birthday party with friends", or an image showing a specific car.
To take the previous section on embeddings a bit further, let's enter Retrieval Augmented Generation (RAG). RAG allows interactions between an LLM and external sources to produce better and individual results. Vector databases like ObjectBox are extremely efficient in that regard as they speak the "native language" of LLMs: vector embeddings. In contrast, a standard web search is magnitudes slower. In short, you can make individual data available in ObjectBox and wire it to an LLM to make this knowledge available. This wiring is already done by various libraries such as LangChain.
For Python, we offer the package "langchain-objectbox" that integrates ObjectBox into the LangChain framework:
pip install langchain-objectbox --upgrade
Once installed, you can use ObjectBox as a local on-device vector store. It implements VectorStore
and thus you can instantiate it with e.g. from_documents
or from_texts
or there asynchronous variants. Example:
from langchain_objectbox.vectorstores import ObjectBox
objectbox = ObjectBox.from_texts(texts, embeddings,
embedding_dimensions=768)
Current limitations:
Python APIs are non-final and subject to change
Expect more features to come:
Releases for all programming languages supported by ObjectBox
Closer integration into queries to prefilter objects
Performance improvements, e.g. on ARM CPUs
Additional distance functions
Quantization and support for non-float vectors
ObjectBox Vector Search offers significant advantages by combining the capabilities of a vector search engine with the robustness of a full-featured database:
No Memory Limitations
Unlike in-memory libraries like FAISS, ObjectBox uses disk storage when data exceeds available memory, enabling scalability.
Smart caching ensures frequently accessed data remains in memory for optimal performance.
Instant Readiness
There's no need for an initial data load when starting the application. Simply open the database, and you can immediately begin performing vector searches.
Efficient Updates
When data changes (e.g., adding, modifying, or deleting entries), ObjectBox only persists the changes (deltas).
In contrast, most libraries rewrite the entire dataset, if they even support persistent storage.
ACID Transactions
ObjectBox ensures all updates are safely persisted, offering robust data integrity.
This eliminates concerns about data corruption during unexpected events, such as power outages.
Unified Data Storage
Vector data is rarely standalone; it often ties to related objects or metadata.
ObjectBox enables you to store your entire data model, keeping all related data in one place. For example:
An embedding model can generate vectors for images. These vectors can be stored alongside related properties like creation date, tags, filenames, etc.
Additionally, associated objects like users (e.g., the creator or those who liked the image) can also be stored within the same database.
With ObjectBox Vector Search, you get a powerful, flexible, and scalable solution tailored for modern applications where data relationships matter.
Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.
ObjectBox is a database that persists objects. For a clear distinction, we sometimes call those persistable objects entities.
To let ObjectBox know which classes are entities you annotate them with @Entity
. This annotation identifies the class User
in the following example as a persistable entity. This will trigger ObjectBox to generate persistence code tailored for this class:
@Entity
public class User {
@Id
private long id;
private String name;
// Not persisted:
@Transient
private int tempUsageCount;
// TODO: getters and setters.
}
@Entity
data class User(
@Id var id: Long = 0,
var name: String? = null,
// Not persisted:
@Transient var tempUsageCount: Int = 0
)
@Entity()
class User {
// Annotate with @Id() if name isn't "id" (case insensitive).
int id = 0;
String? name;
// Not persisted:
@Transient
int tempUsageCount = 0;
)
@Entity()
class User:
id = Id
name = String
temp_usage_count = None
In ObjectBox, entities must have one 64-bit integer ID property with non-private visibility (or non-private getter and setter method) to efficiently get or reference objects.
@Entity
public class User {
@Id public long id;
// Note: You can use the nullable java.lang.Long, but we do not recommend it.
...
}
@Entity
data class User(
@Id var id: Long = 0,
...
)
@Entity()
class User {
// Annotate with @Id() if name isn't "id" (case insensitive).
int id;
...
}
@Entity()
class User:
id = Id # Use either class Id or instance notation (e.g. Id())
If you need to use another type for IDs (such as a string UID given by a server), model them as regular properties and use queries to look up objects by your application-specific ID. Also, make sure to index the property, and if it's a string use a case-sensitive condition, to speed up lookups. To prevent duplicates it is also possible to enforce a unique value for this secondary ID.
@Entity
class StringIdEntity {
@Id public long id;
@Index public String uid;
// Alternatively:
// @Unique String uid;
}
StringIdEntity entity = box.query()
.equal(StringIdEntity_.uid, uid, StringOrder.CASE_SENSITIVE)
.build().findUnique()
@Entity
data class StringIdEntity(
@Id var id: Long = 0,
@Index var uid: String? = null
// Alternatively:
// @Unique uid: String? = null
)
val entity = box.query()
.equal(StringIdEntity_.uid, uid, StringOrder.CASE_SENSITIVE)
.build().findUnique()
@Entity()
class StringIdEntity(
int id;
@Index() // or alternatively use @Unique()
String uid;
)
final objects = box.query(StringIdEntity_.uid.equals('...')).build().find();
ID properties are unique and indexed by default.
When you put a new object you do not assign an ID. By default IDs for new objects are assigned by ObjectBox. See the page on Object IDs for details.
ObjectBox needs to access the data of your entity’s properties (e.g. in the generated code). You have two options:
Make sure properties do not have private visibility.
Provide standard getters (your IDE can generate them easily).
To improve performance when ObjectBox constructs your entities, you might also want to provide an all-properties constructor.
@Entity
public class Order {
// Option 1: field is not private.
@Id long id;
// Option 2: field is private, but getter is available.
private String name;
public ToOne<Customer> customer;
public ToMany<Order> relatedOrders;
// At minimum, provide a default constructor for ObjectBox.
public Order() {
}
// Optional: all-properties constructor for better performance.
// - make sure type matches exactly,
// - for ToOne add its virtual ID property instead,
// - for ToMany add no parameter.
public Order(long id, String name, long customerId) {
this.id = id;
this.name = name;
this.customer.setTargetId(customerId);
}
public String getName() {
return this.name;
}
}
// For Kotlin a data class with default values
// meets all above requirements.
@Entity
data class User(
@Id var id: Long = 0,
var name: String? = null
)
@Entity()
class User {
int id;
String? _name;
String get name {...}
set name(String value) {...}
User(this.id);
}
ObjectBox can store almost any type (class) of property as long as it can be converted to one of the built-in types. See the dedicated page for details:
@Transient
marks properties that should not be persisted. In Java static
or transient
properties will also not be persisted.
@Transient
private int notPersisted;
@Transient
var notPersisted: Int = 0
@Transient()
int? notPersisted;
@NameInDb
lets you define a name on the database level for a property. This allows you to rename the property without affecting the property name on the database level.
We recommend using @Uid annotations to rename properties and even entities instead.
@NameInDb only works with inline constants to specify a column name.
@NameInDb("username")
private String name;
@NameInDb("username")
var name: String? = null
Annotate a property with @Index
to create a database index for the corresponding database column. This can improve performance when querying for that property.
@Index
private String name;
@Index is currently not supported for String[]
, byte[]
, float
and double
@Index
var name: String? = null
@Index is currently not supported for Array<String>
, ByteArray
, Float
and Double
@Index()
String name;
@Index is currently not supported for double
and listsList<String>, List<int>, Uint8List, Int8List
@Entity()
class User:
name = String(index=Index())
An index stores additional information in the database to make lookups faster. As an analogy, we could look at Java-like programming languages where you store objects in a list. For example, you could store persons using a List<Person>
. Now, you want to search for all persons with a specific name so you would iterate through the list and check for the name property of each object. This is an O(N) operation and thus doesn't scale well with an increasing number of objects. To make this more scalable you can introduce a second data structure Map<String, Person>
with the name as a key. This will give you a constant lookup time (O(1)). The downside of this is that it needs more resources (here: RAM) and slows down add/remove operations on the list a bit. These principles can be transferred to database indexes, just that the primary resource consumed is disk space.
For scalar properties, ObjectBox uses a value-based index. Because String
properties typically require more storage space than scalar values, by default ObjectBox uses a hash index for strings instead.
To override the default and use a value-based index for a String
property, specify the index type
:
@Index(type = IndexType.VALUE)
private String name;
@Index(type = IndexType.VALUE)
var name: String? = null
@Index(type: IndexType.value)
String name;
Keep in mind that for String
, depending on the length of your values, a value-based index may require more storage space than the default hash-based index.
ObjectBox supports these index types:
Not specified or DEFAULT Uses the best index based on property type (HASH for String
, VALUE for others).
VALUE Uses property values to build the index. For String,
this may require more storage than a hash-based index.
HASH Uses a 32-bit hash of property values to build the index. Occasional collisions may occur which should not have any performance impact in practice. Usually, a better choice than HASH64, as it requires less storage.
HASH64 Uses a 64-bit hash of property values to build the index. Requires more storage than HASH and thus should not be the first choice in most cases.
To enable nearest neighbor search, a special index type for vector properties is available:
Annotate a property with @Unique
to enforce that values are unique before an entity is put:
@Unique
private String name;
@Unique
var name: String? = null
@Unique()
String? name;
A put()
operation will abort and throw a UniqueViolationException
if the unique constraint is violated:
try {
box.put(new User("Sam Flynn"));
} catch (UniqueViolationException e) {
// A User with that name already exists.
}
try {
box.put(User("Sam Flynn"))
} catch (e: UniqueViolationException) {
// A User with that name already exists.
}
try {
box.put(User('Sam Flynn'))
} on UniqueViolationException catch (e) {
// A User with that name already exists.
}
For a single property it is possible to specify that a conflicting object should be replaced instead of an exception being thrown:
@Unique(onConflict = ConflictStrategy.REPLACE)
private String name;
@Unique(onConflict = ConflictStrategy.REPLACE)
var name: String? = null
@Unique(onConflict: ConflictStrategy.replace)
String? name;
The REPLACE
strategy will add a new object with a different ID. As relations (ToOne/ToMany) reference objects by ID, if the previous object was referenced in any relations, these need to be updated manually.
Use @Type
in Java/Kotlin or the type attribute on @Property
in Dart to override how the value of a property is stored and interpreted in the database.
// Store 64-bit integer as time in nanoseconds.
@Type(DatabaseType.DateNano)
private long timeInNanos;
// Store 64-bit integer as time in nanoseconds.
@Type(DatabaseType.DateNano)
var timeInNanos: Long = 0
// Time with nanosecond precision.
@Property(type: PropertyType.dateNano)
DateTime nanoDate;
@Property(type: PropertyType.byte)
int byte; // 1 byte
@Property(type: PropertyType.short)
int short; // 2 bytes
@Property(type: PropertyType.char)
int char; // 1 bytes
@Property(type: PropertyType.int)
int int32; // 4 bytes
@Property(type: PropertyType.float)
double float; // 4 bytes
@Property(type: PropertyType.byteVector)
List<int> byteList;
class Tasks:
date_started = Date(py_type=int)
Creating to-one and to-many relations between objects is possible as well, see the Relations documentation for details.
Once your entity schema is in place, you can trigger the code generation process.
Release notes for ObjectBox 1.5.0 and older.
Although this is technically the changelog for Java, this is also a good reference of what changed in the ObjectBox C++ core.
Add convenience oneOf
and notOneOf
conditions that accept Date
to avoid manual conversion using getTime()
.
When BoxStore
is closing, briefly wait on active transactions to finish.
Guard against crashes when BoxStore
was closed, but database operations do still occur concurrently (transactions are still active).
Examples: added Vector Search example that demonstrates how to perform on-device approximate nearest neighbor (ANN) search.
Revert deprecation of Box.query()
, it is still useful for queries without any condition.
Add note on old query API methods of QueryBuilder
that they are not recommended for new projects. Use the new query APIs instead.
Update and expand documentation on ToOne
and ToMany
.
ObjectBox now supports Vector Search to enable efficient similarity searches.
This is particularly useful for AI/ML/RAG applications, e.g. image, audio, or text similarity. Other use cases include semantic search or recommendation engines.
Create a Vector (HNSW) index for a floating point vector property. For example, a City
with a location vector:
@Entity
public class City {
@HnswIndex(dimensions = 2)
float[] location;
}
Perform a nearest neighbor search using the new nearestNeighbors(queryVector, maxResultCount)
query condition and the new "find with scores" query methods (the score is the distance to the query vector). For example, find the 2 closest cities:
final float[] madrid = {40.416775F, -3.703790F};
final Query<City> query = box
.query(City_.location.nearestNeighbors(madrid, 2))
.build();
final City closest = query.findWithScores().get(0).get();
For an introduction to Vector Search, more details and other supported languages see the Vector Search documentation.
BoxStore: deprecated BoxStore.sizeOnDisk()
. Instead use one of the new APIs to determine the size of a database:
BoxStore.getDbSize()
which for a file-based database returns the file size and for an in-memory database returns the approximately used memory,
BoxStore.getDbSizeOnDisk()
which only returns a non-zero size for a file-based database.
Query: add properly named setParameter(prop, value)
methods that only accept a single parameter value, deprecated the old setParameters(prop, value)
variants.
Sync: add SyncCredentials.userAndPassword(user, password)
.
Gradle plugin: the license of the Gradle plugin has changed to the GNU Affero General Public License (AGPL).
Support creating file-less in-memory databases, e.g. for caching and testing. To create one use inMemory()
when building a BoxStore
:
store = MyObjectBox.builder()
.androidContext(context)
.inMemory("test-db")
.build();
See the BoxStoreBuilder.inMemory()
documentation for details.
Change BoxStore.deleteAllFiles()
to support deleting an in-memory database.
The maxDataSizeInKByte()
option when building a store is ready for production use. This is different from the existing maxSizeInKByte()
option in that it is possible to remove data after reaching the limit and continue to use the database. See its documentation for more details.
Admin will now print a warning when it does not have permission to show the Admin notification. When testing your app on a device with Android 13 or newer, developers should manually turn on notifications to make use of the Admin notification.
Added examples on how to use Kotlin's unsigned integer types to Custom types.
Restore compatibility with Kotlin 1.5. However, need to exclude kotlin-stdlib
1.8 from objectbox-kotlin
as it includes classes previously in the -jdk7/-jdk8 libraries to avoid duplicate class file errors. So if not absolutely needed, we still recommend to use at least Kotlin 1.8.
Throw an exception instead of crashing when trying to create a query on a closed store. #1154
The Gradle plugin now requires at least Gradle 7.0 and Android Gradle Plugin 4.1.
The Android library now requires Android 4.4 (API 19) or newer.
A new key/value validation option validateOnOpenKv()
is available on MyObjectBox.builder()
to help diagnose FileCorruptException: Corrupt DB, min key size violated
issues. If enabled, the build()
call will throw a FileCorruptException
if corruption is detected with details on which key/value is affected. #1143
Admin: integer and floating point arrays introduced with the previous release are now nicely displayed and collapsed if long.
Admin: the data table again displays all items of a page. #1135
The __cxa_pure_virtual
crash should not occur anymore; if you get related exceptions, they should contain additional information to better diagnose this issue. Let us know details in #1131
Queries: all expected results are now returned when using a less-than or less-or-equal condition for a String property with index type VALUE
. Reported via objectbox-dart#318
Queries: when combining multiple conditions with OR and adding a condition on a related entity ("link condition") the combined conditions are now properly applied. Reported via objectbox-dart#546
Some flags classes have moved to the new config
package:
io.objectbox.DebugFlags
is deprecated, use io.objectbox.config.DebugFlags
instead.
io.objectbox.model.ValidateOnOpenMode
is deprecated, use io.objectbox.config.ValidateOnOpenModePages
instead.
Support for integer and floating point arrays: store
short[]
, char[]
, int[]
, long[]
and
float[]
and double[]
(or their Kotlin counterparts, e.g. FloatArray
) without a converter.
A simple example is a shape entity that stores a palette of RGB colors:
@Entity
public class Shape {
@Id public long id;
// An array of RGB color values that are used by this shape.
public int[] palette;
}
// Find all shapes that use red in their palette
try (Query<Shape> query = store.boxFor(Shape.class)
.query(Shape_.palette.equal(0xFF0000))
.build()) {
query.findIds();
}
This can also be useful to store vector embeddings produced by machine learning, e.g.:
@Entity
public class ImageEmbedding {
@Id public long id;
// Link to the actual image, e.g. on Cloud storage
public String url;
// The coordinates computed for this image (vector embedding)
public float[] coordinates;
}
Fix incorrect Cursor code getting generated when using @Convert
to convert to a String
array.
The io.objectbox.sync
plugin now also automatically adds a Sync-enabled JNI library on macOS and Windows (previously on Linux x64 only; still need to add manually for Linux on ARM).
Fixes writes failing with "error code -30786", which may occur in some corner cases on some devices. #1099
Add docs to DbSchemaException
on how to resolve its typical causes.
This release includes breaking changes to generated code. If you encounter build errors, make sure to clean and build your project (e.g. Build > Rebuild project in Android Studio).
Add Query.copy()
and QueryThreadLocal
to obtain a Query
instance to use in different threads. Learn more about re-using queries. #1071
Add relationCount
query condition to match objects that have a certain number of related objects pointing to them. E.g. Customer_.orders.relationCount(2)
will match all customers with two orders, Customer_.orders.relationCount(0)
will match all customers with no associated order. This can be useful to find objects where the relation was dissolved, e.g. after the related object was removed.
Allow using a relation target ID property with a property query. E.g. query.property(Order_.customerId)
will map results to the ID of the customer of an order. #1028
Add docs on DbFullException
about why it occurs and how to handle it.
Do not fail to transform an entity class that contains a transient relation field when using Android Gradle Plugin 7.1 or lower.
Restore compatibility for Android projects using Gradle 6.1. The minimum supported version for Gradle is 6.1 and for the Android Gradle Plugin 3.4. This should make it easier for older projects to update to the latest version of ObjectBox.
Using Sync? This release uses a new Sync protocol which improves efficiency. Reach out via your existing contact to check if any actions are required for your setup.
Add findFirstId()
and findUniqueId()
to Query
which just return the ID of a matching object instead of the full object.
Experimental support for setting a maximum data size via the maxDataSizeInKByte
property when building a Store. This is different from the existing maxSizeInKByte
property in that it is possible to remove data after reaching the limit and continue to use the database. See its documentation for more details.
Fix a crash when querying a value-based index (e.g. @Index(type = IndexType.VALUE)
) on Android 32-bit ARM devices. #1105
Various small improvements to the native libraries.
Using Sync? There is no Sync version for this release, please continue using version 3.2.1.
Note: V3.3.0 contains a bug preventing correct transformation of some classes, please use V3.3.1 instead.
Gradle plugin: use new transform API with Android Plugin 7.2.0 and newer. Builds should be slightly faster as only entity and cursor classes and only incremental changes are transformed. #1078
Gradle plugin: improve detection of applied Android plugins, improve registration of byte-code transform for non-Android Java projects, add check for minimum supported version of Gradle.
Using Sync? There is no Sync version for this release, please continue using version 3.2.1.
Resolve an issue that prevented resources from getting cleaned up after closing BoxStore
, causing the reference table to overflow when running many instrumentation tests on Android. #1080
Plugin: support Kotlin 1.7. #1085
Query: throw IllegalStateException
when query is closed instead of crashing the virtual machine. #1081
BoxStore and Query now throw IllegalStateException
when trying to subscribe but the store or query is closed already.
Various internal improvements including minor optimizations for binary size and performance.
The Data Browser has been renamed to ObjectBox Admin. Deprecated AndroidObjectBrowser
, use Admin
instead. AndroidObjectBrowser
will be removed in a future release.
Windows: using a database directory path that contains unicode (UTF-8) characters does not longer create an additional, unused, directory with garbled characters.
Query: when using a negative offset or limit display a helpful error message.
Processor: do not crash, but error if ToOne/ToMany type arguments are not supplied (e.g. ToOne
instead of ToOne<Entity>
).
This release only contains bug fixes for the Android library when used with ObjectBox for Dart/Flutter.
Fix incorrect unique constraint violation if an entity contains at least two unique properties with a certain combination of non-unique indexes.
Data Browser/Admin: improved support when running multiple on the same host, but a different port (e.g. localhost:8090
and localhost:8091
).
Read the blog post with more details and code examples for the new flex properties and query conditions.
Support Flex properties. Expanding on the string and flexible map support in 3.0.0
, it is now possible to add a property using Object
in Java or Any?
in Kotlin. These "flex properties" allow to store values of various types like integers, floating point values, strings and byte arrays. Or lists and maps (using string keys) of those.
The containsElement
query condition now matches keys of string map properties. It also matches string or integer elements of a Flex list.
New containsKeyValue
query condition to match key/value combinations of string map and Flex map properties containing strings and integers. Also added matching Query.setParameters
overload.
Add ProGuard/R8 rule to not warn about SuppressFBWarnings
annotation. #1011
Add more detailed error message when loading the native library fails on Android. #1024
Data browser: byte arrays are now correctly displayed in Base64 encoding. #1033
Add BoxStore.awaitCallInTx
suspend function which wraps BoxStore.callInTx
.
Do not crash trying to add dependencies to Java desktop projects that only apply the Gradle application
plugin.
A new Query API is available that works similar to the ObjectBox for Dart/Flutter Query API and makes it easier to create nested conditions. #201
// equal AND (less OR oneOf)
val query = box.query(
User_.firstName equal "Joe"
and (User_.age less 12
or (User_.stamp oneOf longArrayOf(1012))))
.order(User_.age)
.build()
For the existing Query API, String property conditions now require to explicitly specify case. See the documentation of StringOrder
for which one to choose (typically StringOrder.CASE_INSENSITIVE
).
// Replace String conditions like
query().equal(User_.firstName, "Joe")
// With the one accepting a StringOrder
query().equal(User_.firstName, "Joe", StringOrder.CASE_INSENSITIVE)
Subscriptions now publish results in serial instead of in parallel (using a single thread vs. multiple threads per publisher). Publishing in parallel could previously lead to outdated results getting delivered after the latest results. As a side-effect transformers now run in serial instead of in parallel as well (on the same single thread per publisher). #793
Support annotating a single property with @Unique(onConflict = ConflictStrategy.REPLACE)
to replace an existing Object if a conflict occurs when doing a put. #509
@Entity
data class Example(
@Id
var id: Long = 0,
@Unique(onConflict = ConflictStrategy.REPLACE)
var uniqueKey: String? = null
)
Support @Unsigned
to indicate that values of an integer property (e.g. Integer
and Long
in Java) should be treated as unsigned when doing queries or creating indexes.
Store time in nanoseconds using the new @Type
annotation for compatibility with other ObjectBox language bindings:
@Type(DatabaseType.DateNano)
var timeInNanos: Long;
Package FlatBuffers version into library to avoid conflicts with apps or other libraries using FlatBuffers. #894
Kotlin: add Flow
extension functions for BoxStore
and Query
. #900
Data browser: display query results if a property has a NaN
value. #984
Android 12: support using Data Browser if targeting Android 12 (SDK 31). #1007
String arrays (Java String[]
and Kotlin Array<String>
) and lists (Java List<String>
and Kotlin MutableList<String>
). Using the new containsElement("item")
condition, it is also possible to query for entities where "item" is equal to one of the elements.
@Entity
data class Example(
@Id
var id: Long = 0,
var stringArray: Array<String>? = null,
var stringMap: MutableMap<String, String>? = null
)
// matches [“first”, “second”, “third”]
box.query(Example_.stringArray.containsElement(“second”)).build()
String maps (Java Map<String, String>
or Kotlin MutableMap<String, String>
). Stored internally as a byte array using FlexBuffers.
Flexible maps:
map keys must all have the same type,
map keys or values must not be null,
map values must be one of the supported database type, or a list of them (e.g. String, Boolean, Integer, Double, byte array...).
The generated JSON model file no longer contains Java-specific flags that would lead to errors if used with Sync server.
Additional checks when calling client or server methods.
Note: this is a preview release. Future releases may add, change or remove APIs.
A new experimental Query API provides support for nested AND and OR conditions. #201
Subscriptions now publish results in serial instead of in parallel (using a single thread vs. multiple threads per publisher). Publishing in parallel could previously lead to outdated results getting delivered after the latest results. As a side-effect transformers now run in serial instead of in parallel as well (on the same single thread per publisher). #793
Add documentation that string property conditions ignore case by default. Point to using case-sensitive conditions for high-performance look-ups, e.g. when using string UIDs.
Support annotating a single property with @Unique(onConflict = ConflictStrategy.REPLACE)
to replace an existing Object if a conflict occurs when doing a put. #509
Support @Unsigned
to indicate that values of an integer property (e.g. Integer
and Long
in Java) should be treated as unsigned when doing queries or creating indexes. See the Javadoc of the annotation for more details.
Store time in nanoseconds by annotating a Long
property with @Type(DatabaseType.DateNano)
.
Package FlatBuffers version into library to avoid conflicts if your code uses FlatBuffers as well. #894
Kotlin: add Flow extension functions for BoxStore and Query. #900
Data browser: display query results if a property has a NaN value. #984
New supported property types
When adding new properties, a converter is no longer necessary to store these types:
String arrays (Java String[]
and Kotlin Array<String>
). Using the new containsElement("item")
condition, it is also possible to query for entities where "item" is equal to one of the array items.
String maps (Java Map<String, String>
or Kotlin MutableMap<String, String>
). Stored internally as a byte array using FlexBuffers.
Sync
The generated JSON model file no longer contains Java-specific flags that would lead to errors if used with Sync server.
Additional checks when calling client or server methods.
This is the first release available on the Central repository (Sonatype OSSRH). Make sure to adjust your build.gradle
files accordingly:
repositories {
mavenCentral()
}
Changes:
Javadoc for find(offset, limit)
of Query
is more concrete on how offset and limit work.
Javadoc for between conditions explicitly mentions it is inclusive of the two given values.
Sync: Instead of the same name and a Maven classifier, Sync artifacts now use a different name. E.g. objectbox-android:2.9.0:sync
is replaced with objectbox-sync-android:2.9.1
.
Query: Add lessOrEqual
and greaterOrEqual
conditions for long, String, double and byte[] properties.
Support Java applications on ARMv7 and AArch64 devices. #657
To use, add implementation "io.objectbox:objectbox-linux-armv7:$objectboxVersion
or implementation "io.objectbox:objectbox-linux-arm64:$objectboxVersion
to your dependencies. Otherwise the setup is identical with Java Desktop Apps.
Resolve rare ClassNotFoundException: kotlin.text.Charsets
when running processor. #946
Ensure Query setParameters works if running the x86 library on x64 devices (which could happen if ABI filters were set up incorrectly). #927
Minor improvements to Sync tooling.
See the 2.8.0 release notes below for the latest changes.
Added Sync API.
Fixed "illegal reflective access" warning in the plugin.
The data browser notification is now silent by default, for quieter testing. #903
Updated and improved API documentation in various places (e.g. on how Query.findLazy()
and Query.findLazyCached()
work with LazyList
#906).
Print full name and link to element for @Index
and @Id
errors. #902
Explicitly allow to remove a DbExceptionListener
by accepting null values for BoxStore.setDbExceptionListener(listener)
.
Fix exception handling during BoxStoreBuilder.build()
to allow retries. For example, after a FileCorruptException
you could try to open the database again using the recently added usePreviousCommit()
option.
Add PagesCorruptException
as a special case of FileCorruptException
.
DbExceptionListener
is called more robustly.
Several database store improvements forBoxStore
and BoxStoreBuilder
New configuration options to open the database, e.g. a new read-only mode and using the previous data snapshot (second last commit) to potentially recover data.
Database validation. We got a GitHub report indicating that some specific devices ship with a broken file system. While this is not a general concern (file systems should not be broken), we decided to detect some typical problems and provide some options to deal with these.
Get the size on disk
Add an efficient check if an object exist in a Box
via contains(id)
.
Android improvements
Resolve Android Studio Build Analyzer warning about a prepare tasks not specifying outputs.
Data Browser drawables are no longer packaged in the regular Android library. GitHub #857
Fixes for one-to-many relations, e.g. allow removing both entity classes of a one-to-many relation. GitHub #859
@DefaultValue("")
annotation for properties to return an empty string instead of null. This is useful if a not-null property is added to an entity, but there are existing entities in the database that will return null for the new property. GH#157
RxJava 3 support library objectbox-rxjava3
. Also includes Kotlin extension functions to more easily obtain Rx types, e.g. use query.observable()
to get an Observable
. GH#83
The annotation processor is incremental by default. GH#620
Fix error handling if ObjectBox can't create a Java entity (the proper exception is now thrown).
Support setting an alias after combining conditions using and()
or or()
. GH#83
Turn on incremental annotation processing by default. GH#620
Add documentation that string property conditions ignore case by default. Point to using case-sensitive conditions for high-performance look-ups, e.g. when using string UIDs.
Repository Artifacts are signed once again.
Changes since 2.6.0-RC (released on 2020/04/28):
Performance improvements with query links (aka "joins"). Note: the order of results has changed unless you explicitly specified properties to order by. Remember: you should not depend on any internal order. If you did, this is a good time to fix it.
objectbox-java
no longer exposes the greenrobot-essentials and FlatBuffers dependencies to consuming projects.
Minor code improvements.
Note: this is a preview release. Future releases may add, change or remove APIs.
Add Kotlin infix extension functions for creating conditions using the new Query API. See the documentation for examples.
The old Query API now also supports setting an alias after combining conditions using and()
or or()
. GH#834
Add documentation that string property conditions ignore case by default. Point to using case-sensitive conditions for high-performance look-ups, e.g. when using string UIDs.
Java's String[]
and Kotlin's Array<String>
are now a supported database type. A converter is no longer necessary to store these types. Using the arrayProperty.equal("item")
condition, it is possible to query for entities where "item" is equal to one of the array items.
Support @Unsigned
to indicate that values of an integer property (e.g. Integer
and Long
in Java) should be treated as unsigned when doing queries or creating indexes. See the Javadoc of the annotation for more details.
Add new library to support RxJava 3, objectbox-rxjava3
. In addition objectbox-kotlin
adds extension functions to more easily obtain Rx types, e.g. use query.observable()
to get an Observable
. GH#839
To use this release change the version of objectbox-gradle-plugin
to 3.0.0-alpha2
. The plugin now properly adds the preview version of objectbox-java
to your dependencies.
buildscript {
dependencies {
classpath "io.objectbox:objectbox-gradle-plugin:3.0.0-alpha2"
}
}
dependencies {
// Artifacts with native code remain at 2.5.1.
implementation "io.objectbox:objectbox-android:2.5.1"
}
The objectbox-android
, objectbox-linux
, objectbox-macos
and objectbox-windows
artifacts shipping native code remain at version 2.5.1 as there have been no changes. If you explicitly include them, make sure to specify their version as 2.5.1
.
Note: this is a preview release. Future releases may add, change or remove APIs.
A new Query API provides support for nested AND and OR conditions. See the documentation for examples and notable changes. GH#201
Subscriptions now publish results in serial instead of in parallel (using a single thread vs. multiple threads per publisher). Publishing in parallel could previously lead to outdated results getting delivered after the latest results. As a side-effect transformers now run in serial instead of in parallel as well (on the same single thread per publisher). GH#793
Turn on incremental annotation processing by default. GH#620
To use this release change the version of objectbox-gradle-plugin
to 3.0.0-alpha1
and add a dependency on objectbox-java
version 3.0.0-alpha1
.
buildscript {
dependencies {
classpath "io.objectbox:objectbox-gradle-plugin:3.0.0-alpha1"
}
}
dependencies {
implementation "io.objectbox:objectbox-java:3.0.0-alpha1"
// Artifacts with native code remain at 2.5.1.
implementation "io.objectbox:objectbox-android:2.5.1"
}
The objectbox-android
, objectbox-linux
, objectbox-macos
and objectbox-windows
artifacts shipping native code remain at version 2.5.1 as there have been no changes. However, if your project explicitly depends on them they will pull in version 2.5.1 of objectbox-java
. Make sure to add an explicit dependency on of objectbox-java
version 3.0.0-alpha1
as mentioned above.
Support Android Gradle Plugin 3.6.0. GH#817
Support for incremental annotation processing. GH#620 It is off by default. To turn it on set objectbox.incremental
to true
in build.gradle
:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [ "objectbox.incremental" : "true" ]
}
}
}
}
Fixed corner case for N:M ToMany (not the backlinks for ToOne) returning wrong results
Property queries compute sums and averages more precisely (improved algorithms and wider internal types)
Query adds "describe" methods to obtain useful debugging information
New method removeAllObjects() in BoxStore to clear the database of all data
More helpful error messages if annotations can not be combined.
Improved documentation on various annotations.
Android: the AAR libraries ship Java 8 bytecode. Your app will not build unless you upgrade com.android.tools.build:gradle to 3.2.1 or later.
Android: the ObjectBox LiveData and Paging integration migrated from Android Support Libraries to Jetpack (AndroidX) Libraries. If you are using them the library will not work unless you make the following changes in your app:
Upgrade com.android.tools.build:gradle to 3.2.1 or later.
Upgrade compileSdkVersion to 28 or later.
Update your app to use Jetpack (AndroidX); follow the instructions in Migrating to AndroidX.
Note: this version requires backwards-incompatible changes to the generated MyObjectBox file. Make sure to rebuild your project before running your app so the MyObjectBox file is re-generated.
V2.4.0 - 2019/10/15
Class transformation works correctly if absolute path contains special characters. GH#135
V2.4.0-RC - Release Candidate 2019/10/03
Box: add getRelationEntities
, getRelationBacklinkEntities
,getRelationIds
and getRelationBacklinkIds
to directly access relations without going through ToMany.
Box: add putBatched
to put entities using a separate transaction for each batch.
Box.removeByKeys()
is now deprecated; use removeByIds()
instead.
Query: fixed performance regressions introduced in version 2.3 on 32 bit devices in combination with ordered results
Fixed removing a relation and the related entity class. GH#490
Resolved issue to enable query conditions on the target ID property of a ToOne relation. GH#537
Box.getAll always returns a mutable list. GH#685
Do not overwrite existing objectbox-java or objectbox-kotlin dependency. GH#693
Resolved a corner case build time crash when parsing package elements. GH#698
When trying to find an appropriate get-method for a property, also check if the return type matches the property type. GH#720
Explicitly display an error if two entities with the same name are detected. GH#744
The code in MyObjectBox is split up by entity to make it less likely to run into the Java method size limit when using many @Entity classes. GH#750
Query: improved performance for ordered results with a limit. GH#769
Query: throw if a filter is used incorrectly with count or remove. GH#771
Documentation and internal improvements.
Avoid UnsatisfiedLinkError on Android devices that are not identifying as Android correctly
Fix displaying large objects in Object Browser 32 bit
Kotlin properties starting with "is" of any type are detected
Add objectbox-kotlin
to dependencies if kotlin-android
plugin is applied (previously only for kotlin
plugin)
@BaseEntity classes can be generic
Fixed a bug introduced by V2.3.2 affecting older Android versions 4.3 and below
Potential work around for UnsatisfiedLinkError probably caused by installation errors mostly in alternative app markets
Support for Android Gradle Plugin 3.3.0: resolves deprecated API usage warnings.
Fixed a corner case for Box.getAll() after removeAll() to return a stale object if no objects are stored
Query improvements: findIds and LazyList also consider the order; offset and limit for findIds
Improved 32 bit support: Windows 32 version officially deployed, fixed a corner case crash
Property queries for a boolean property now allow sum()
Added Box.isEmpty()
Supporting older Linux distributions (now starting at e.g. Ubuntu 16.04 instead of 18.04)
Fix for a corner case with Box.count() when using a maximum
Minor improvements to the ObjectBox code generator
Android: set extractNativeLibs to false to avoid issues with extracting the native library
Improvements & Fixes
Fix: the unique check for string properties had false positives resulting in UniqueViolationException. This occurs only in combination with IndexType.HASH (the default) when hashes actually collide. We advise to update immediately to the newest version if you are using hashed indexes.
The release of new ObjectBox C API made us change name of the JNI library
for better distinction. This should not affect you unless you depended on that (internal) name.
Improved compatibility with class transformers like Jacoco
Fixed query links for M:N backlinks
Improved error messages for the build tools
The Object Browser AAR now includes the required Android permissions
Entity counts are now cached for better performance
Deprecated aggregate function were removed (deprecation in 1.4 with introduction of PropertyQuery)
Object browser hot fix: the hashed indexes introduced in 2.0 broke the object browser
Object browser fixes: filters with long ints, improved performance in the schema view
NPE fix in ToOne
Added a specific NonUniqueResultException if a query did not return an expected unique result
ObjectBox 2.0 introduces index types for String. Before, every index used the property value for all look-ups. Now, ObjectBox can also use a hash to build an index. Because String
properties are typically taking more space than scalar values, ObjectBox switched the default index type to hash for strings.
When migrating data from pre-2.0 ObjectBox versions, for String properties with a plain @Index annotation this will update the indexes automatically: the old value-based indexes will be deleted and the new hash-bashed indexes will be built.
A side effect of this is that the database file might grow in the process. If you want to prevent this, you can instruct ObjectBox to keep using a value-based index for a String
property by specifying the index type
using @Index(type = IndexType.VALUE)
.
Other changes:
Links and relation completeness and other features already announced in the 2.0 beta
Unique constraint for properties via @Unique annotation
Support for char type (16 bit)
RX lib deployed in JCenter
Rework of Query APIs: type safe properties (property now knows its owning entity)
Allow query conditions of links using properties (without parameter alias)
Query performance improvements when using order
Property based count: query for non-null or unique occurrences of entity properties (non-null and unique)
Additional query conditions for strings: "greater than", "less than", "in"
Added query conditions for byte arrays
Set query parameters for "in" condition (int[] and long[])
Query across relation bounds using links (aka "join"): queries just got much more powerful. For example, query for orders that have a customer with an address on "Sesame Street". Or all persons, who have a grand parent called "Alice".
Backlinks for to-many relations: now ObjectBox is "relation complete" with a bi-directional many-to-many relation.
Query performance improvements: getting min/max values of indexed properties in constant time
Android: added Paging library support (architecture components)
Kotlin extensions: more Kotlin fun with ObjectBox KTX
Query parameters aliases: helps setting query parameters in complex scenarios (e.g. for properties of linked entities)
Improved query parameter verification
Many internal improvements to keep us going fast in the future
Full support for Android local tests: use full ObjectBox features in local tests
New count method optimized for a given maximum count
Gradle option to define the package for MyObjectBox explicitly
Query condition startsWith now uses index if available for better performance
Fixed some static methods in BoxStore to ensure that the native lib is loaded
Internal optimizations for 64 bit devices
Some fixes for entities in the default package
Entity can be named Property
, no longer conflicts with ObjectBox Property class
Property queries for strings crashed on some Android devices if there were more than 512 results
Object Browser uses less threads
Object Browser now displays negative int/long values correctly
Changes to relations object in constructors were overwritten when constructors delegated to other constructors
Supply an initial database file using BoxStoreBuilder
Gradle plugin detects plain Java projects and configures dependencies
Improved Box.removeAll() performance for entities that have indexes or relations
Fixed converting from arrays in entities
Fixed @NameInDb
Fixed Gradle “androidTestCompile is obsolete” warning
macOS support: with Linux, Windows, and macOS, ObjectBox now supports all major desktop/server platforms. Use it for local unit tests or standalone Java applications.
Fixed BoxStore.close being stuck in rare scenarios
Fixed an issue with char properties in entities
Note: This release requires the Android Gradle Plugin 3.0.0 or higher.
JCenter: we’ve moved the ObjectBox artifacts to the JCenter repository. This simplifies set up and improves accessibility (e.g. JCenter is not blocked from China).
Instant App support (only with Android Gradle Plugin 3.0.0 or higher)
Added DbExceptionListener as a central place to listen for DB related exceptions
Minor improvements for ToMany and generated Cursor classes
ToMany: fixed handling of duplicate entries (e.g. fixes swap and reverse operations)
ToMany: fixed removal of non-persisted element for standalone relations
Property queries that return individual properties only (including distinct values, unique, null values, primitive result arrays or scalars)
Entity inheritance (non-polymorphic)
50% size reduction of native libraries
ToOne now implements equals() and hashCode() based on the targetId property
Android ABI x86_64 was added to the aar
ID verification does not complain about “resurrected” objects that were loaded, removed, and put again
Fixed setting Query parameters for Date type
Fixes for ObjectBox browser
Please update to the latest version. We made important changes and fixes under the hood to make ObjectBox perform better, generally, and especially in concurrent scenarios. In addition, 1.3.x comes with several improvements for developers.
Object browser lets you download all entities as JSON
Object browser efficiency improvements: introduced streamed processing to reduce memory consumption and increase performance for large data sets
Improved transaction logging, e.g. numbered transactions and waiting times for write transactions
Closing the store (e.g. for tests, an app should just leave it open) will wait for any ongoing write transaction to finish
Two additional overloads for static BoxStore.deleteAllFiles()
Added automatic retries for read transactions; also configurable for queries
Fixes for concurrent setups (multi threaded, in live apps with up to 100 threads); internally we improved our testing automation and CI infrastructure significantly
Fix for sumDouble throwing an exception
Fixed ProGuard rule for ToOne
Improved debug logging for transactions and queries: enable this using BoxStoreBuilder.debugFlags(…) with values from the DebugFlags class
Improved package selection for MyObjectBox if you use entities in multiple packages (please check if you need to adjust your imports after the update)
ObjectBox Browser’s UI is more compact and thus better usable on mobile devices
Fix for ObjectBoxLiveData firing twice
Compatibility note: We removed some Box.find methods, which were all tagged as @Temporary. Only the Property based ones remain (for now, also @Temporary).
ObjectBoxLiveData: Implements LiveData from Android Architecture Components
Object ID based methods for ToMany: getById, indexOfId, removeById
More robust Android app directory detection that works around Android bugs
Using the new official FlatBuffers Maven dependency (FlatBuffer is not anymore embedded in the artifact)
UI improvements for ObjectBox browser
Other minor improvements
Fixed query order by float and double
Fixed an missing import if to-many relations referenced a entity in another package
Other minor fixes
Object Browser to view DB contents (Android)
Plain Java support to run ObjectBox on Windows and Linux
Added ToMany.hasA()/hasAll() to simplify query filters in Java
Sort query result via Comparator
Improved error messages on build errors
Internal clean up, dropping legacy plugin
Annotation processor detects boolean getters starting with “is”
Fixed a NPE with eager and findFirst() when there is no result
First bug fix release for ObjectBox 1.0.
ToMany allows setting a Comparator to order the List (experimental)
Fix UID assignment process: use @Uid without value to see options (pin UID, reset/change)
Fix relation code generation for entities in different packages
Fix Kotlin extension functions in transformed (library) project
Fix ToOne access if field is inaccessible (e.g. in Kotlin data classes if they are part of constructor – lateinit were OK)
ObjectBox is out of beta! See our announcement blog post for details.
Eager loading of relations via query builder
Java filters as query post-processing
Minor improvements like a new callInReadTx method and making Query.forEach breakable
Fixed two corner cases with queries
Fixed: Android flavors in caused the model file (default.json) to be written into the wrong folder (inside the build folder) causing the build to fail
Fixed: failed builds if entity constructor parameters are of specific types
For upgrade notes, please check the announcement post.
No more in-place code generation: Java source code is all yours now. This is based on the new build tool chain introduced in 0.9.13. Thus Kotlin and Java share the same build system. The old Java-based plugin is still available (plugin ID “io.objectbox.legacy”) in this version.
“Standalone” to-many relations (without backing to-one properties/relations)
Gradle plugin tries to automatically add runtime dependencies (also (k)apt, but this does not always work!?)
Improved error reporting
Fixed the issue causing a “Illegal state: Tx destroyed/inactive, writeable cursor still available” error log
Kotlin support (based on a new annotation processor)
Started “object-kotlin”, a sub-project for Kotlin extensions (tiny yet, let us know your ideas!)
BoxStoreBuilder: added maxReaders configuration
Get multiple entities by their IDs via Box methods (see get/getMap(Iterable))
ToOne and ToMany are now serializable (which does not imply serializing is a good idea)
ObjectBox may now opt to construct entities using the no-args constructor if the all-args constructor is absent
Prevents opening two BoxStores for the same DB file, which may have side effects that are hard to debug
Various minor and internal improvements and fixes
Fixed ToOne without an explicit target ID property
Fixed type check of properties to allow ToMany (instead of List)
Fixed @Convert in combination with List
Fixed a race condition with cursor deletion when Java’s finalizer kicked in potentially resulting in a SIGSEGV
Fixed a leak with potentially occurring with indexes
Update 2017/05/19: We just released 0.9.12.1 for the Gradle plugin (only), which fixes two problems with parsing of to-many relations.
Added the new list type ToMany which represents a to-many relation. A ToMany object will be automatically assigned to List types in the entity, eliminating a lot of generated code in the entity.
ToMany comes with change tracking: all changes (add/remove) are automatically applied to the DB when its hosting entity is persisted via put(). Thus, the list content is synced to the DB, e.g. their relationship status is updated and new entities are put.
Streamlined annotations (breaking API changes): @Generated(hash = 123) becomes @Generated(123), @Property was removed, @NameInDb replaces attributes in @Entity and the former @Property, Backlinking to-many relations require @Backlink (only), @Relation is now only used for to-one relations (and is subject to change in the next version)
Smarter to-one relations: if you put a new object that also has a new to-one relation object, the latter will also be put automatically.
Getters and setters for properties are now only generated if no direct field access is possible
JSR-305 annotations (@Nullable and others) to help the IDE find problems in your code
@Uid(-1) will reassign IDs to simplify some migrations (docs will follow soon)
No more getter for ToOne objects in favor of direct field access
Quite a few internal improvements (evolved EntityInfo meta info object, etc.)
Breaking API: Replaced “uid” attribute of @Entity and @Property with @Uid annotation
An empty @Uid will retrieve the current UID automatically
Some minor efficency improvements for read transactions
Better DB resources clean up for internal thread pool
Better compatibility with Android Gradle plugin
Fixes for multithreaded reads of relation and index data
Fixed compilation error in generated sources for Entities without non-ID properties
Query.forEach() to iterate efficiently over query result objects
Various bug fixes
Data observers with reactive extensions for transformations, thread scheduling, etc.
Optional RxJava 2 library
OR conditions for QueryBuilder allow more powerful queries
Fixed: Changing the order of an entity’s properties could cause errors in some cases
Fixed: Querying using relation IDs
LazyList returned by Query: another query option to defer getting actual objects until actually accessing them. This enables memory efficient iterations over large results. Also minimizes the time for a query to return. Note: LazyList cannot be combined with order specifications just yet.
QueryBuilder and Query now support Date and boolean types directly
QueryBuilder supports now a notIn opperator
put() now uses entity fields directly unless they are private (can be more efficient than calling getters)
At this early point in the beta we decided to break backward compatibility. This allowed us to make important improvements without worrying about rather complex migrations of previous versions. We believe this was a special situation and future versions will likely be backward compatible although we cannot make promises. If you intend to publish an app with ObjectBox it’s a good idea to contact us before.
The internal data format was optimized to store data more compact. Previous database files are not compatible and should be deleted.
We improved some details how IDs are used in the meta model. This affects the model file, which is stored in your project directory (objectbox-models/default.json). Files created by previous versions should be deleted.
ObjectBox Relations: Learn how to create and update to-one and to-many relations between entities in ObjectBox and improve performance.
Objects may reference other objects, for example using a simple reference or a list of objects. In database terms, we call those references relations. The object defining a relation we call the source object, the referenced object we call the target object. So a relation has a direction.
If there is one target object, we call the relation to-one. And if there can be multiple target objects, we call it to-many. Relations are lazily initialized: the actual target objects are fetched from the database when they are first accessed. Once the target objects are fetched, they are cached for further accesses.
You define a to-one relation using the ToOne
class, a smart proxy to the target object. It gets and caches the target object transparently. For example, an order is typically made by one customer. Thus, we could model the Order
class to have a to-one relation to the Customer
like this:
// Customer.java
@Entity
public class Customer {
@Id public long id;
}
// Order.java
@Entity
public class Order {
@Id public long id;
public ToOne<Customer> customer;
}
@Entity
data class Customer(
@Id var id: Long = 0
)
@Entity
data class Order(
@Id var id: Long = 0
) {
lateinit var customer: ToOne<Customer>
}
@Entity()
class Customer {
int id;
}
@Entity()
class Order {
int id;
final customer = ToOne<Customer>();
}
Now let’s add a customer and some orders. To set the related customer object, call setTarget()
(or assign target
in Kotlin) on the ToOne instance and put the order object:
Customer customer = new Customer();
Order order = new Order();
order.customer.setTarget(customer);
// Puts order and customer:
long orderId = boxStore.boxFor(Order.class).put(order);
val customer = Customer()
val order = Order()
order.customer.target = customer
// Puts order and customer:
val orderId = boxStore.boxFor(Order::class.java).put(order)
final customer = Customer();
final order = Order();
// set the relation
order.customer.target = customer;
// Or you could create the target object in place:
// order.customer.target = Customer();
// Save the order and customer to the database
int orderId = store.box<Order>().put(order);
If the customer object does not yet exist in the database (i.e. its ID is zero), ToOne
will put it (so there will be two puts, one for Order
, one for Customer
). If it already exists, ToOne
will only create the relation (so there's only one put for Order
, as explicitly written in the code). See further below for details about updating relations.
Have a look at the following code how you can get (read) the customer of an order:
Order order = boxStore.boxFor(Order.class).get(orderId);
Customer customer = order.customer.getTarget();
val order = boxStore.boxFor(Order::class.java)[orderId]
val customer = order.customer.target
final order = store.box<Order>().get(orderId);
final customer = order.customer.target;
This will do a database call on the first access (lazy loading). It uses lookup by ID, which is very fast in ObjectBox. If you only need the ID instead of the whole target object, you can completely avoid this database operation because it's already loaded: use order.customer.targetId/getTargetId()
.
We can also remove the relationship to a customer:
order.customer.setTarget(null);
boxStore.boxFor(Order.class).put(order);
order.customer.target = null
boxStore.boxFor(Order::class.java).put(order)
order.customer.target = null; // same as .targetId = 0;
store.box<Order>().put(order);
Note that this does not remove the customer from the database, it just dissolves the relationship.
If you look at your model in objectbox-models/default.json
(or lib/bjectbox-model.json
in Dart) you can see, a ToOne property is not actually stored. Instead, the ID of the target object is saved in a virtual property named like the ToOne property appended with Id.
You can directly access the target ID property by defining a long
(or Long
in Kotlin) property in your entity class with the expected name:
@Entity
public class Order {
@Id public long id;
public long customerId; // ToOne target ID property
public ToOne<Customer> customer;
}
@Entity
data class Order(
@Id var id: Long = 0,
var customerId: Long = 0
) {
lateinit var customer: ToOne<Customer>
}
You can change the name of the expected target ID property by adding the @TargetIdProperty(String) annotation to a ToOne.
Did you notice that the ToOne
field customer
was never initialized in the code example above? Why can the code still use customer
without any NullPointerException? Because the field actually is initialized – the initialization code just is not visible in your sources.
The ObjectBox Gradle plugin will transform your entity class (supported for Android projects and Java JVM projects) to do the proper initialization in constructors before your code is executed. Thus, even in your constructor code, you can just assume ToOne
and ToMany
/ List
properties have been initialized and are ready for you to use.
@Entity
public class Example {
// Initialize ToOne and ToMany manually.
ToOne<Order> order = new ToOne<>(this, Example_.order);
ToMany<Order> orders = new ToMany<>(this, Example_.orders);
// Add a BoxStore field.
transient BoxStore __boxStore;
}
@Entity
class Example() {
// Initialize ToOne and ToMany manually.
var order = ToOne<Order>(this, Example_.order)
var orders = ToMany<Order>(this, Example_.orders)
// Add a BoxStore field.
@JvmField
@Transient
@Suppress("PropertyName")
var __boxStore: BoxStore? = null
}
To improve performance when ObjectBox constructs your entities, you should provide an all-properties constructor.
For a ToOne you have to add an id parameter, typically named like the ToOne field appended with Id
. Check your objectbox-models/default.json
file to find the correct name.
An example:
@Entity
public class Order {
@Id public long id;
public ToOne<Customer> customer;
public Order() { /* default constructor */ }
public Order(long id, long customerId /* virtual ToOne id property */) {
this.id = id;
this.customer.setTargetId(customerId);
}
}
To define a to-many relation, you can use a property of type ToMany
. As the ToOne class, the ToMany
class helps you to keep track of changes and to apply them to the database.
Note that to-many relations are resolved lazily on first access, and then cached in the source entity inside the ToMany
object. So subsequent calls to any method, like size()
of the ToMany
, do not query the database, even if the relation was changed elsewhere. To get the latest data fetch the source entity again or call reset()
on the ToMany
.
There is a slight difference if you require a one-to-many (1:N) or many-to-many (N:M) relation. A 1:N relation is like the example above where a customer can have multiple orders, but an order is only associated with a single customer. An example for an N:M relation is students and teachers: students can have classes by several teachers but a teacher can also instruct several students.
To define a one-to-many relation, you need to annotate your relation property with @Backlink
. It links back to a to-one relation in the target object. Using the customer and orders example, we can modify the customer class to have a to-many relation to the customer's orders:
// Customer.java
@Entity
public class Customer {
@Id public long id;
@Backlink(to = "customer")
public ToMany<Order> orders;
}
// Order.java
@Entity
public class Order {
@Id public long id;
public String name;
public ToOne<Customer> customer;
public Order(String name) {
this.name = name;
}
public Order() {
}
}
@Entity
data class Customer(
@Id var id: Long = 0
) {
@Backlink(to = "customer")
lateinit var orders: ToMany<Order>
}
@Entity
data class Order(
@Id var id: Long = 0,
var name: String? = ""
) {
lateinit var customer: ToOne<Customer>
}
@Entity()
class Customer {
int id;
@Backlink('customer')
final orders = ToMany<Order>();
}
@Entity()
class Order {
int id;
final customer = ToOne<Customer>();
}
Let’s add some orders together with a new customer. ToMany
implements the Java List interface, so we can simply add orders to it:
Customer customer = new Customer();
customer.orders.add(new Order("Order #1"));
customer.orders.add(new Order("Order #2"));
// Puts customer and orders:
long customerId = boxStore.boxFor(Customer.class).put(customer);
val customer = Customer()
customer.orders.add(Order("Order #1"))
customer.orders.add(Order("Order #2"))
// Puts customer and orders:
val customerId = boxStore.boxFor(Customer::class.java).put(customer)
Customer customer = Customer();
customer.orders.add(Order()); // Order #1
customer.orders.add(Order()); // Order #2
// Puts customer and orders:
final customerId = store.box<Customer>().put(customer);
If the order entities do not yet exist in the database, ToMany
will put them. If they already exist, it will only create the relation (but not put them). See further below for details about updating relations.
We can easily get the orders of a customer back by accessing the list of orders:
Customer customer = boxStore.boxFor(Customer.class).get(customerId);
for (Order order : customer.orders) {
// Do something with each order.
}
val customer = boxStore.boxFor(Customer::class.java).get(customerId)
for (order in customer.orders) {
// Do something with each order.
}
Customer customer = store.box<Customer>().get(customerId);
// you can use any List<> functions, for example:
final valueSum = customer.orders.fold(0, (sum, order) => sum + order.value);
// though you could use property queries and their .sum() function for that
Removing orders from the relation works as expected:
// Remove the relation to the first order in the list
Order order = customer.orders.remove(0);
boxStore.boxFor(Customer.class).put(customer);
// Optional: also remove the order entity from its box:
// boxStore.boxFor(Order.class).remove(order);
// Remove the relation to the first order in the list
val order = customer.orders.removeAt(0)
boxStore.boxFor(Customer::class.java).put(customer)
// Optional: also remove the order entity from its box:
// boxStore.boxFor(Order::class.java).remove(order)
// Remove the relation to the first order in the list
Order order = customer.orders.removeAt(0);
store.box<Customer>().put(customer);
// Optional: also remove the order entity from its box:
// store.box<Order>().remove(order);
To define a many-to-many relation you simply add a property using the ToMany
class. Assuming a students and teachers example, this is how a simple student class that has a to-many relation to teachers can look like:
// Teacher.java
@Entity
public class Teacher{
@Id public long id;
public String name;
public Teacher(String name) {
this.name = name;
}
public Teacher() {
}
}
// Student.java
@Entity
public class Student{
@Id public long id;
public ToMany<Teacher> teachers;
}
@Entity
data class Teacher(
@Id var id: Long = 0,
var name: String? = ""
)
@Entity
data class Student(
@Id var id: Long = 0
) {
lateinit var teachers: ToMany<Teacher>
}
@Entity()
class Teacher{
int id;
}
@Entity()
class Student{
int id;
final teachers = ToMany<Teacher>();
}
Adding the teachers of a student works exactly like with a list:
Teacher teacher1 = new Teacher("Teacher 1");
Teacher teacher2 = new Teacher("Teacher 2");
Student student1 = new Student();
student1.teachers.add(teacher1);
student1.teachers.add(teacher2);
Student student2 = new Student();
student2.teachers.add(teacher2);
// Puts students and teachers:
boxStore.boxFor(Student.class).put(student1, student2);
val teacher1 = Teacher("Teacher 1")
val teacher2 = Teacher("Teacher 2")
val student1 = Student()
student1.teachers.add(teacher1)
student1.teachers.add(teacher2)
val student2 = Student()
student2.teachers.add(teacher2)
// Puts students and teachers:
boxStore.boxFor(Student::class.java).put(student1, student2)
Teacher teacher1 = Teacher();
Teacher teacher2 = Teacher();
Student student1 = Student();
student1.teachers.add(teacher1);
student1.teachers.add(teacher2);
Student student2 = Student();
student2.teachers.add(teacher2);
// saves students as well as teachers in the database
store.box<Student>().putMany([student1, student2]);
If the teacher entities do not yet exist in the database, ToMany
will also put them. If they already exist, ToMany
will only create the relation (but not put them). See further below for details about updating relations.
To get the teachers of a student we just access the list:
Student student = boxStore.boxFor(Student.class).get(studentId);
for (Teacher teacher : student.teachers) {
// Do something with each teacher.
}
val student = boxStore.boxFor(Student::class.java).get(studentId)
for (teacher in student.teachers) {
// Do something with each teacher.
}
Student student = store.box<Student>().get(studentId);
// you can use any List<> functions, for example:
student.teachers.forEach((Teacher teacher) => ...);
And if a student drops out of a class, we can remove a teacher from the relation:
student.teachers.remove(0);
// Simply put the student again:
// boxStore.boxFor(Student.class).put(student);
// Or more efficient (than writing the thole object), store just the relations:
student.teachers.applyChangesToDb();
student.teachers.removeAt(0)
// Simply put the student again:
// boxStore.boxFor(Student::class.java).put(student)
// Or more efficient (than writing the thole object), store just the relations:
student.teachers.applyChangesToDb()
student.teachers.removeAt(0)
// Simply put the student again:
// store.box<Student>().put(student);
// Or more efficient (than writing the thole object), store just the relations:
student.teachers.applyToDb();
Following the above example, you might want an easy way to find out what students a teacher has. Instead of having to perform a query, you can just add a to-many relation to the teacher and annotate it with the @Backlink
annotation:
// Teacher.java
@Entity
public class Teacher{
@Id public long id;
// Backed by the to-many relation in Student:
@Backlink(to = "teachers")
public ToMany<Student> students;
}
// Student.java
@Entity
public class Student{
@Id public long id;
public ToMany<Teacher> teachers;
}
@Entity
data class Teacher(
@Id var id: Long = 0
) {
// Backed by the to-many relation in Student:
@Backlink(to = "teachers")
lateinit var students: ToMany<Student>
}
@Entity
data class Student(
@Id var id: Long = 0
) {
lateinit var teachers: ToMany<Teacher>
}
@Entity()
class Teacher{
int id;
// Backed by the to-many relation in Student:
@Backlink()
final students = ToMany<Student>();
}
@Entity()
public class Student{
int id;
final teachers = ToMany<Teacher>();
}
Instead of the ToMany
type it is also possible to use List
(or MutableList
in Kotlin) for a to-many property. At runtime the property will still be a ToMany
instance (ToMany
does implement the List
interface) due to the initialization magic described above, or if manually initialized as seen in the example below.
// Teacher.java
@Entity
public class Teacher{
@Id public long id;
}
// Student.java
@Entity
public class Student{
@Id public long id;
public List<Teacher> teachers = new ToMany<>(this, Student_.teachers);
}
@Entity
data class Teacher(
@Id var id: Long = 0
)
@Entity
data class Student(
@Id var id: Long = 0
) {
var teachers: MutableList<Teacher> = ToMany<>(this, Student_.teachers)
}
Box<Student> studentBox = store.boxFor(Student.class);
Student student = new Student();
Teacher teacher = new Teacher();
// Simulate what a JSON deserialzer would do:
// replace ToMany instance with ArrayList.
student.teachers = new ArrayList();
student.teachers.add(teacher);
// Put will skip the teachers property.
studentBox.put(student);
System.out.println(store.boxFor(Teacher.class).count());
// prints 0
// Need to manually create the relation.
Student student2 = studentBox.get(student.id);
student2.teachers.addAll(student.teachers);
studentBox.put(student2);
The ToOne and ToMany relation classes assist you to persist the relation state. They keep track of changes and apply them to the database once you put the Object containing them. ObjectBox supports relation updates for new (not yet persisted; ID == 0) and existing (persisted before; ID != 0) Objects.
For convenience, ToOne and ToMany will put Objects added to them if they do not yet exist (ID == 0). If they already exist (their ID != 0, or you are using @Id(assignable = true)
), only the relation will be created or destroyed (internally the Object ID is added to or removed from the relation). In that case, to put changes to the properties of related Objects use their specific Box instead:
// update a related entity using its box
Order orderToUpdate = customer.orders.get(0);
orderToUpdate.text = "Revised description";
// DOES NOT WORK
// boxStore.boxFor(Customer.class).put(customer);
// WORKS
boxStore.boxFor(Order.class).put(orderToUpdate);
// update a related entity using its box
Order orderToUpdate = customer.orders.getAt(0);
orderToUpdate.text = "Revised description";
// DOES NOT WORK
// boxStore.boxFor(Customer::class.java).put(customer);
// WORKS
boxStore.boxFor(Order::class.java).put(orderToUpdate);
// update a related entity using its box
Order orderToUpdate = customer.orders[0];
orderToUpdate.text = 'Revised description';
// DOES NOT WORK - the change to the order is not saved
// store.box<Customer>().put(customer);
// WORKS
store.box<Order>().put(orderToUpdate);
The ToOne class offers the following methods to update the relation:
setTarget(target)
makes the given (new or existing) Object the new relation target; pass null
to clear the relation.
setTargetId(targetId)
sets the relation target based on the given ID of an existing Object; pass 0
(zero) to clear the relation.
Java/Kotlin only: setAndPutTarget(target)
makes the given (new or existing) Object the new relation target and puts the owning, source Object and if needed the target Object.
// Option 1: set target and put.
order.customer.setTarget(customer);
// Or set target via Object ID:
// order.customer.setCustomerId(customer.getId());
orderBox.put(order);
// Option 2: combined set target and put.
order.customer.setAndPutTarget(customer);
// Option 1: set target and put.
order.customer.setTarget(customer)
// Or set target via Object ID:
// order.customer.setCustomerId(customer.getId())
orderBox.put(order)
// Option 2: combined set target and put.
order.customer.setAndPutTarget(customer)
Note: attach the Box before calling setAndPutTarget()
on a new (not put) Object owning a ToOne:
Order order = new Order();
orderBox.attach(order);
order.customer.setAndPutTarget(customer);
Note: if the target Object class uses manually assigned IDs with @Id(assignable = true)
it will not be put when the Object that owns the relation is put:
Customer customer = new Customer();
// If ID is manually assigned, put target Object first
customer.id = 12;
customerBox.put(customer);
// Then can safely set as target
order.customer.setTarget(customer);
// Or set target via Object ID
// order.customer.setCustomerId(customer.getId());
orderBox.put(order);
This is because ObjectBox only puts related entities with an ID of 0. See the documentation about IDs for background information.
The ToMany
relation class is based on a standard List
with added change tracking for Objects. As mentioned above, it will put new Objects (ID == 0) that are added to it once the Object owning it is put. And when removing Objects from it, just the relation is cleared, the Objects are not removed from their Box.
See the documentation on One-to-Many and Many-to-Many above for details.
Note (Java/Kotlin only): if your entities are using manually assigned IDs with @Id(assignable = true)
additional steps are required. Read on for details:
If the owning, source Object uses @Id(assignable = true)
attach its Box before modifying its ToMany:
// If source has a manually assigned ID attach Box first
customer.id = 12;
customerBox.attach(customer);
// Then can safely modify ToMany
customer.orders.add(order);
customerBox.put(customer);
If the target Object, like Order
above, is using manually assigned IDs put the target Objects before adding them to the ToMany relation:
// If ID is manually assigned put target Object first
order.id = 42;
orderBox.put(order);
// Then can safely add target Object to ToMany
customer.orders.add(order);
customerBox.put(customer);
The above steps are required because, when putting the Object owning the ToMany only the relation is updated. This is because ObjectBox only puts target Objects with an ID of 0. See the documentation about IDs for the background information.
A typical extension to the customer/order example we have started earlier would be to add an Address
type. While you could model street, ZIP and so on directly into Customer
, it usually makes sense to normalize that out into a separate entity. And when you think about the usual shopping sites, they all let you have multiple addresses...
So, typically, this is how you would model that:
an address is bound to a customer; add a ToOne<Customer> customer
to Address
an order is shipped to one address, and might have a diverging billing address optionally; add ToOne<Address> shippingAddress
and ToOne<Address> billingAddress
to Order
For each ToOne
you could have a matching ToMany
on the other side of the relation (backlink)
You can model a tree relation with a to-one and a to-many relation pointing to itself:
@Entity
public class TreeNode {
@Id long id;
ToOne<TreeNode> parent;
@Backlink
ToMany<TreeNode> children;
}
@Entity
data class TreeNode(
@Id var id: Long = 0
) {
lateinit var parent: ToOne<TreeNode>
@Backlink
lateinit var children: ToMany<TreeNode>
}
This lets you navigate a tree nodes parent and children:
TreeNode parent = treeNode.parent.getTarget();
List<TreeNode> children = treeNode.children;
val parent: TreeNode = treeNode.parent.target
val children: List<TreeNode> = treeNode.children