Only this pageAll pages
Powered by GitBook
1 of 29

ObjectBox Docs

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...

Android (Java/Kotlin)

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.

ObjectBox Docs

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.

We're hiring! We believe resource-efficient coding is still cool 😎 and are looking for a C / C++ developer who shares our sentiment.

C++ and Swift: Check the docs in https://cpp.objectbox.io/ and https://swift.objectbox.io/.

🚀 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 ⭐

Getting started

Advanced

Guides and documentation for advanced use cases of ObjectBox.

Object IDs

Explanation of Object IDs and how they are used and assigned in ObjectBox.

ObjectBox - Object IDs

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.

Object ID: new vs. persisted entities

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.

Special Object 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.

Object ID assignment (default)

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.

Manually assigned Object IDs

This is not recommended. Check if using a unique indexed property is a viable alternative.

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.

String ID alias (future work)

Check this issue on Github for status.

App Bundle, split APKs and LinkageError

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 .

App Bundle and split APKs

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.

Using the Play Core library to check for missing splits

Update August 2020: Google-certified devices (those running Google Play Services) or those running Android 10 (API level 29) or higher prevent users from sideloading split APKs, preventing this crash. Adding the below check is no longer necessary for these devices.

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:

Alternative: Catch exception and inform users

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 .

Alternative: turn off splitting by ABI

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:

Buggy devices (API 22 or lower)

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:

. We are using the ProGuard format (multiDexKeepProguard property). You can also use the multiDexKeepFile property, but make sure to adapt the rule above to that format.

Enable ReLinker debug log

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();
App Bundle
Multidex
GitHub issue 605
Play Core library
how we updated our example app
how we added this to our Android app example
Android Developers
ReLinker
Multidex supports two file formats to keep files

FAQ

Answers to questions specific to ObjectBox for Java and Dart

Does ObjectBox support Kotlin? RxJava?

ObjectBox comes with full Kotlin support including data classes. And yes, it supports RxJava and reactive queries without RxJava.

Does ObjectBox support object relations?

Yes. ObjectBox comes with strong relation support and offers features like “eager loading” for optimal performance.

Does ObjectBox support multi-module projects? Can entities be spread across modules?

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.

Is ObjectBox a “zero copy” database? Are properties fetched lazily?

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.

Are there any threading constrictions?

No. The objects you get from ObjectBox are POJOs (plain objects). You are safe to pass them around in threads.

Should I use ObjectBox on the main thread (or UI thread)?

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.

On which platforms does ObjectBox run?

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.

Can I use ObjectBox on the desktop/server?

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!

Can I use ObjectBox on smart IoT devices?

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.

How do I rename object properties or classes?

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.

How much does ObjectBox add to my APK size?

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.

Tip: Open your APK or AAB in Android Studio and have a look at the lib folder to see the raw file size and download size added.

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:

This is also currently recommended in any case when building a Flutter app with minimum API level 23 (Android 6.0).

<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.

Can I ship my app with a pre-built database?

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).

For Java, there is an experimental initialDbFile() method when building BoxStore. Let us know if this is useful!

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.

How does ObjectBox use disk space? And, can I reclaim disk space?

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.

The storage layout on disk is optimized for performance. Database structures and concepts like B+ trees, multi-version concurrency and indexes use more space than storing data e.g. in a text file. Advantages like scalable data operations easily make up for it. Also keep in mind that in many cases data stored in a database is a small proportion compared to media files.

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.

BoxStore.removeAllObjects() does not reclaim disk space. It keeps the allocated disk space so it returns fast and to avoid the performance hit of having to allocate the same disk space when data is put again.

Answers to other questions

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

Entity Inheritance

How to inherit properties from entity super classes.

ObjectBox - Entity Super Classes

Only available for Java/Kotlin at the moment

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
    }
}

Notes on usage

  • 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.

Restrictions

  • 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>
}

Troubleshooting

Android Local Unit Tests

How to create ObjectBox local unit tests for Android projects.

Android Local Unit Tests

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.

Set Up Your Testing Environment

The setup step is only required for ObjectBox 1.4 or older (or if you want to manually add the dependencies). In newer versions the ObjectBox plugin automatically adds the native ObjectBox library required for your current operating system.

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")
}

The ObjectBox native libraries currently only support 64-bit desktop operating systems.

On Windows you might have to install the Microsoft Visual C++ 2015 Redistributable (x64) packages to use the native library.

Create a Local Unit Test Class

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")
    }
}

To help diagnose issues you can enable log output for ObjectBox actions, such as queries, by specifying one or more debug flags when building BoxStore.

Base class for tests

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")
    }
}

Testing Entities with Relations

Kotlin desktop projects only. Since 3.0.0 this is no longer necessary for Android projects (either Kotlin or Java), initialization magic works for them now as well.

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.

Meta Model, IDs, and UIDs

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.

IDs

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.

Note: do not confuse object IDs with meta model IDs: object IDs are the values of the @Id property (see Object IDs in basics). In contrast, all objects are instances of the entity type associated with a single meta model ID.

ObjectBox assigns meta model IDs sequentially (1, 2, 3, 4, …) and keeps track of the last used ID to prevent ID collisions.

