io.objectbox.Box Maven / Gradle / Ivy
Show all versions of objectbox-java Show documentation
/*
* Copyright 2017-2019 ObjectBox Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.objectbox;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import io.objectbox.annotation.Backlink;
import io.objectbox.annotation.Id;
import io.objectbox.annotation.apihint.Beta;
import io.objectbox.annotation.apihint.Experimental;
import io.objectbox.annotation.apihint.Internal;
import io.objectbox.exception.DbException;
import io.objectbox.internal.CallWithHandle;
import io.objectbox.internal.IdGetter;
import io.objectbox.internal.ReflectionCache;
import io.objectbox.query.QueryBuilder;
import io.objectbox.query.QueryCondition;
import io.objectbox.relation.RelationInfo;
import io.objectbox.relation.ToMany;
import io.objectbox.relation.ToOne;
/**
* A Box to put and get Objects of a specific Entity class.
*
* Thread-safe.
*/
@ThreadSafe
@SuppressWarnings("WeakerAccess,UnusedReturnValue,unused")
public class Box {
private final BoxStore store;
private final Class entityClass;
/** Set when running inside TX */
final ThreadLocal> activeTxCursor = new ThreadLocal<>();
private final ThreadLocal> threadLocalReader = new ThreadLocal<>();
private final IdGetter idGetter;
private EntityInfo entityInfo;
private volatile Field boxStoreField;
Box(BoxStore store, Class entityClass) {
this.store = store;
this.entityClass = entityClass;
idGetter = store.getEntityInfo(entityClass).getIdGetter();
}
Cursor getReader() {
Cursor cursor = getActiveTxCursor();
if (cursor != null) {
return cursor;
} else {
cursor = threadLocalReader.get();
if (cursor != null) {
Transaction tx = cursor.tx;
if (tx.isClosed() || !tx.isRecycled()) {
throw new IllegalStateException("Illegal reader TX state");
}
tx.renew();
cursor.renew();
} else {
cursor = store.beginReadTx().createCursor(entityClass);
threadLocalReader.set(cursor);
}
}
return cursor;
}
Cursor getActiveTxCursor() {
Transaction activeTx = store.activeTx.get();
if (activeTx != null) {
if (activeTx.isClosed()) {
throw new IllegalStateException("Active TX is closed");
}
Cursor cursor = activeTxCursor.get();
if (cursor == null || cursor.getTx().isClosed()) {
cursor = activeTx.createCursor(entityClass);
activeTxCursor.set(cursor);
}
return cursor;
}
return null;
}
Cursor getWriter() {
Cursor cursor = getActiveTxCursor();
if (cursor != null) {
return cursor;
} else {
Transaction tx = store.beginTx();
try {
return tx.createCursor(entityClass);
} catch (RuntimeException e) {
tx.close();
throw e;
}
}
}
void commitWriter(Cursor cursor) {
// NOP if TX is ongoing
if (activeTxCursor.get() == null) {
cursor.close();
cursor.getTx().commitAndClose();
}
}
void releaseWriter(Cursor cursor) {
// NOP if TX is ongoing
if (activeTxCursor.get() == null) {
Transaction tx = cursor.getTx();
if (!tx.isClosed()) {
cursor.close();
tx.abort();
tx.close();
}
}
}
void releaseReader(Cursor cursor) {
// NOP if TX is ongoing
if (activeTxCursor.get() == null) {
Transaction tx = cursor.getTx();
if (tx.isClosed() || tx.isRecycled() || !tx.isReadOnly()) {
throw new IllegalStateException("Illegal reader TX state");
}
tx.recycle();
}
}
/**
* Like {@link BoxStore#closeThreadResources()}, but limited to only this Box.
*
* Rule of thumb: prefer {@link BoxStore#closeThreadResources()} unless you know that your thread only interacted
* with this Box.
*/
public void closeThreadResources() {
Cursor cursor = threadLocalReader.get();
if (cursor != null) {
cursor.close();
cursor.getTx().close(); // a read TX is always started when the threadLocalReader is set
threadLocalReader.remove();
}
}
void txCommitted(Transaction tx) {
// Thread local readers will be renewed on next get, so we do not need clean them up
Cursor cursor = activeTxCursor.get();
if (cursor != null) {
activeTxCursor.remove();
cursor.close();
}
}
/**
* Called by {@link BoxStore#callInReadTx(Callable)} - does not throw so caller does not need try/finally.
*/
void readTxFinished(Transaction tx) {
Cursor cursor = activeTxCursor.get();
if (cursor != null && cursor.getTx() == tx) {
activeTxCursor.remove();
cursor.close();
}
}
/** Used by tests */
int getPropertyId(String propertyName) {
Cursor reader = getReader();
try {
return reader.getPropertyId(propertyName);
} finally {
releaseReader(reader);
}
}
@Internal
public long getId(T entity) {
return idGetter.getId(entity);
}
/**
* Get the stored object for the given ID.
*
* @return null if not found
*/
public T get(long id) {
Cursor reader = getReader();
try {
return reader.get(id);
} finally {
releaseReader(reader);
}
}
/**
* Get the stored objects for the given IDs.
*
* @return null if not found
*/
public List get(Iterable ids) {
ArrayList list = new ArrayList<>();
Cursor reader = getReader();
try {
for (Long id : ids) {
T entity = reader.get(id);
if (entity != null) {
list.add(entity);
}
}
} finally {
releaseReader(reader);
}
return list;
}
/**
* Get the stored objects for the given IDs.
*
* @return null if not found
*/
public List get(long[] ids) {
ArrayList list = new ArrayList<>(ids.length);
Cursor reader = getReader();
try {
for (Long id : ids) {
T entity = reader.get(id);
if (entity != null) {
list.add(entity);
}
}
} finally {
releaseReader(reader);
}
return list;
}
/**
* Get the stored objects for the given IDs as a Map with IDs as keys, and entities as values.
* IDs for which no entity is found will be put in the map with null values.
*
* @return null if not found
*/
public Map getMap(Iterable ids) {
HashMap map = new HashMap<>();
Cursor reader = getReader();
try {
for (Long id : ids) {
map.put(id, reader.get(id));
}
} finally {
releaseReader(reader);
}
return map;
}
/**
* Returns the count of all stored objects in this box.
*/
public long count() {
return count(0);
}
/**
* Returns the count of all stored objects in this box or the given maxCount, whichever is lower.
*
* @param maxCount maximum value to count or 0 (zero) to have no maximum limit
*/
public long count(long maxCount) {
Cursor reader = getReader();
try {
return reader.count(maxCount);
} finally {
releaseReader(reader);
}
}
/** Returns true if no objects are in this box. */
public boolean isEmpty() {
return count(1) == 0;
}
/**
* Returns all stored Objects in this Box.
*
* @return since 2.4 the returned list is always mutable (before an empty result list was immutable)
*/
public List getAll() {
ArrayList list = new ArrayList<>();
Cursor cursor = getReader();
try {
for (T object = cursor.first(); object != null; object = cursor.next()) {
list.add(object);
}
return list;
} finally {
releaseReader(cursor);
}
}
/**
* Check if an object with the given ID exists in the database.
* This is more efficient than a {@link #get(long)} and comparing against null.
*
* @return true if an object with the given ID was found, false otherwise.
* @since 2.7
*/
public boolean contains(long id) {
Cursor reader = getReader();
try {
return reader.seek(id);
} finally {
releaseReader(reader);
}
}
/**
* Puts the given object and returns its (new) ID.
*
* This means that if its {@link Id @Id} property is 0 or null, it is inserted as a new object and assigned the next
* available ID. For example, if there is an object with ID 1 and another with ID 100, it will be assigned ID 101.
* The new ID is also set on the given object before this returns.
*
* If instead the object has an assigned ID set, if an object with the same ID exists it is updated. Otherwise, it
* is inserted with that ID.
*
* If the ID was not assigned before an {@link IllegalArgumentException} is thrown.
*
* When the object contains {@link ToOne} or {@link ToMany} relations, they are created (or updated) to point to the
* (new) target objects. The target objects themselves are typically not updated or removed. To do so, put or remove
* them using their {@link Box}. However, for convenience, if a target object is new, it will be inserted and
* assigned an ID in its Box before creating or updating the relation. Also, for ToMany relations based on a
* {@link Backlink} the target objects are updated (to store changes in the linked ToOne or ToMany relation).
*
* Performance note: if you want to put several objects, consider {@link #put(Collection)}, {@link #put(Object[])},
* {@link BoxStore#runInTx(Runnable)}, etc. instead.
*/
public long put(T entity) {
Cursor cursor = getWriter();
try {
long key = cursor.put(entity);
commitWriter(cursor);
return key;
} finally {
releaseWriter(cursor);
}
}
/**
* Puts the given entities in a box using a single transaction.
*
* See {@link #put(Object)} for more details.
*/
@SafeVarargs // Not using T... as Object[], no ClassCastException expected.
public final void put(@Nullable T... entities) {
if (entities == null || entities.length == 0) {
return;
}
Cursor cursor = getWriter();
try {
for (T entity : entities) {
cursor.put(entity);
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* Puts the given entities in a box using a single transaction.
*
* See {@link #put(Object)} for more details.
*
* @param entities It is fine to pass null or an empty collection:
* this case is handled efficiently without overhead.
*/
public void put(@Nullable Collection entities) {
if (entities == null || entities.isEmpty()) {
return;
}
Cursor cursor = getWriter();
try {
for (T entity : entities) {
cursor.put(entity);
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* Puts the given entities in a box in batches using a separate transaction for each batch.
*
* See {@link #put(Object)} for more details.
*
* @param entities It is fine to pass null or an empty collection:
* this case is handled efficiently without overhead.
* @param batchSize Number of entities that will be put in one transaction. Must be 1 or greater.
*/
public void putBatched(@Nullable Collection entities, int batchSize) {
if (batchSize < 1) {
throw new IllegalArgumentException("Batch size must be 1 or greater but was " + batchSize);
}
if (entities == null) {
return;
}
Iterator iterator = entities.iterator();
while (iterator.hasNext()) {
Cursor cursor = getWriter();
try {
int number = 0;
while (number++ < batchSize && iterator.hasNext()) {
cursor.put(iterator.next());
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
}
/**
* Removes (deletes) the object with the given ID.
*
* If the object is part of a relation, it will be removed from that relation as well.
*
* @return true if the object did exist and was removed, otherwise false.
*/
public boolean remove(long id) {
Cursor cursor = getWriter();
boolean removed;
try {
removed = cursor.deleteEntity(id);
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
return removed;
}
/**
* Like {@link #remove(long)}, but removes multiple objects in a single transaction.
*/
public void remove(@Nullable long... ids) {
if (ids == null || ids.length == 0) {
return;
}
Cursor cursor = getWriter();
try {
for (long key : ids) {
cursor.deleteEntity(key);
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* @deprecated use {@link #removeByIds(Collection)} instead.
*/
@Deprecated
public void removeByKeys(@Nullable Collection ids) {
removeByIds(ids);
}
/**
* Like {@link #remove(long)}, but removes multiple objects in a single transaction.
*/
public void removeByIds(@Nullable Collection ids) {
if (ids == null || ids.isEmpty()) {
return;
}
Cursor cursor = getWriter();
try {
for (long key : ids) {
cursor.deleteEntity(key);
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* Like {@link #remove(long)}, but obtains the ID from the {@link Id @Id} property of the given object instead.
*/
public boolean remove(T object) {
Cursor cursor = getWriter();
boolean removed;
try {
long id = cursor.getId(object);
removed = cursor.deleteEntity(id);
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
return removed;
}
/**
* Like {@link #remove(Object)}, but removes multiple objects in a single transaction.
*/
@SafeVarargs // Not using T... as Object[], no ClassCastException expected.
@SuppressWarnings("Duplicates") // Detected duplicate has different type
public final void remove(@Nullable T... objects) {
if (objects == null || objects.length == 0) {
return;
}
Cursor cursor = getWriter();
try {
for (T entity : objects) {
long key = cursor.getId(entity);
cursor.deleteEntity(key);
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* Like {@link #remove(Object)}, but removes multiple objects in a single transaction.
*/
@SuppressWarnings("Duplicates") // Detected duplicate has different type
public void remove(@Nullable Collection objects) {
if (objects == null || objects.isEmpty()) {
return;
}
Cursor cursor = getWriter();
try {
for (T entity : objects) {
long key = cursor.getId(entity);
cursor.deleteEntity(key);
}
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* Like {@link #remove(long)}, but removes all objects in a single transaction.
*/
public void removeAll() {
Cursor cursor = getWriter();
try {
cursor.deleteAll();
commitWriter(cursor);
} finally {
releaseWriter(cursor);
}
}
/**
* WARNING: this method should generally be avoided as it is not transactional and thus may leave the DB in an
* inconsistent state. It may be the a last resort option to recover from a full DB.
* Like removeAll(), it removes all objects, returns the count of objects removed.
* Logs progress using warning log level.
*/
@Experimental
public long panicModeRemoveAll() {
return store.panicModeRemoveAllObjects(getEntityInfo().getEntityId());
}
/**
* Create a query with no conditions.
*
* @see #query(QueryCondition)
*/
public QueryBuilder query() {
return new QueryBuilder<>(this, store.getNativeStore(), store.getDbName(entityClass));
}
/**
* Applies the given query conditions and returns the builder for further customization, such as result order.
* Build the condition using the properties from your entity underscore classes.
*
* An example with a nested OR condition:
*
* # Java
* box.query(User_.name.equal("Jane")
* .and(User_.age.less(12)
* .or(User_.status.equal("child"))));
*
* # Kotlin
* box.query(User_.name.equal("Jane")
* and (User_.age.less(12)
* or User_.status.equal("child")))
*
* This method is a shortcut for {@code query().apply(condition)}.
*
* @see QueryBuilder#apply(QueryCondition)
*/
public QueryBuilder query(QueryCondition queryCondition) {
return query().apply(queryCondition);
}
public BoxStore getStore() {
return store;
}
public synchronized EntityInfo getEntityInfo() {
if (entityInfo == null) {
Cursor reader = getReader();
try {
entityInfo = reader.getEntityInfo();
} finally {
releaseReader(reader);
}
}
return entityInfo;
}
@Beta
public void attach(T entity) {
if (boxStoreField == null) {
try {
boxStoreField = ReflectionCache.getInstance().getField(entityClass, "__boxStore");
} catch (Exception e) {
throw new DbException("Entity cannot be attached - only active entities with relationships support " +
"attaching (class has no __boxStore field(?)) : " + entityClass, e);
}
}
try {
boxStoreField.set(entity, store);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
// Sketching future API extension
private boolean isChanged(T entity) {
return false;
}
// Sketching future API extension
private boolean putIfChanged(T entity) {
return false;
}
public Class getEntityClass() {
return entityClass;
}
@Internal
public List internalGetBacklinkEntities(int entityId, Property> relationIdProperty, long key) {
Cursor reader = getReader();
try {
return reader.getBacklinkEntities(entityId, relationIdProperty, key);
} finally {
releaseReader(reader);
}
}
@Internal
public List internalGetRelationEntities(int sourceEntityId, int relationId, long key, boolean backlink) {
Cursor reader = getReader();
try {
return reader.getRelationEntities(sourceEntityId, relationId, key, backlink);
} finally {
releaseReader(reader);
}
}
@Internal
public long[] internalGetRelationIds(int sourceEntityId, int relationId, long key, boolean backlink) {
Cursor reader = getReader();
try {
return reader.getRelationIds(sourceEntityId, relationId, key, backlink);
} finally {
releaseReader(reader);
}
}
/**
* Given a ToMany relation and the ID of a source entity gets the target entities of the relation from their box,
* for example {@code orderBox.getRelationEntities(Customer_.orders, customer.getId())}.
*/
public List getRelationEntities(RelationInfo, T> relationInfo, long id) {
return internalGetRelationEntities(relationInfo.sourceInfo.getEntityId(), relationInfo.relationId, id, false);
}
/**
* Given a ToMany relation and the ID of a target entity gets all source entities pointing to this target entity,
* for example {@code customerBox.getRelationEntities(Customer_.orders, order.getId())}.
*/
public List getRelationBacklinkEntities(RelationInfo relationInfo, long id) {
return internalGetRelationEntities(relationInfo.sourceInfo.getEntityId(), relationInfo.relationId, id, true);
}
/**
* Like {@link #getRelationEntities(RelationInfo, long)}, but only returns the IDs of the target entities.
*/
public long[] getRelationIds(RelationInfo, T> relationInfo, long id) {
return internalGetRelationIds(relationInfo.sourceInfo.getEntityId(), relationInfo.relationId, id, false);
}
/**
* Like {@link #getRelationBacklinkEntities(RelationInfo, long)}, but only returns the IDs of the source entities.
*/
public long[] getRelationBacklinkIds(RelationInfo relationInfo, long id) {
return internalGetRelationIds(relationInfo.sourceInfo.getEntityId(), relationInfo.relationId, id, true);
}
@Internal
public RESULT internalCallWithReaderHandle(CallWithHandle task) {
Cursor reader = getReader();
try {
return task.call(reader.internalHandle());
} finally {
releaseReader(reader);
}
}
@Internal
public RESULT internalCallWithWriterHandle(CallWithHandle task) {
Cursor writer = getWriter();
RESULT result;
try {
result = task.call(writer.internalHandle());
commitWriter(writer);
} finally {
releaseWriter(writer);
}
return result;
}
public String getReaderDebugInfo() {
Cursor reader = getReader();
try {
return reader + " with " + reader.getTx() + "; store's commit count: " + getStore().commitCount;
} finally {
releaseReader(reader);
}
}
}