Permalink
Cannot retrieve contributors at this time
/* | |
* Copyright (C) 2011-2016 Markus Junginger, greenrobot (http://greenrobot.org) | |
* | |
* 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 org.greenrobot.greendao; | |
import android.database.CrossProcessCursor; | |
import android.database.Cursor; | |
import android.database.CursorWindow; | |
import android.database.sqlite.SQLiteDatabase; | |
import android.database.sqlite.SQLiteStatement; | |
import org.greenrobot.greendao.annotation.apihint.Experimental; | |
import org.greenrobot.greendao.database.Database; | |
import org.greenrobot.greendao.database.DatabaseStatement; | |
import org.greenrobot.greendao.identityscope.IdentityScope; | |
import org.greenrobot.greendao.identityscope.IdentityScopeLong; | |
import org.greenrobot.greendao.internal.DaoConfig; | |
import org.greenrobot.greendao.internal.FastCursor; | |
import org.greenrobot.greendao.internal.TableStatements; | |
import org.greenrobot.greendao.query.Query; | |
import org.greenrobot.greendao.query.QueryBuilder; | |
import org.greenrobot.greendao.rx.RxDao; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.List; | |
import rx.schedulers.Schedulers; | |
/** | |
* Base class for all DAOs: Implements entity operations like insert, load, delete, and query. | |
* <p> | |
* This class is thread-safe. | |
* | |
* @param <T> Entity type | |
* @param <K> Primary key (PK) type; use Void if entity does not have exactly one PK | |
* @author Markus | |
*/ | |
/* | |
* When operating on TX, statements, or identity scope the following locking order must be met to avoid deadlocks: | |
* | |
* 1.) If not inside a TX already, begin a TX to acquire a DB connection (connection is to be handled like a lock) | |
* | |
* 2.) The DatabaseStatement | |
* | |
* 3.) identityScope | |
*/ | |
public abstract class AbstractDao<T, K> { | |
protected final DaoConfig config; | |
protected final Database db; | |
protected final boolean isStandardSQLite; | |
protected final IdentityScope<K, T> identityScope; | |
protected final IdentityScopeLong<T> identityScopeLong; | |
protected final TableStatements statements; | |
protected final AbstractDaoSession session; | |
protected final int pkOrdinal; | |
private volatile RxDao<T, K> rxDao; | |
private volatile RxDao<T, K> rxDaoPlain; | |
public AbstractDao(DaoConfig config) { | |
this(config, null); | |
} | |
@SuppressWarnings("unchecked") | |
public AbstractDao(DaoConfig config, AbstractDaoSession daoSession) { | |
this.config = config; | |
this.session = daoSession; | |
db = config.db; | |
isStandardSQLite = db.getRawDatabase() instanceof SQLiteDatabase; | |
identityScope = (IdentityScope<K, T>) config.getIdentityScope(); | |
if (identityScope instanceof IdentityScopeLong) { | |
identityScopeLong = (IdentityScopeLong<T>) identityScope; | |
} else { | |
identityScopeLong = null; | |
} | |
statements = config.statements; | |
pkOrdinal = config.pkProperty != null ? config.pkProperty.ordinal : -1; | |
} | |
public AbstractDaoSession getSession() { | |
return session; | |
} | |
TableStatements getStatements() { | |
return config.statements; | |
} | |
public String getTablename() { | |
return config.tablename; | |
} | |
public Property[] getProperties() { | |
return config.properties; | |
} | |
public Property getPkProperty() { | |
return config.pkProperty; | |
} | |
public String[] getAllColumns() { | |
return config.allColumns; | |
} | |
public String[] getPkColumns() { | |
return config.pkColumns; | |
} | |
public String[] getNonPkColumns() { | |
return config.nonPkColumns; | |
} | |
/** | |
* Loads the entity for the given PK. | |
* | |
* @param key a PK value or null | |
* @return The entity or null, if no entity matched the PK value | |
*/ | |
public T load(K key) { | |
assertSinglePk(); | |
if (key == null) { | |
return null; | |
} | |
if (identityScope != null) { | |
T entity = identityScope.get(key); | |
if (entity != null) { | |
return entity; | |
} | |
} | |
String sql = statements.getSelectByKey(); | |
String[] keyArray = new String[]{key.toString()}; | |
Cursor cursor = db.rawQuery(sql, keyArray); | |
return loadUniqueAndCloseCursor(cursor); | |
} | |
public T loadByRowId(long rowId) { | |
String[] idArray = new String[]{Long.toString(rowId)}; | |
Cursor cursor = db.rawQuery(statements.getSelectByRowId(), idArray); | |
return loadUniqueAndCloseCursor(cursor); | |
} | |
protected T loadUniqueAndCloseCursor(Cursor cursor) { | |
try { | |
return loadUnique(cursor); | |
} finally { | |
cursor.close(); | |
} | |
} | |
protected T loadUnique(Cursor cursor) { | |
boolean available = cursor.moveToFirst(); | |
if (!available) { | |
return null; | |
} else if (!cursor.isLast()) { | |
throw new DaoException("Expected unique result, but count was " + cursor.getCount()); | |
} | |
return loadCurrent(cursor, 0, true); | |
} | |
/** Loads all available entities from the database. */ | |
public List<T> loadAll() { | |
Cursor cursor = db.rawQuery(statements.getSelectAll(), null); | |
return loadAllAndCloseCursor(cursor); | |
} | |
/** Detaches an entity from the identity scope (session). Subsequent query results won't return this object. */ | |
public boolean detach(T entity) { | |
if (identityScope != null) { | |
K key = getKeyVerified(entity); | |
return identityScope.detach(key, entity); | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Detaches all entities (of type T) from the identity scope (session). Subsequent query results won't return any | |
* previously loaded objects. | |
*/ | |
public void detachAll() { | |
if (identityScope != null) { | |
identityScope.clear(); | |
} | |
} | |
protected List<T> loadAllAndCloseCursor(Cursor cursor) { | |
try { | |
return loadAllFromCursor(cursor); | |
} finally { | |
cursor.close(); | |
} | |
} | |
/** | |
* Inserts the given entities in the database using a transaction. | |
* | |
* @param entities The entities to insert. | |
*/ | |
public void insertInTx(Iterable<T> entities) { | |
insertInTx(entities, isEntityUpdateable()); | |
} | |
/** | |
* Inserts the given entities in the database using a transaction. | |
* | |
* @param entities The entities to insert. | |
*/ | |
public void insertInTx(T... entities) { | |
insertInTx(Arrays.asList(entities), isEntityUpdateable()); | |
} | |
/** | |
* Inserts the given entities in the database using a transaction. The given entities will become tracked if the PK | |
* is set. | |
* | |
* @param entities The entities to insert. | |
* @param setPrimaryKey if true, the PKs of the given will be set after the insert; pass false to improve | |
* performance. | |
*/ | |
public void insertInTx(Iterable<T> entities, boolean setPrimaryKey) { | |
DatabaseStatement stmt = statements.getInsertStatement(); | |
executeInsertInTx(stmt, entities, setPrimaryKey); | |
} | |
/** | |
* Inserts or replaces the given entities in the database using a transaction. The given entities will become | |
* tracked if the PK is set. | |
* | |
* @param entities The entities to insert. | |
* @param setPrimaryKey if true, the PKs of the given will be set after the insert; pass false to improve | |
* performance. | |
*/ | |
public void insertOrReplaceInTx(Iterable<T> entities, boolean setPrimaryKey) { | |
DatabaseStatement stmt = statements.getInsertOrReplaceStatement(); | |
executeInsertInTx(stmt, entities, setPrimaryKey); | |
} | |
/** | |
* Inserts or replaces the given entities in the database using a transaction. | |
* | |
* @param entities The entities to insert. | |
*/ | |
public void insertOrReplaceInTx(Iterable<T> entities) { | |
insertOrReplaceInTx(entities, isEntityUpdateable()); | |
} | |
/** | |
* Inserts or replaces the given entities in the database using a transaction. | |
* | |
* @param entities The entities to insert. | |
*/ | |
public void insertOrReplaceInTx(T... entities) { | |
insertOrReplaceInTx(Arrays.asList(entities), isEntityUpdateable()); | |
} | |
private void executeInsertInTx(DatabaseStatement stmt, Iterable<T> entities, boolean setPrimaryKey) { | |
db.beginTransaction(); | |
try { | |
synchronized (stmt) { | |
if (identityScope != null) { | |
identityScope.lock(); | |
} | |
try { | |
if (isStandardSQLite) { | |
SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); | |
for (T entity : entities) { | |
bindValues(rawStmt, entity); | |
if (setPrimaryKey) { | |
long rowId = rawStmt.executeInsert(); | |
updateKeyAfterInsertAndAttach(entity, rowId, false); | |
} else { | |
rawStmt.execute(); | |
} | |
} | |
} else { | |
for (T entity : entities) { | |
bindValues(stmt, entity); | |
if (setPrimaryKey) { | |
long rowId = stmt.executeInsert(); | |
updateKeyAfterInsertAndAttach(entity, rowId, false); | |
} else { | |
stmt.execute(); | |
} | |
} | |
} | |
} finally { | |
if (identityScope != null) { | |
identityScope.unlock(); | |
} | |
} | |
} | |
db.setTransactionSuccessful(); | |
} finally { | |
db.endTransaction(); | |
} | |
} | |
/** | |
* Insert an entity into the table associated with a concrete DAO. | |
* | |
* @return row ID of newly inserted entity | |
*/ | |
public long insert(T entity) { | |
return executeInsert(entity, statements.getInsertStatement(), true); | |
} | |
/** | |
* Insert an entity into the table associated with a concrete DAO <b>without</b> setting key property. | |
* <p> | |
* Warning: This may be faster, but the entity should not be used anymore. The entity also won't be attached to | |
* identity scope. | |
* | |
* @return row ID of newly inserted entity | |
*/ | |
public long insertWithoutSettingPk(T entity) { | |
return executeInsert(entity, statements.getInsertOrReplaceStatement(), false); | |
} | |
/** | |
* Insert an entity into the table associated with a concrete DAO. | |
* | |
* @return row ID of newly inserted entity | |
*/ | |
public long insertOrReplace(T entity) { | |
return executeInsert(entity, statements.getInsertOrReplaceStatement(), true); | |
} | |
private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) { | |
long rowId; | |
if (db.isDbLockedByCurrentThread()) { | |
rowId = insertInsideTx(entity, stmt); | |
} else { | |
// Do TX to acquire a connection before locking the stmt to avoid deadlocks | |
db.beginTransaction(); | |
try { | |
rowId = insertInsideTx(entity, stmt); | |
db.setTransactionSuccessful(); | |
} finally { | |
db.endTransaction(); | |
} | |
} | |
if (setKeyAndAttach) { | |
updateKeyAfterInsertAndAttach(entity, rowId, true); | |
} | |
return rowId; | |
} | |
private long insertInsideTx(T entity, DatabaseStatement stmt) { | |
synchronized (stmt) { | |
if (isStandardSQLite) { | |
SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); | |
bindValues(rawStmt, entity); | |
return rawStmt.executeInsert(); | |
} else { | |
bindValues(stmt, entity); | |
return stmt.executeInsert(); | |
} | |
} | |
} | |
protected void updateKeyAfterInsertAndAttach(T entity, long rowId, boolean lock) { | |
if (rowId != -1) { | |
K key = updateKeyAfterInsert(entity, rowId); | |
attachEntity(key, entity, lock); | |
} else { | |
// TODO When does this actually happen? Should we throw instead? | |
DaoLog.w("Could not insert row (executeInsert returned -1)"); | |
} | |
} | |
/** | |
* "Saves" an entity to the database: depending on the existence of the key property, it will be inserted | |
* (key is null) or updated (key is not null). | |
* <p> | |
* This is similar to {@link #insertOrReplace(Object)}, but may be more efficient, because if a key is present, | |
* it does not have to query if that key already exists. | |
*/ | |
public void save(T entity) { | |
if (hasKey(entity)) { | |
update(entity); | |
} else { | |
insert(entity); | |
} | |
} | |
/** | |
* Saves (see {@link #save(Object)}) the given entities in the database using a transaction. | |
* | |
* @param entities The entities to save. | |
*/ | |
public void saveInTx(T... entities) { | |
saveInTx(Arrays.asList(entities)); | |
} | |
/** | |
* Saves (see {@link #save(Object)}) the given entities in the database using a transaction. | |
* | |
* @param entities The entities to save. | |
*/ | |
public void saveInTx(Iterable<T> entities) { | |
int updateCount = 0; | |
int insertCount = 0; | |
for (T entity : entities) { | |
if (hasKey(entity)) { | |
updateCount++; | |
} else { | |
insertCount++; | |
} | |
} | |
if (updateCount > 0 && insertCount > 0) { | |
List<T> toUpdate = new ArrayList<>(updateCount); | |
List<T> toInsert = new ArrayList<>(insertCount); | |
for (T entity : entities) { | |
if (hasKey(entity)) { | |
toUpdate.add(entity); | |
} else { | |
toInsert.add(entity); | |
} | |
} | |
db.beginTransaction(); | |
try { | |
updateInTx(toUpdate); | |
insertInTx(toInsert); | |
db.setTransactionSuccessful(); | |
} finally { | |
db.endTransaction(); | |
} | |
} else if (insertCount > 0) { | |
insertInTx(entities); | |
} else if (updateCount > 0) { | |
updateInTx(entities); | |
} | |
} | |
/** Reads all available rows from the given cursor and returns a list of entities. */ | |
protected List<T> loadAllFromCursor(Cursor cursor) { | |
int count = cursor.getCount(); | |
if (count == 0) { | |
return new ArrayList<T>(); | |
} | |
List<T> list = new ArrayList<T>(count); | |
CursorWindow window = null; | |
boolean useFastCursor = false; | |
if (cursor instanceof CrossProcessCursor) { | |
window = ((CrossProcessCursor) cursor).getWindow(); | |
if (window != null) { // E.g. Robolectric has no Window at this point | |
if (window.getNumRows() == count) { | |
cursor = new FastCursor(window); | |
useFastCursor = true; | |
} else { | |
DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count); | |
} | |
} | |
} | |
if (cursor.moveToFirst()) { | |
if (identityScope != null) { | |
identityScope.lock(); | |
identityScope.reserveRoom(count); | |
} | |
try { | |
if (!useFastCursor && window != null && identityScope != null) { | |
loadAllUnlockOnWindowBounds(cursor, window, list); | |
} else { | |
do { | |
list.add(loadCurrent(cursor, 0, false)); | |
} while (cursor.moveToNext()); | |
} | |
} finally { | |
if (identityScope != null) { | |
identityScope.unlock(); | |
} | |
} | |
} | |
return list; | |
} | |
private void loadAllUnlockOnWindowBounds(Cursor cursor, CursorWindow window, List<T> list) { | |
int windowEnd = window.getStartPosition() + window.getNumRows(); | |
for (int row = 0; ; row++) { | |
list.add(loadCurrent(cursor, 0, false)); | |
row++; | |
if (row >= windowEnd) { | |
window = moveToNextUnlocked(cursor); | |
if (window == null) { | |
break; | |
} | |
windowEnd = window.getStartPosition() + window.getNumRows(); | |
} else { | |
if (!cursor.moveToNext()) { | |
break; | |
} | |
} | |
} | |
} | |
/** | |
* Unlock identityScope during cursor.moveToNext() when it is about to fill the window (needs a db connection): | |
* We should not hold the lock while trying to acquire a db connection to avoid deadlocks. | |
*/ | |
private CursorWindow moveToNextUnlocked(Cursor cursor) { | |
identityScope.unlock(); | |
try { | |
if (cursor.moveToNext()) { | |
return ((CrossProcessCursor) cursor).getWindow(); | |
} else { | |
return null; | |
} | |
} finally { | |
identityScope.lock(); | |
} | |
} | |
/** Internal use only. Considers identity scope. */ | |
final protected T loadCurrent(Cursor cursor, int offset, boolean lock) { | |
if (identityScopeLong != null) { | |
if (offset != 0) { | |
// Occurs with deep loads (left outer joins) | |
if (cursor.isNull(pkOrdinal + offset)) { | |
return null; | |
} | |
} | |
long key = cursor.getLong(pkOrdinal + offset); | |
T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key); | |
if (entity != null) { | |
return entity; | |
} else { | |
entity = readEntity(cursor, offset); | |
attachEntity(entity); | |
if (lock) { | |
identityScopeLong.put2(key, entity); | |
} else { | |
identityScopeLong.put2NoLock(key, entity); | |
} | |
return entity; | |
} | |
} else if (identityScope != null) { | |
K key = readKey(cursor, offset); | |
if (offset != 0 && key == null) { | |
// Occurs with deep loads (left outer joins) | |
return null; | |
} | |
T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key); | |
if (entity != null) { | |
return entity; | |
} else { | |
entity = readEntity(cursor, offset); | |
attachEntity(key, entity, lock); | |
return entity; | |
} | |
} else { | |
// Check offset, assume a value !=0 indicating a potential outer join, so check PK | |
if (offset != 0) { | |
K key = readKey(cursor, offset); | |
if (key == null) { | |
// Occurs with deep loads (left outer joins) | |
return null; | |
} | |
} | |
T entity = readEntity(cursor, offset); | |
attachEntity(entity); | |
return entity; | |
} | |
} | |
/** Internal use only. Considers identity scope. */ | |
final protected <O> O loadCurrentOther(AbstractDao<O, ?> dao, Cursor cursor, int offset) { | |
return dao.loadCurrent(cursor, offset, /* TODO check this */true); | |
} | |
/** A raw-style query where you can pass any WHERE clause and arguments. */ | |
public List<T> queryRaw(String where, String... selectionArg) { | |
Cursor cursor = db.rawQuery(statements.getSelectAll() + where, selectionArg); | |
return loadAllAndCloseCursor(cursor); | |
} | |
/** | |
* Creates a repeatable {@link Query} object based on the given raw SQL where you can pass any WHERE clause and | |
* arguments. | |
*/ | |
public Query<T> queryRawCreate(String where, Object... selectionArg) { | |
List<Object> argList = Arrays.asList(selectionArg); | |
return queryRawCreateListArgs(where, argList); | |
} | |
/** | |
* Creates a repeatable {@link Query} object based on the given raw SQL where you can pass any WHERE clause and | |
* arguments. | |
*/ | |
public Query<T> queryRawCreateListArgs(String where, Collection<Object> selectionArg) { | |
return Query.internalCreate(this, statements.getSelectAll() + where, selectionArg.toArray()); | |
} | |
public void deleteAll() { | |
// String sql = SqlUtils.createSqlDelete(config.tablename, null); | |
// db.execSQL(sql); | |
db.execSQL("DELETE FROM '" + config.tablename + "'"); | |
if (identityScope != null) { | |
identityScope.clear(); | |
} | |
} | |
/** Deletes the given entity from the database. Currently, only single value PK entities are supported. */ | |
public void delete(T entity) { | |
assertSinglePk(); | |
K key = getKeyVerified(entity); | |
deleteByKey(key); | |
} | |
/** Deletes an entity with the given PK from the database. Currently, only single value PK entities are supported. */ | |
public void deleteByKey(K key) { | |
assertSinglePk(); | |
DatabaseStatement stmt = statements.getDeleteStatement(); | |
if (db.isDbLockedByCurrentThread()) { | |
synchronized (stmt) { | |
deleteByKeyInsideSynchronized(key, stmt); | |
} | |
} else { | |
// Do TX to acquire a connection before locking the stmt to avoid deadlocks | |
db.beginTransaction(); | |
try { | |
synchronized (stmt) { | |
deleteByKeyInsideSynchronized(key, stmt); | |
} | |
db.setTransactionSuccessful(); | |
} finally { | |
db.endTransaction(); | |
} | |
} | |
if (identityScope != null) { | |
identityScope.remove(key); | |
} | |
} | |
private void deleteByKeyInsideSynchronized(K key, DatabaseStatement stmt) { | |
if (key instanceof Long) { | |
stmt.bindLong(1, (Long) key); | |
} else if (key == null) { | |
throw new DaoException("Cannot delete entity, key is null"); | |
} else { | |
stmt.bindString(1, key.toString()); | |
} | |
stmt.execute(); | |
} | |
private void deleteInTxInternal(Iterable<T> entities, Iterable<K> keys) { | |
assertSinglePk(); | |
DatabaseStatement stmt = statements.getDeleteStatement(); | |
List<K> keysToRemoveFromIdentityScope = null; | |
db.beginTransaction(); | |
try { | |
synchronized (stmt) { | |
if (identityScope != null) { | |
identityScope.lock(); | |
keysToRemoveFromIdentityScope = new ArrayList<K>(); | |
} | |
try { | |
if (entities != null) { | |
for (T entity : entities) { | |
K key = getKeyVerified(entity); | |
deleteByKeyInsideSynchronized(key, stmt); | |
if (keysToRemoveFromIdentityScope != null) { | |
keysToRemoveFromIdentityScope.add(key); | |
} | |
} | |
} | |
if (keys != null) { | |
for (K key : keys) { | |
deleteByKeyInsideSynchronized(key, stmt); | |
if (keysToRemoveFromIdentityScope != null) { | |
keysToRemoveFromIdentityScope.add(key); | |
} | |
} | |
} | |
} finally { | |
if (identityScope != null) { | |
identityScope.unlock(); | |
} | |
} | |
} | |
db.setTransactionSuccessful(); | |
if (keysToRemoveFromIdentityScope != null && identityScope != null) { | |
identityScope.remove(keysToRemoveFromIdentityScope); | |
} | |
} finally { | |
db.endTransaction(); | |
} | |
} | |
/** | |
* Deletes the given entities in the database using a transaction. | |
* | |
* @param entities The entities to delete. | |
*/ | |
public void deleteInTx(Iterable<T> entities) { | |
deleteInTxInternal(entities, null); | |
} | |
/** | |
* Deletes the given entities in the database using a transaction. | |
* | |
* @param entities The entities to delete. | |
*/ | |
public void deleteInTx(T... entities) { | |
deleteInTxInternal(Arrays.asList(entities), null); | |
} | |
/** | |
* Deletes all entities with the given keys in the database using a transaction. | |
* | |
* @param keys Keys of the entities to delete. | |
*/ | |
public void deleteByKeyInTx(Iterable<K> keys) { | |
deleteInTxInternal(null, keys); | |
} | |
/** | |
* Deletes all entities with the given keys in the database using a transaction. | |
* | |
* @param keys Keys of the entities to delete. | |
*/ | |
public void deleteByKeyInTx(K... keys) { | |
deleteInTxInternal(null, Arrays.asList(keys)); | |
} | |
/** Resets all locally changed properties of the entity by reloading the values from the database. */ | |
public void refresh(T entity) { | |
assertSinglePk(); | |
K key = getKeyVerified(entity); | |
String sql = statements.getSelectByKey(); | |
String[] keyArray = new String[]{key.toString()}; | |
Cursor cursor = db.rawQuery(sql, keyArray); | |
try { | |
boolean available = cursor.moveToFirst(); | |
if (!available) { | |
throw new DaoException("Entity does not exist in the database anymore: " + entity.getClass() | |
+ " with key " + key); | |
} else if (!cursor.isLast()) { | |
throw new DaoException("Expected unique result, but count was " + cursor.getCount()); | |
} | |
readEntity(cursor, entity, 0); | |
attachEntity(key, entity, true); | |
} finally { | |
cursor.close(); | |
} | |
} | |
public void update(T entity) { | |
assertSinglePk(); | |
DatabaseStatement stmt = statements.getUpdateStatement(); | |
if (db.isDbLockedByCurrentThread()) { | |
synchronized (stmt) { | |
if (isStandardSQLite) { | |
updateInsideSynchronized(entity, (SQLiteStatement) stmt.getRawStatement(), true); | |
} else { | |
updateInsideSynchronized(entity, stmt, true); | |
} | |
} | |
} else { | |
// Do TX to acquire a connection before locking the stmt to avoid deadlocks | |
db.beginTransaction(); | |
try { | |
synchronized (stmt) { | |
updateInsideSynchronized(entity, stmt, true); | |
} | |
db.setTransactionSuccessful(); | |
} finally { | |
db.endTransaction(); | |
} | |
} | |
} | |
public QueryBuilder<T> queryBuilder() { | |
return QueryBuilder.internalCreate(this); | |
} | |
protected void updateInsideSynchronized(T entity, DatabaseStatement stmt, boolean lock) { | |
// To do? Check if it's worth not to bind PKs here (performance). | |
bindValues(stmt, entity); | |
int index = config.allColumns.length + 1; | |
K key = getKey(entity); | |
if (key instanceof Long) { | |
stmt.bindLong(index, (Long) key); | |
} else if (key == null) { | |
throw new DaoException("Cannot update entity without key - was it inserted before?"); | |
} else { | |
stmt.bindString(index, key.toString()); | |
} | |
stmt.execute(); | |
attachEntity(key, entity, lock); | |
} | |
protected void updateInsideSynchronized(T entity, SQLiteStatement stmt, boolean lock) { | |
// To do? Check if it's worth not to bind PKs here (performance). | |
bindValues(stmt, entity); | |
int index = config.allColumns.length + 1; | |
K key = getKey(entity); | |
if (key instanceof Long) { | |
stmt.bindLong(index, (Long) key); | |
} else if (key == null) { | |
throw new DaoException("Cannot update entity without key - was it inserted before?"); | |
} else { | |
stmt.bindString(index, key.toString()); | |
} | |
stmt.execute(); | |
attachEntity(key, entity, lock); | |
} | |
/** | |
* Attaches the entity to the identity scope. Calls attachEntity(T entity). | |
* | |
* @param key Needed only for identity scope, pass null if there's none. | |
* @param entity The entitiy to attach | |
*/ | |
protected final void attachEntity(K key, T entity, boolean lock) { | |
attachEntity(entity); | |
if (identityScope != null && key != null) { | |
if (lock) { | |
identityScope.put(key, entity); | |
} else { | |
identityScope.putNoLock(key, entity); | |
} | |
} | |
} | |
/** | |
* Sub classes with relations additionally set the DaoMaster here. Must be called before the entity is attached to | |
* the identity scope. | |
* | |
* @param entity The entitiy to attach | |
*/ | |
protected void attachEntity(T entity) { | |
} | |
/** | |
* Updates the given entities in the database using a transaction. | |
* | |
* @param entities The entities to insert. | |
*/ | |
public void updateInTx(Iterable<T> entities) { | |
DatabaseStatement stmt = statements.getUpdateStatement(); | |
db.beginTransaction(); | |
// txEx: just to preserve original exception in case another exceptions is thrown in endTransaction() | |
RuntimeException txEx = null; | |
try { | |
synchronized (stmt) { | |
if (identityScope != null) { | |
identityScope.lock(); | |
} | |
try { | |
if (isStandardSQLite) { | |
SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement(); | |
for (T entity : entities) { | |
updateInsideSynchronized(entity, rawStmt, false); | |
} | |
} else { | |
for (T entity : entities) { | |
updateInsideSynchronized(entity, stmt, false); | |
} | |
} | |
} finally { | |
if (identityScope != null) { | |
identityScope.unlock(); | |
} | |
} | |
} | |
db.setTransactionSuccessful(); | |
} catch (RuntimeException e) { | |
txEx = e; | |
} finally { | |
try { | |
db.endTransaction(); | |
} catch (RuntimeException e) { | |
if (txEx != null) { | |
DaoLog.w("Could not end transaction (rethrowing initial exception)", e); | |
throw txEx; | |
} else { | |
throw e; | |
} | |
} | |
} | |
if (txEx != null) { | |
throw txEx; | |
} | |
} | |
/** | |
* Updates the given entities in the database using a transaction. | |
* | |
* @param entities The entities to update. | |
*/ | |
public void updateInTx(T... entities) { | |
updateInTx(Arrays.asList(entities)); | |
} | |
protected void assertSinglePk() { | |
if (config.pkColumns.length != 1) { | |
throw new DaoException(this + " (" + config.tablename + ") does not have a single-column primary key"); | |
} | |
} | |
public long count() { | |
return statements.getCountStatement().simpleQueryForLong(); | |
} | |
/** See {@link #getKey(Object)}, but guarantees that the returned key is never null (throws if null). */ | |
protected K getKeyVerified(T entity) { | |
K key = getKey(entity); | |
if (key == null) { | |
if (entity == null) { | |
throw new NullPointerException("Entity may not be null"); | |
} else { | |
throw new DaoException("Entity has no key"); | |
} | |
} else { | |
return key; | |
} | |
} | |
/** | |
* The returned RxDao is a special DAO that let's you interact with Rx Observables without any Scheduler set | |
* for subscribeOn. | |
* | |
* @see #rx() | |
*/ | |
@Experimental | |
public RxDao<T, K> rxPlain() { | |
if (rxDaoPlain == null) { | |
rxDaoPlain = new RxDao<>(this); | |
} | |
return rxDaoPlain; | |
} | |
/** | |
* The returned RxDao is a special DAO that let's you interact with Rx Observables using RX's IO scheduler for | |
* subscribeOn. | |
* | |
* @see #rxPlain() | |
*/ | |
@Experimental | |
public RxDao<T, K> rx() { | |
if (rxDao == null) { | |
rxDao = new RxDao<>(this, Schedulers.io()); | |
} | |
return rxDao; | |
} | |
/** Gets the SQLiteDatabase for custom database access. Not needed for greenDAO entities. */ | |
public Database getDatabase() { | |
return db; | |
} | |
/** Reads the values from the current position of the given cursor and returns a new entity. */ | |
abstract protected T readEntity(Cursor cursor, int offset); | |
/** Reads the key from the current position of the given cursor, or returns null if there's no single-value key. */ | |
abstract protected K readKey(Cursor cursor, int offset); | |
/** Reads the values from the current position of the given cursor into an existing entity. */ | |
abstract protected void readEntity(Cursor cursor, T entity, int offset); | |
/** Binds the entity's values to the statement. Make sure to synchronize the statement outside of the method. */ | |
abstract protected void bindValues(DatabaseStatement stmt, T entity); | |
/** | |
* Binds the entity's values to the statement. Make sure to synchronize the enclosing DatabaseStatement outside | |
* of the method. | |
*/ | |
protected abstract void bindValues(SQLiteStatement stmt, T entity); | |
/** | |
* Updates the entity's key if possible (only for Long PKs currently). This method must always return the entity's | |
* key regardless of whether the key existed before or not. | |
*/ | |
abstract protected K updateKeyAfterInsert(T entity, long rowId); | |
/** | |
* Returns the value of the primary key, if the entity has a single primary key, or, if not, null. Returns null if | |
* entity is null. | |
*/ | |
abstract protected K getKey(T entity); | |
/** | |
* Returns true if the entity is not null, and has a non-null key, which is also != 0. | |
* entity is null. | |
*/ | |
abstract protected boolean hasKey(T entity); | |
/** Returns true if the Entity class can be updated, e.g. for setting the PK after insert. */ | |
abstract protected boolean isEntityUpdateable(); | |
} |