UIDs

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.

JSON for consistent IDs

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.

Meta Model Synchronization

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.

Stable Renames using UIDs

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.

Resolving Meta Model Conflicts

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.

Manual conflict resolution

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.

The Nuke Option

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;
    }
}

Tutorial: Demo Project

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.

The Note entity and Box class

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.

Inserting notes

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.

Note: In the example project, ObjectBox is the name of a helper class to set up and keep a reference to BoxStore.

Note: In the example project, ObjectBox is the name of a helper class to set up and keep a reference to BoxStore.

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.

Removing/deleting notes

When the user taps a note, it is deleted. The Box provides remove() to achieve this:

Querying notes

To query and display notes in a list a Query instance is built once:

And then executed each time any notes change:

You can also use self._task_box.get_all() to get all Task objects without any condition (instead of building a query).

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.

Updating notes and more

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.

Setting up the database

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.

Note: it is perfectly fine to never close the database. That’s even recommended for most apps.

More In-Depth Tutorials

🌿

Transactions

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.

ObjectBox - Transactions

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.

TL;DR - a quick summary on Transactions

  • 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.

Explicit Transactions

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:

Transaction Costs

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.putcalls) 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.

Read Transactions

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).

Multiversion Concurrency

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.

Locking inside a Write Transaction

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);
database transactions
BoxStore
Multiversion concurrency control (MVCC)
example/tasks/main.py

@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.
> 
NoteActivity.java
@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    notesBox = ObjectBox.get().boxFor(Note.class);
    ...
}
NoteActivity.kt
public override fun onCreate(savedInstanceState: Bundle?) {
    ...
    notesBox = ObjectBox.boxStore.boxFor()
    ...
}
lib/objectbox.dart
class ObjectBox {
  late final Store _store;
  late final Box<Note> _box;
  
  ObjectBox._create(this._store) {
    _noteBox = Box<Note>(_store);
    ...
example/tasks/main.py
class TasklistCmd(Cmd):
    # ...

    def __init__(self):
        # ...
        self._store = Store(directory="tasklist-db")
        self._task_box = self._store.box(Task)
NoteActivity.java
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());
    ...
}
NoteActivity.kt
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)
    ...
}
lib/objectbox.dart
Future<void> addNote(String text) => _noteBox.putAsync(Note(text));
example/tasks/main.py
def add_task(self, text: str):
    task = Task(text=text, date_created=now_ms())
    self._task_box.put(task)
NoteActivity.java
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());
        ...
    }
};
NoteActivity.kt
private val noteClickListener = OnItemClickListener { _, _, position, _ ->
    notesAdapter.getItem(position)?.also {
        notesBox.remove(it)
        Log.d(App.TAG, "Deleted note, ID: " + it.id)
    }
    ...
}
lib/objectbox.dart
Future<void> removeNote(int id) => _noteBox.removeAsync(id);
example/tasks/main.py
def remove_task(self, task_id: int) -> bool:
    is_removed = self._task_box.remove(task_id)
    return is_removed
NoteActivity.java
@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    // Query all notes, sorted a-z by their text.
    notesQuery = notesBox.query().order(Note_.text).build();
    ...
}
NoteActivity.kt
public override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Query all notes, sorted a-z by their text.
    notesQuery = notesBox.query {
        order(Note_.text)
    }
    ...
}
lib/objectbox.dart
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);
  ...
example/tasks/main.py
def __init__(self):
    # ...
    self._query = self._task_box.query().build()
NoteActivity.java
private void updateNotes() {
    List<Note> notes = notesQuery.find();
    notesAdapter.setNotes(notes);
}
NoteActivity.kt
private fun updateNotes() {
    val notes = notesQuery.find()
    notesAdapter.setNotes(notes)
}
lib/objectbox.dart
  ...
  // 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());
}
example/tasks/main.py
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)
Getting Started
Java/Kotlin
Dart
helper class as recommended in the Getting Started guide
Learn how to build a Food Sharing app with ObjectBox in Flutter/Dart
src/Note.java
@Entity
public class Note {
    
    @Id
    long id;
    
    String text;
    String comment;
    Date date;
    
    ...
}
Entity Annotations
ObjectBox Queries
Getting started
Getting started

Data Model Updates

How to rename entities and properties, change property types in ObjectBox.

ObjectBox - Data Model Updates

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.

UIDs

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.

Renaming Entities and Properties

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:

How-to and Example (Java/Kotlin and Dart)

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.

Note: Instead of the above you can also find the UID of the entity/property in the model JSON mentioned in the introduction. You can add the UID value to the annotation yourself, before renaming the entity/property, and skip the intermediate error where ObjectBox just prints it for you. This can be faster when renaming multiple properties.

How-to and Example (Python)

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.

Changing Property Types

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:

How-to and Example

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.

greenDAO Compat

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.

src/Note.kt
@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
lib/model.dart
@Entity()
class Note {
  int id;
  String text;
  String? comment;
  DateTime date;

  ...
}

Getting started

Discover ObjectBox: The Lightning-Fast Mobile Database for Persistent Object Storage. Streamline Your Workflow, Eliminate Repetitive Tasks, and Enjoy a User-Friendly Data Interface.

