All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.objectbox.relation.ToOne Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-2024 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.relation;

import java.io.Serializable;
import java.lang.reflect.Field;

import javax.annotation.Nullable;

import io.objectbox.Box;
import io.objectbox.BoxStore;
import io.objectbox.Cursor;
import io.objectbox.annotation.Backlink;
import io.objectbox.annotation.Entity;
import io.objectbox.annotation.apihint.Internal;
import io.objectbox.exception.DbDetachedException;
import io.objectbox.internal.ReflectionCache;

/**
 * A to-one relation of an entity that references one object of a {@link TARGET} entity.
 * 

* Example: *

{@code
 * // Java
 * @Entity
 * public class Order {
 *     private ToOne customer;
 * }
 *
 * // Kotlin
 * @Entity
 * data class Order() {
 *     lateinit var customer: ToOne
 * }
 * }
*

* Uses lazy initialization. The target object ({@link #getTarget()}) is only read from the database when it is first * accessed. *

* Common usage: *

    *
  • Set the target object with {@link #setTarget} to create a relation. * When the object with the ToOne is put, if the target object is new (its ID is 0), it will be put as well. * Otherwise, only the target ID in the database is updated. *
  • {@link #setTargetId} of the target object to create a relation. *
  • {@link #setTarget} with {@code null} or {@link #setTargetId} to {@code 0} to remove the relation. *
*

* Then, to persist the changes {@link Box#put} the object with the ToOne. *

*

{@code
 * // Example 1: create a relation
 * order.getCustomer().setTarget(customer);
 * // or order.getCustomer().setTargetId(customerId);
 * store.boxFor(Order.class).put(order);
 *
 * // Example 2: remove the relation
 * order.getCustomer().setTarget(null);
 * // or order.getCustomer().setTargetId(0);
 * store.boxFor(Order.class).put(order);
 * }
*

* The target object is referenced by its ID. * This target ID ({@link #getTargetId()}) is persisted as part of the object with the ToOne in a special * property created for each ToOne (named like "customerId"). *

* To get all objects with a ToOne that reference a target object, see {@link Backlink}. * * @param target object type ({@link Entity @Entity} class). */ // TODO not exactly thread safe public class ToOne implements Serializable { private static final long serialVersionUID = 5092547044335989281L; private final Object entity; private final RelationInfo relationInfo; private final boolean virtualProperty; transient private BoxStore boxStore; transient private Box entityBox; transient private volatile Box targetBox; transient private Field targetIdField; /** * Resolved target entity is cached */ private TARGET target; private long targetId; private volatile long resolvedTargetId; /** To avoid calls to {@link #getTargetId()}, which may involve expensive reflection. */ private boolean checkIdOfTargetForPut; private boolean debugRelations; /** * In Java, the constructor call is generated by the ObjectBox plugin. * * @param sourceEntity The source entity that owns the to-one relation. * @param relationInfo Meta info as generated in the Entity_ (entity name plus underscore) classes. */ @SuppressWarnings("unchecked") // RelationInfo cast: ? is at least Object. public ToOne(Object sourceEntity, RelationInfo relationInfo) { if (sourceEntity == null) { throw new IllegalArgumentException("No source entity given (null)"); } if (relationInfo == null) { throw new IllegalArgumentException("No relation info given (null)"); } this.entity = sourceEntity; this.relationInfo = (RelationInfo) relationInfo; virtualProperty = relationInfo.targetIdProperty.isVirtual; } /** * Returns the target object or {@code null} if there is none. *