Add ObjectBox to your project

Prefer to look at example code? Check out our examples repository.

ObjectBox tools and dependencies are available on the Maven Central repository.

To add ObjectBox to your Android project, follow these steps:

  1. 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:

/build.gradle(.kts)
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")
    }
}
  1. Open the Gradle build file for your app or module subproject and, after the com.android.application plugin, apply the io.objectbox plugin:

/app/build.gradle(.kts)
// 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

If you encounter any problems in this or later steps, check out the FAQ and Troubleshooting pages.

  1. Then do "Sync Project with Gradle Files" in Android Studio so the Gradle plugin automatically adds the required ObjectBox libraries and code generation tasks.

  2. Your project can now use ObjectBox, continue by defining entity classes.

Prefer to look at example code? Check out our examples repository.

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

The instructions assume a multi-project build is used.

  1. Open the Gradle build script of your root project and

    1. add a global variable to store the common version of ObjectBox dependencies and

    2. add the ObjectBox Gradle plugin:

/build.gradle(.kts)
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")
    }
}
  1. Open the Gradle build file for your application subproject and, after other plugins, apply the io.objectbox plugin:

/app/build.gradle(.kts)
// 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.

Using your IDE of choice with a Gradle project might require additional configuration. E.g.

  • For IntelliJ IDEA see the help page for Gradle.

  • For Eclipse see the Buildship project and Getting Started article.

  1. 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")

The ObjectBox database runs mostly in native code written in C/C++ for optimal performance. Thus, ObjectBox will load a runtime library: a “.dll” on Windows, a “.so” on Linux, and a “.dylib” on macOS.\

By default, the Gradle plugin adds a runtime library (only) for your current operating system. It also adds the Java SDK (objectbox-java) and if needed the ObjectBox Kotlin extension functions (objectbox-kotlin).

ObjectBox only supports 64-bit systems for best performance going forward. Talk to us if you require 32-bit support.

  1. Your project can now use ObjectBox, continue by defining entity classes.

You can watch these video tutorials as well 😀:

  • Event Management app

  • Restaurant: chef and order apps

  • Task-list app (in Spanish)

Prefer to look at example code? Check out our examples directory.

To add ObjectBox to your Flutter project:

  1. 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

To run unit tests on your machine, download the latest native ObjectBox library for your machine by running this script in a bash shell (e.g. Git Bash on Windows):

bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh)

To get a variant of the library that supports ObjectBox Sync, append the --sync argument to above command.

  1. 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
  1. If you added the above lines manually, then install the packages with flutter pub get.

For Android increase the NDK version:

/android/app/build.gradle
android {
    // ObjectBox: Flutter defaults to NDK 23.1.7779620, but
    // - objectbox_flutter_libs requires Android NDK 25.1.8937393
    // - path_provider_android requires Android NDK 25.1.8937393
    // Until Flutter uses a newer version (https://github.com/flutter/flutter/commit/919bed6e0a18bd5b76fb581ede10121f8c14a6f7)
    // manually set the required one:
    // ndkVersion flutter.ndkVersion
    ndkVersion = "25.1.8937393"
}   

For all macOS apps need to target macOS 10.15: in Podfile change the platform and in the Runner.xcodeproj/poject.pbxproj file update MACOSX_DEPLOYMENT_TARGET.

For Linux Desktop apps: the Flutter snap ships with an outdated version of CMake. Install Flutter manually instead to use the version of CMake installed on your system.

Prefer to look at example code? Check out our examples directory.

  1. Run these commands:

dart pub add objectbox
dart pub add --dev build_runner objectbox_generator:any
  1. 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
  1. If you added the above lines manually, then install the packages with dart pub get

  2. 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

By default the library is downloaded into the lib subdirectory of the working directory. It's not necessary to install the library system-wide. This also allows to use different versions for different projects. For details see below.

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).

Prefer to look at example code? Check out our examples directory.

ObjectBox for Python is available via PyPI: Stable Version (4.0.0):

pip install --upgrade objectbox

Define Entity Classes

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:

User.java
@Entity
public class User {
    @Id 
    public long id;
    public String name;
}
models.kt
@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.

models.dart
@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.

model.py
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.

ObjectBox also supports changing your model at a later point. You can add and remove properties in entities and the database model is updated automatically (after re-generating some code, see section below). There is no need to write migration code.

To rename entities or properties, change the type of a property and more details in general see Data Model Updates.

Generate ObjectBox code

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.

Note: If you make significant changes to your entities, e.g. by moving them or modifying annotations, make sure to rebuild the project so generated ObjectBox code is updated.

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.

If you make changes to your entities, e.g. by adding a property or modifying annotations, or after the ObjectBox library has updated make sure to re-run the generator so generated ObjectBox code is updated.

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.

Actually we lied above. The generator will process lib and test folders separately and generate files for each one (if @Entity classes exist there). This allows to create a separate test database that does not share any of the entity classes with the main database.

Python bindings offer a convenient default Model to which Entity definitions are automatically associated if not specified otherwise. Similar to the other bindings, a JSON model file is also used for management of Schema history (i.e. to handle add/remove/rename of Entity and Property).

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.

In Android Studio you might have to switch the Project view from Android to Project to see the default.json model file. Python checks for the call-stack to determine the user-module directory in which the JSON file is stored.

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.

Create a Store

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);
  }
}

For sandboxed macOS apps also pass macosApplicationGroup to openStore(). See the notes about "macOS application group" in the constructor documentation of the Store class.

For example:

openStore(macosApplicationGroup: "FGDTDLOBXDJ.demo")

On mobile devices or sandboxed apps data should be stored in the app's documents directory. See Flutter: read & write files for more info. This is exactly what openStore()does, if the directory argument is not specified.

On desktop systems it is recommended to specify a directory to create a custom sub-directory to avoid conflicts with other apps.

If your code passes a directory that the application can't write to, you get an error that looks somewhat like this: failed to create store: 10199 Dir does not exist: objectbox (30).

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());
}

When using Dart isolates, note that each Dart isolate has its own global fields, they do not share state on the Dart level.

However, as ObjectBox runs on the native or process level (so one native instance shared across all isolates), instead of creating a new Store in another isolate your code should instead attach to the open native store.

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.

When using Dart isolates, note that each Dart isolate has its own global fields, they do not share state on the Dart level.

However, as ObjectBox runs on the native or process level (so one native instance shared across all isolates), instead of creating a new Store in another isolate your code should instead attach to the open native store.

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)

Basic Box operations

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.

Asynchronous operations

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));

Currently work in progress.

Object IDs

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.

If you need to assign IDs by yourself, have a look at how to switch to self-assigned IDs and what side effects apply.

Reserved Object IDs

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.

Transactions

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.

Have an app with greenDAO? DaoCompat is for you!

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!

Next steps

  • Check out the ObjectBox example projects on GitHub.

  • Learn about Queries and Relations.

  • Learn how to write unit tests.

  • To enable debug mode and for advanced use cases, see the Advanced Setup page.

Paging (Arch. Comp.)

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.

Since 2.0.0

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 .

Note: the following assumes that you have already in your project.

Using ObjectBoxDataSource

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.

Next steps

  • Have a look at the .

  • Check out .

  • Learn how to .

Changelogs

The changelogs for each programming language are available at GitHub:

For other languages, please check there respective doc pages:

Changelog History before 4.0.3

Older entries (4.0.2 and earlier) are available in the .

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;
    }
}
Paging library
Android Architecture Components
ObjectBox Android library
PositionalDataSource
added and set up the Paging library
creating a LiveData directly
build your ObjectBox query
Paging library documentation
ObjectBox Architecture Components example code
ObjectBox support for LiveData
build queries
Java changelog (GitHub releases)
Dart changelog
Python changelog (GitHub releases)
C and C++
Swift
Go
release history

LiveData (Arch. Comp.)

LiveData is an observable data holder class. Learn to use ObjectBox database with LiveData from Android Architecture Components.

ObjectBox - LiveData with Android Architecture Components

Since 1.2.0. Have a look at the example project on GitHub.

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.

Troubleshooting

Solutions for common issues with ObjectBox for Java and Dart

Unresolved reference: MyObjectBox (class not found, etc.)

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.

Merge conflict or DbSchemaException after concurrent data model modifications

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.

DbSchemaException: incoming ID does not match existing UID

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.

DbSchemaException after switching git branch (no concurrent data model modifications)

See below.

DbSchemaException: DB's last ID is higher

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.

DbFullException: Could not put

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 */);

Couldn’t find “libobjectbox.so”

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.

Version conflict with ‘com.google.code.findbugs:jsr305’

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'

Background info.

Incompatible property type

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

Flutter iOS builds for armv7 fail with "ObjectBox does not contain that architecture"

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.

Error Codes

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

Help with other issues

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

FAQ

Kotlin Support

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 and Kotlin

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.

This page assumes that you have added ObjectBox to your project and that you are familiar with basic functionality. The Getting Started page will help you out if you are not. This page discusses additional capabilities for Kotlin only.

Kotlin Entities

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:

Defining Relations in Kotlin Entities

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.

Two data classes that have the same property values (excluding those defined in the class body) are equal and have the same hash code. Keep this in mind when working with ToMany which uses a HashMap to keep track of changes. E.g. adding the same data class multiple times has no effect, it is treated as the same entity.

Using the provided extension functions

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()

Queries

The new Query API makes below extensions functions unnecessary.

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()

Relations

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)
}

Flow

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.

Coroutines

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))
}

Next Steps

  • Check out the Kotlin example on GitHub.

  • Continue with Getting Started.

Desktop Apps

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.

ObjectBox – Embedded Database for Java Desktop Apps

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.

Setup and Usage

See the Getting Started page on how to set up your project, add entities and use the ObjectBox APIs.

Examples

There are example command line apps available in our examples repository.

Building Unit Tests

The setup and writing tests is identical to writing unit tests that run on the local JVM for Android, see Android Local Unit Tests.

Video Tutorial on Getting Started with ObjectBox for Flutter
Getting started
Getting started
Entity Annotations
Relations
Getting started
Video Tutorial on Getting Started with ObjectBox for Android and Java

Custom Types

Which types are supported by default in ObjectBox, how to store types that are not, recommendations for storing enums.