* {@link ToOne} uses lazy initialization, so on first access this will read the target object from the database. */ public TARGET getTarget() { return getTarget(getTargetId()); } /** If property backed, entities can pass the target ID to avoid reflection. */ @Internal public TARGET getTarget(long targetId) { synchronized (this) { if (resolvedTargetId == targetId) { return target; } } ensureBoxes(null); // Do not synchronize while doing DB stuff TARGET targetNew = targetBox.get(targetId); setResolvedTarget(targetNew, targetId); return targetNew; } private void ensureBoxes(@Nullable TARGET target) { // Only check the property set last if (targetBox == null) { Field boxStoreField = ReflectionCache.getInstance().getField(entity.getClass(), "__boxStore"); try { boxStore = (BoxStore) boxStoreField.get(entity); if (boxStore == null) { if (target != null) { boxStoreField = ReflectionCache.getInstance().getField(target.getClass(), "__boxStore"); boxStore = (BoxStore) boxStoreField.get(target); } if (boxStore == null) { throw new DbDetachedException("Cannot resolve relation for detached entities, " + "call box.attach(entity) beforehand."); } } debugRelations = boxStore.isDebugRelations(); } catch (IllegalAccessException e) { throw new RuntimeException(e); } entityBox = boxStore.boxFor(relationInfo.sourceInfo.getEntityClass()); targetBox = boxStore.boxFor(relationInfo.targetInfo.getEntityClass()); } } public TARGET getCachedTarget() { return target; } public boolean isResolved() { return resolvedTargetId == getTargetId(); } public boolean isResolvedAndNotNull() { return resolvedTargetId != 0 && resolvedTargetId == getTargetId(); } public boolean isNull() { return getTargetId() == 0 && target == null; } /** * Prepares to set the target of this relation to the object with the given ID. Pass {@code 0} to remove an existing * one. *

* To apply changes, put the object with the ToOne. For important details, see the notes about relations of * {@link Box#put(Object)}. * * @see #setTarget */ public void setTargetId(long targetId) { if (virtualProperty) { this.targetId = targetId; } else { try { getTargetIdField().set(entity, targetId); } catch (IllegalAccessException e) { throw new RuntimeException("Could not update to-one ID in entity", e); } } if (targetId != 0) { checkIdOfTargetForPut = false; } } // To do a more efficient put with only one property changed. void setAndUpdateTargetId(long targetId) { setTargetId(targetId); ensureBoxes(null); // TODO update on targetId in DB throw new UnsupportedOperationException("Not implemented yet"); } /** * Prepares to set the target object of this relation. Pass {@code null} to remove an existing one. *

* To apply changes, put the object with the ToOne. For important details, see the notes about relations of * {@link Box#put(Object)}. * * @see #setTargetId */ public void setTarget(@Nullable final TARGET target) { if (target != null) { long targetId = relationInfo.targetInfo.getIdGetter().getId(target); checkIdOfTargetForPut = targetId == 0; setTargetId(targetId); setResolvedTarget(target, targetId); } else { setTargetId(0); clearResolved(); } } /** * Sets or clears the target entity and ID in the source entity, then puts the source entity to persist changes. * Pass null to clear. *

* If the target entity was not put yet (its ID is 0), it will be put before the source entity. */ public void setAndPutTarget(@Nullable final TARGET target) { ensureBoxes(target); if (target != null) { long targetId = targetBox.getId(target); if (targetId == 0) { setAndPutTargetAlways(target); } else { setTargetId(targetId); setResolvedTarget(target, targetId); entityBox.put(entity); } } else { setTargetId(0); clearResolved(); entityBox.put(entity); } } /** * Sets or clears the target entity and ID in the source entity, * then puts the target (if not null) and source entity to persist changes. * Pass null to clear. *

* When clearing the target entity, this does not remove it from its box. * This only dissolves the relation. */ public void setAndPutTargetAlways(@Nullable final TARGET target) { ensureBoxes(target); if (target != null) { boxStore.runInTx(() -> { long targetKey = targetBox.put(target); setResolvedTarget(target, targetKey); entityBox.put(entity); }); } else { setTargetId(0); clearResolved(); entityBox.put(entity); } } /** Both values should be set (and read) "atomically" using synchronized. */ private synchronized void setResolvedTarget(@Nullable TARGET target, long targetId) { if (debugRelations) { System.out.println("Setting resolved ToOne target to " + (target == null ? "null" : "non-null") + " for ID " + targetId); } resolvedTargetId = targetId; this.target = target; } /** * Clears the target. */ private synchronized void clearResolved() { resolvedTargetId = 0; target = null; } public long getTargetId() { if (virtualProperty) { return targetId; } else { // Future alternative: Implemented by generated ToOne sub classes to avoid reflection Field keyField = getTargetIdField(); try { Long key = (Long) keyField.get(entity); return key != null ? key : 0; } catch (IllegalAccessException e) { throw new RuntimeException("Could not access field " + keyField); } } } private Field getTargetIdField() { if (targetIdField == null) { targetIdField = ReflectionCache.getInstance().getField(entity.getClass(), relationInfo.targetIdProperty.name); } return targetIdField; } @Internal public boolean internalRequiresPutTarget() { return checkIdOfTargetForPut && target != null && getTargetId() == 0; } @Internal public void internalPutTarget(Cursor targetCursor) { checkIdOfTargetForPut = false; long id = targetCursor.put(target); setTargetId(id); setResolvedTarget(target, id); } /** For tests */ Object getEntity() { return entity; } @Override public boolean equals(Object obj) { if (!(obj instanceof ToOne)) return false; ToOne other = (ToOne) obj; return relationInfo == other.relationInfo && getTargetId() == other.getTargetId(); } @Override public int hashCode() { long targetId = getTargetId(); return (int) (targetId ^ targetId >>> 32); } }