ObjectBox - Supported Types

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

Flex properties

Only Java/Kotlin

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.

Convert annotation and property converter

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);
  }
}

Things to look out for

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.

Note: For optimal performance, ObjectBox will use a single converter instance for all conversions. Make sure the converter does not have any other constructor besides the parameter-less default constructor. Also, make it thread-safe, because it might be called concurrently on multiple entities.

List/Array types

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).

ObjectBox (Java, Dart) has built-in support for String lists. ObjectBox for Java also has built-in support for String arrays.

How to convert Enums correctly

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.

Custom types in queries

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).

ObjectBox Admin

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.

ObjectBox Admin Web App

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.

The Admin web app with the Data page open.

Run via Docker

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.

2KB
objectbox-admin.sh
objectbox-admin.sh Shell Front-End for ObjectBox Admin Docker Image

Download 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.

Admin for Android

Works for Android apps built with ObjectBox for Java or Flutter

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:

app/build.gradle
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:

android/app/build.gradle
// 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();
Info: added Android manifest permissions

For your information, these are the permissions the objectbox-android-objectbrowser dependency automatically adds to AndroidManifest.xml:

<!-- Required to provide the web interface -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Required to run keep-alive service when targeting API 28 or higher -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- When targeting API level 33 or higher to post the initial Admin notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

If the dependency is only used in debug builds as recommended above, these permissions will not be added to your release build.

Browse data on your test device

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.

Browse data on your dev machine

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.

Download Objects

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.

The download option for objects.

ObjectBox Queries

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.

Build a query

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()

one_of is not yet available in Python.

Other notable features

  • 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.

Common conditions

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:

  • Property conditions: Java and Dart

  • Relation conditions: Java

Dart only: DateTime caveat

For Dart DateTime is stored as time in milliseconds internally in the database (or nanoseconds for @Property(type: PropertyType.dateNano)).

To avoid having to manually convert to int when creating a query condition, extra methods that accept DateTime exist.

For example, to query an Order entity with a date field to find all orders in 2023:

final query = box
    .query(Order_.date.betweenDate(DateTime.utc(2023),
        DateTime.utc(2024).subtract(Duration(milliseconds: 1))))
    .build();

Nearest neighbor vector search

A special condition is available for vector properties with an HNSW index. See the dedicated page for details:

Order results

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();

Order results feature is not yet available in Python.

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 results feature is not yet available in Python.

Order directives can also be chained. Check the method documentation (Java) for details.

Run a query

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.

Find objects

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.

Remove objects

To remove all objects matching a query, call query.remove() .

Reuse Queries and Parameters

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.

Query is not thread safe. To use a query in a different thread, either build a new query or synchronize access to it. Alternatively, in Java use query.copy() or a QueryThreadLocal to obtain an instance for each thread.

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()

Limit, Offset, and Pagination

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.

Lazy-load results (Java)

Only Java/Kotlin

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.

Stream results (Dart)

Only Dart

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();

Observe or listen to changes

To learn how to observe or listen to changes to the results of a query, see the data observers page:

Query a single property

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 Users, 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();

Note: the returned array of property values is not in any particular order, even if you did specify an order when building the query.

Handle null values

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();

Distinct and unique results

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();

Aggregate values

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.

Query a related entity (links)

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();

Eager-load relations

Only Java/Kotlin

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

Only Java/Kotlin. For Dart, use the built-in where() method.

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:

  1. Narrow down results using standard database conditions to a reasonable number (use QueryBuilder to get “candidates”)

  2. 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.

Query filters and ToMany relation

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)

Debug queries

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)

Advanced Setup

Additional configuration options when creating an ObjectBox database.

This page contains:

ObjectBox for Java - Advanced Setup

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.

Manually Add Libraries

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:

Add libraries for distribution

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.

Processor Options

In your app’s Gradle build script, the following processor options, explained below, are available:

Change the Model File Path

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.

Change the MyObjectBox package

Since 1.5.0

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.

Enable Debug Mode

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:

Enable DaoCompat mode

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.

ObjectBox for Flutter/Dart - Advanced Setup

Change the generated files directory

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
added ObjectBox
DaoCompat documentation
ObjectBox for Java - Advanced Setup
ObjectBox for Flutter/Dart - Advanced Setup
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")
On-Device Vector Search
Data Observers & Rx

Data Observers & Rx

How to observe box and query changes using ObjectBox with Java or Dart, how to integrate with RxJava.

On this page:

  • ObjectBox Java - Data Observers and Reactive Extensions

  • ObjectBox Dart - Reactive Queries

ObjectBox Java - Data Observers and Reactive Extensions

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.

Reactive Observers: A First Example

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.

Data Observers Basics

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.

Note: onData() is called asynchronously and decoupled from the thread causing the data change (like the thread that committed a transaction).

Observing General 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.

Note: there is also subscribe() which takes no arguments. It subscribes the observer to receive changes for all available object classes.

Observing 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.

Canceling Subscriptions

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();
}

Note: On Android, you would typically create the subscription in one of the onCreate()/onStart()/onResume() lifecycle methods and cancel it in its counterpart onDestroy()/onStop()/onPause().

Observers and Transactions

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.

Reactive Extensions

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.

Thread Scheduling

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.

Transforming Data

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.

ErrorObserver

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().

Single Notifications vs. Only-Changes

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.

Weak References

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().

Threading overview

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.

ObjectBox RxJava Extension Library

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.

ObjectBox Dart - Reactive Queries

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()

On-Device Vector Search

Local on-device approximate nearest neighbor (ANN) search on high-dimensional vector properties

Vector Search is currently available for Python, C, C++, Dart/Flutter, Java/Kotlin and Swift. Other languages will follow soon.

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:

  1. Define your data model along with a vector index,

  2. Insert your data/vectors,

  3. Search for nearest neighbors.

An Example: Cities and their Location

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.

Data Model and Vector Index

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.

Hierarchical Navigable Small Worlds (HNSW) - Background Information

HNSW spans a graph over all vectors by connecting each vector node to its closest neighbors. To search for the nearest neighbors of a given vector, you start at an arbitrary node and check which of its neighbors are closer to the given vector. This repeats until the closest nodes are identified. To make this scalable, HNSW uses layers: higher layers have less nodes to get closer to the destination vector faster. Once the closest node in a layer is identified, the algorithm moves down one layer. This repeats until the lowest layer which contains all nodes is reached and the actual approximate nearest neighbors are found.

Approximate nearest neighbor (ANN) search: with high-dimensional vectors, exact nearest neighbor search is extremely time consuming (see also: curse of dimensionality). Thus, approximate solutions like HNSW are the way to go in this domain. Typically, they come with a quality/performance trade-off. In the case of HNSW you have parameters to control that.

If you are interested in scientific details, check out the HNSW paper.

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):

city.fbs
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.

Insert Vector Objects

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).

Performance note: for inserting multiple objects at once, wrap a transaction around the put commands.

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}}
});

Note: for the City example, it is easy to obtain the vector values. For more complex use cases it usually takes an additional step to create the vectors. Often an AI model is used, which is covered in a section further down.

Perform a nearest neighbor search

(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. 🎉

Note: if results do not need to be in any particular order, use the standard find methods that do not come with scores. Performance-wise they may have a tiny advantage, but again, do not expect the nearest elements to be the first results.

AI Embeddings

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.

RAG

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.

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 and Future Plans

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

Vector Search FAQ

How does this compare to libraries like FAISS?

ObjectBox Vector Search offers significant advantages by combining the capabilities of a vector search engine with the robustness of a full-featured database:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Entity Annotations

Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.

ObjectBox - Database Persistence with Entity Annotations

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.
}

Note:

  • It’s often good practice to model entities as “dumb” data classes (POJOs) with just properties.

  • Entities must have a no-args constructor, or for better performance, a constructor with all properties as arguments. In the above example, a default, no-args constructor is generated by the compiler.

@Entity
data class User(
        @Id var id: Long = 0,
        var name: String? = null,
        // Not persisted:
        @Transient var tempUsageCount: Int = 0
)

Note:

  • It’s often good practice to model entities as “dumb” data classes (POJOs) with just properties.

  • Entities must have a no-args constructor, or for better performance, a constructor with all properties as arguments. In the above example, a default, no-args constructor is generated by the compiler. For Kotlin data classes this can be achieved by adding default values for all parameters. (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.)

@Entity()
class User {
  // Annotate with @Id() if name isn't "id" (case insensitive).
  int id = 0;
  String? name;
  
  // Not persisted:
  @Transient
  int tempUsageCount = 0;
)

It’s often good practice to model entities as “dumb” data classes with just properties.

For ObjectBox to be able to construct objects read from the database, entities must have a no-args constructor or a constructor with argument names matching the properties, for example:

User({this.id, this.name});
@Entity()
class User:
    id = Id
    name = String
    temp_usage_count = None

Available Property types include: Bool, Char, Bytes, String, Int8/16/32/64, Float32/64 *Vector and *List variants (e.g. Float32Vector)

Object IDs: @Id

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.

If you need to assign IDs by yourself have a look at how to switch to self-assigned IDs and what side effects apply.

Make entity data accessible

ObjectBox needs to access the data of your entity’s properties (e.g. in the generated code). You have two options:

  1. Make sure properties do not have private visibility.

  2. 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);
}

Supported property types

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:

Basic annotations for entity properties

Transient

@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

Only available for Java/Kotlin at the moment

@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

Property Indexes

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.

Index types (String)

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.

Limits of hash-based indexes: Hashes work great for equality checks, but not for "starts with" type conditions. If you frequently use those, you should use value-based indexes instead.

Vector Index for Nearest Neighbor Search

To enable nearest neighbor search, a special index type for vector properties is available:

Unique constraints

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.

Unique constraints are based on an index, so it is possible to further configure the index with an @Index annotation.

Change database type

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)

Relations

Creating to-one and to-many relations between objects is possible as well, see the Relations documentation for details.

Triggering code generation

Once your entity schema is in place, you can trigger the code generation process.

Custom Types
On-Device Vector Search

Java Release History (<= v1.5)

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.

4.0.2 - 2024-08-20

  • 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).

4.0.1 - 2024-06-03

  • 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.

4.0.0 - Vector Search - 2024-05-16

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).

V3.8.0 - 2024-02-13

  • 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.

V3.7.1 - 2023/11/07

  • 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.

V3.7.0 - 2023/08/22

  • 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.

V3.6.0 - 2023/05/16

  • 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).

V3.5.1 - 2023/01/31

  • 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.

V3.5.0 - 2022/12/05

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.

V3.4.0 - 2022/10/18

  • 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.

V3.3.1 - 2022/09/05

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.

V3.2.1 - 2022/07/05

  • 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

V3.2.0 - 2022/06/20

  • 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.

V3.1.3 - 2022/05/10

  • 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>).

V3.1.2 - 2022/02/21

This release only contains bug fixes for the Android library when used with ObjectBox for Dart/Flutter.

V3.1.1 - 2022/01/26

  • 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).

V3.1.0 - 2021/12/15

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

Kotlin

  • Add BoxStore.awaitCallInTx suspend function which wraps BoxStore.callInTx.

Gradle plugin

  • Do not crash trying to add dependencies to Java desktop projects that only apply the Gradle application plugin.

V3.0.0 - 2021/10/19

2021/10/19: Released version 3.0.1, which contains a fix for Android Java projects.

  • 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

New supported property types

  • 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...).

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.

V2.9.2-RC4 - 2021/08/19

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.

V2.9.1 - 2021/03/15

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.

V2.9.0 - 2021/02/16

  • 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

V2.8.1 - 2020/11/10

  • Minor improvements to Sync tooling.

See the 2.8.0 release notes below for the latest changes.

V2.8.0 - 2020/11/05

  • 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).

V2.7.1 - 2020/08/19

  • 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.

V2.7.0 - 2020/07/30

  • 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

V2.6.0 - 2020/06/09

  • @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.

V3.0.0-alpha2 - 2020/03/24

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.

V3.0.0-alpha1 - 2020/03/09

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.

V2.5.1 - 2020/02/10

  • 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" ]
            }
        }
    }
}

V2.5.0 - 2019/12/12

Important bug fix - please update asap if you are using N:M relations!

  • Fixed corner case for N:M ToMany (not the backlinks for ToOne) returning wrong results

Improvements and New Features

  • 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

V2.4.1 - 2019/10/29

  • More helpful error messages if annotations can not be combined.

  • Improved documentation on various annotations.

V2.4.0 - 2019/10/15

Upgrade Notes

  • 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.

Improvements & Fixes

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.

V2.3.4 - 2019/03/19

  • 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

V2.3.3 - 2019/02/14

  • Fixed a bug introduced by V2.3.2 affecting older Android versions 4.3 and below

V2.3.2 - 2019/02/04

  • 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.

V2.3.1 - 2019/01/08

  • Fixed a corner case for Box.getAll() after removeAll() to return a stale object if no objects are stored

V2.3 - 2018/12/30

Improvements & Fixes

  • 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

V2.2 - 2018/09/27

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

V2.1 - 2018/08/16

Minor Improvements & Fixes

  • 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

V2.0 - 2018/07/25

New Features/Improvements

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[])

V2.0 beta – 2018/06/26

New Features/Improvements

  • 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

V1.5.0 – 2018/04/17

New Features/Improvements

  • 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

Fixes

  • 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

V1.4.4 – 2018/03/08

New Features/Improvements

  • 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

Fixes

  • Fixed converting from arrays in entities

  • Fixed @NameInDb

  • Fixed Gradle “androidTestCompile is obsolete” warning

V1.4.3 – 2018/03/01

New Features

  • 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.

Fixes

  • Fixed BoxStore.close being stuck in rare scenarios

  • Fixed an issue with char properties in entities

V1.4.2 – 2018/02/25

Note: This release requires the Android Gradle Plugin 3.0.0 or higher.

Improvements

  • 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)

V1.4.1 – 2018/01/23

Improvements

  • Added DbExceptionListener as a central place to listen for DB related exceptions

  • Minor improvements for ToMany and generated Cursor classes

Fixes

  • ToMany: fixed handling of duplicate entries (e.g. fixes swap and reverse operations)

  • ToMany: fixed removal of non-persisted element for standalone relations

V1.4.0 – 2018/01/11

New Features

  • 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

V1.3.4 – 2017/12/07

Improvements

  • ToOne now implements equals() and hashCode() based on the targetId property

  • Android ABI x86_64 was added to the aar

Fixes

  • 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

V1.3.3 (1.3.x) – 2017/12/04

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.

Improvements

  • Flag for query parameter logging

  • 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

  • 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

V1.2.1 – 2017/11/10

Improvements

  • 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

Fixes

  • Fix for ObjectBoxLiveData firing twice

V1.2.0 – 2017/10/31

Compatibility note: We removed some Box.find methods, which were all tagged as @Temporary. Only the Property based ones remain (for now, also @Temporary).

New Features

  • 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

Fixes

  • Fixed query order by float and double

  • Fixed an missing import if to-many relations referenced a entity in another package

  • Other minor fixes

V1.1.0 – 2017/10/03

New Features

  • 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

Fixes

  • Annotation processor detects boolean getters starting with “is”

  • Fixed a NPE with eager and findFirst() when there is no result

V1.0.1 – 2017/09/10

First bug fix release for ObjectBox 1.0.

New Features

  • ToMany allows setting a Comparator to order the List (experimental)

Fixes

  • 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)

V1.0.0 – 2017/09/04

ObjectBox is out of beta! See our announcement blog post for details.

New Features

  • 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

Fixes

  • Fixed two corner cases with queries

V0.9.15 (beta) 2017/08/21 Hotfixes

Fixes

  • 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

V0.9.14 (beta) 2017/08/14 Standalone relations, new build tools

For upgrade notes, please check the announcement post.

New Features

  • 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

Fixes

  • Fixed the issue causing a “Illegal state: Tx destroyed/inactive, writeable cursor still available” error log

V0.9.13 (beta) 2017/07/12 Kotlin Support

New Features

  • 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

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

V0.9.12 (beta) 2017/05/08 ToMany class

  • 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)

V0.9.11 (beta) 2017/04/25: Various improvements

  • 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.)

V0.9.10 (beta) 2017/04/10: Bug Fixes and minor improvements

New features and improvements

  • 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

Bug fixes

  • 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

V0.9.9 (beta) 2017/03/07: Bug Fixes

New features

  • Query.forEach() to iterate efficiently over query result objects

Bug fixes

  • Various bug fixes

V0.9.8 (beta) 2017/02/22: Going Reactive

New features

  • Data observers with reactive extensions for transformations, thread scheduling, etc.

  • Optional RxJava 2 library

  • OR conditions for QueryBuilder allow more powerful queries

Bug fixes

  • Fixed: Changing the order of an entity’s properties could cause errors in some cases

  • Fixed: Querying using relation IDs

V0.9.7 (beta) 2017/02/10

New features

  • 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)

Breaking internal changes

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.

V0.9.6 (first public beta release) 2017/01/24

See ObjectBox Announcement

Relations

ObjectBox Relations: Learn how to create and update to-one and to-many relations between entities in ObjectBox and improve performance.

ObjectBox - Relations

Prefer to dive right into code? Check out our

  • Kotlin Android example app using relations,

  • Java relations playground Android app,

  • Flutter relations example app.

The Python API does not yet support relations.

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.

To-One Relations

To-one Relations

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>
}

For Kotlin desktop (Linux, macOS, Windows) apps, additional code is required. See Initialization Magic.

@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.

Note: if your related entity uses manually assigned IDs with @Id(assignable = true) ObjectBox won't know if a target object is a new one or an existing one, therefore it will NOT insert it, customerBox.put(customer) would have to be called manually (considering the previous example). See below about updating ToOne for details.

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.

How ToOne works behind the scenes

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.

Expose the ToOne target ID property

Only Java/Kotlin

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.

Initialization Magic

Only Java/Kotlin

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.

If your setup does not support transformations, currently Kotlin JVM (Linux, macOS, Windows) projects, add the below modifications yourself. You also will have to call box.attach(entity) before modifying ToOne or ToMany properties.

@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

}

Improve Performance

Only Java/Kotlin

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-Many Relations

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.

One-to-Many (1:N)

One-to-Many (1:N)

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>
}

For Kotlin desktop (Linux, macOS, Windows) apps, additional code is required. See Initialization Magic.

@Entity()
class Customer {
  int id;

  @Backlink('customer')
  final orders = ToMany<Order>();
}

@Entity()
class Order {
  int id;

  final customer = ToOne<Customer>();
}

When using @Backlink it is recommended to explicitly specify the linked to relation using to. It is possible to omit this if there is only one matching relation. However, it helps with code readability and avoids a compile-time error if at any point another matching relation is added (in the above case, if another ToOne<Customer> is added to the Order class).

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)

Two data classes that have the same property values (excluding those defined in the class body) are equal and have the same hash code. Keep this in mind when working with ToMany which uses a HashMap to keep track of changes. E.g. adding the same data class multiple times has no effect, it is treated as the same entity.

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.

Note: if your entities use manually assigned IDs with @Id(assignable = true) the above will not work. See below about updating ToMany for details.

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);

Many-to-Many (N:M)

Many-to-Many (N:M)

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>
}

For Kotlin desktop (Linux, macOS, Windows) apps, additional code is required. See Initialization Magic.

@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.

Note: if your entities use manually assigned IDs with @Id(assignable = true) the above will not work. See below about updating ToMany for details.

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();

Access Many-To-Many in the reverse direction

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>
}

For Kotlin desktop (Linux, macOS, Windows) apps, additional code is required. See Initialization Magic.

@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>();
}

Using the List interface for to-many

Only for Java/Kotlin

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)
}

This may be helpful when trying to deserialize an object that contains a to-many from JSON. However, note that if the JSON deserializer replaces the ToMany instance with e.g. an ArrayList during put the to-many property is skipped. It is then up to you to create the relation.

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);

Updating Relations

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);

Updating ToOne

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.

Updating ToMany

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.

Example: Extending the Model with an Address

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)

Example: Modelling Tree Relations

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>
}

For Kotlin desktop (Linux, macOS, Windows) apps, additional code is required. See Initialization Magic.

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
to-one-relations
Onte-to-Many
Many-to-Many