Room - #Upsert annotation giving me SQLiteConstraintException: UNIQUE constraint failed - android

I'm trying to do an upsert with Android Room's #Upsert annotation, but I'm unsure what I'm doing incorrectly because when it's returning me the exception:
android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: coin_status.coin_query_id (code 2067 SQLITE_CONSTRAINT_UNIQUE)
My Dao object with the upsert annotation is:
#Dao
public interface CoinStatusDao {
#Upsert
Single<List<Long>> updateFavorites(List<CoinStatus> favoriteCoins);
}
With the CoinStatus object:
#Entity(tableName = "coin_status", indices = {#Index(value = {"coin_query_id"}, unique = true)})
public class CoinStatus {
#PrimaryKey(autoGenerate = true)
private int primaryKey;
#ColumnInfo(name = "coin_query_id")
private String coinQueryId;
#ColumnInfo(name = "is_favorite")
private boolean isFavorite;
public CoinStatus(String coinQueryId, boolean isFavorite) {
this.coinQueryId = coinQueryId;
this.isFavorite = isFavorite;
}
...
...
//getters and setters
}
I am able to successfully do inserts and querys with no problem
#Insert(onConflict = OnConflictStrategy.IGNORE)
Single<List<Long>> insertFavorite(List<CoinStatus> coinStatuses);
#Query("SELECT * FROM coin_status ORDER BY coin_query_id ASC")
Single<List<CoinStatus>> getFavoriteCoins();
But I'm not sure what I'm missing with the Upsert.
What am I doing incorrectly here?

It is because you have two UNIQUE constraints, the Primary Key and the index on the coinQuery.
If you drop the primaryKey member variable then the UPSERT will work fine.
e.g. :-
#Entity(tableName = "coin_status"/*, indices = {#Index(value = {"coin_query_id"}, unique = true)}*/)
public class CoinStatus {
//#PrimaryKey(autoGenerate = true)
//private int primaryKey;
#PrimaryKey
#NotNull
#ColumnInfo(name = "coin_query_id")
private String coinQueryId;
#ColumnInfo(name = "is_favorite")
private boolean isFavorite;
public CoinStatus(String coinQueryId, boolean isFavorite) {
this.coinQueryId = coinQueryId;
this.isFavorite = isFavorite;
}
/*
public int getPrimaryKey() {
return primaryKey;
}
*/
public String getCoinQueryId() {
return coinQueryId;
}
public boolean isFavorite() {
return isFavorite;
}
/*
public void setPrimaryKey(int primaryKey) {
this.primaryKey = primaryKey;
}
*/
public void setCoinQueryId(String coinQueryId) {
this.coinQueryId = coinQueryId;
}
public void setFavorite(boolean favorite) {
isFavorite = favorite;
}
}
note that rather than excluding code, the excluded code has been commented out
With the above and :-
public class MainActivity extends AppCompatActivity {
private static final String TAG = "DBINFO";
TheDatabase db;
CoinStatusDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = TheDatabase.getInstance(this);
dao = db.getCoinStatusDao();
try {
dao.updateFavorites(Arrays.asList(
new CoinStatus("Penny", true),
new CoinStatus("Cent", false)
));
logCurrentCoinStatusRow("_RUN1");
dao.updateFavorites(Arrays.asList(
new CoinStatus("Penny", false),
new CoinStatus("Cent", true),
new CoinStatus("Shilling",true)
));
logCurrentCoinStatusRow("_RUN2");
}
catch (SQLiteException e) {
e.printStackTrace();
}
}
private void logCurrentCoinStatusRow(String tagSuffix) {
for(CoinStatus c: dao.getFavoriteCoins()) {
Log.d(TAG + tagSuffix," CoinQueryID = " + c.getCoinQueryId() + " IsFavourite is " + c.isFavorite());
}
}
}
The the log includes:-
D/DBINFO_RUN1: CoinQueryID = Cent IsFavourite is false
D/DBINFO_RUN1: CoinQueryID = Penny IsFavourite is true
D/DBINFO_RUN2: CoinQueryID = Cent IsFavourite is true
D/DBINFO_RUN2: CoinQueryID = Penny IsFavourite is false
D/DBINFO_RUN2: CoinQueryID = Shilling IsFavourite is true
i.e. the two non-existent rows have been inserted as expected and in the 2nd run the Cent and Penny have been updated (flipped the IsFavourite value) and the Shilling has been added.
Without modifying the CointStatus class
The alternative would be to keep the dual UNQIUE indexes and thus no changes to CoinStatus but to instead ensure that existing CoinStatus's have all values set (so as to not try to generate the primaryKey).
This would be the more awkward solution and very likely less efficient as you instead of need to just know the coinQueryId you have to also get the associated primaryKey which would likely mean getting it/them from the table.
So using the original CoinStatus then the second RUN will work if the code is changed, as an example, to:-
dao.updateFavorites(Arrays.asList(
new CoinStatus("Penny", true),
new CoinStatus("Cent", false)
));
logCurrentCoinStatusRow("_RUN1");
List<CoinStatus> coins = dao.getFavoriteCoins();
for (CoinStatus c: coins) {
c.setFavorite(!c.isFavorite());
}
coins.add(new CoinStatus("Shilling",true));
dao.updateFavorites(coins);
logCurrentCoinStatusRow("_RUN2");
i.e. the primaryKey values are being obtained and then the isFavorite value is being switched, then the new Shilling is being added. The results being identical:-
D/DBINFO_RUN1: CoinQueryID = Cent IsFavourite is false
D/DBINFO_RUN1: CoinQueryID = Penny IsFavourite is true
D/DBINFO_RUN2: CoinQueryID = Cent IsFavourite is true
D/DBINFO_RUN2: CoinQueryID = Penny IsFavourite is false
D/DBINFO_RUN2: CoinQueryID = Shilling IsFavourite is true

Related

Android Room Database auto-genereated code does not compile

I cannot compile the project because the auto-generated code does not compile.
Here is my code:
I'm using version 2.4.2 of Room
My database class:
RecipeDatabase.java
#Database(entities = {Person.class, Recipe.class}, version = 1)
public abstract class RecipeDatabase extends RoomDatabase {
public abstract PersonDao personDao();
public abstract RecipeDao recipeDao();
}
Entity classes:
Person.java
#Entity(tableName = "people")
public class Person {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "person_id")
public int id;
public String name;
public Person(String name) {
this.name = name;
}
}
Recipe.java
#Entity(tableName = "recipes")
public class Recipe {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "recipe_id")
public int id;
public String name;
public FoodType type; //it is an enum
public Recipe(String name, FoodType type) {
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public String getTypeName() {
return type.getName();
}
public int getImageResource() {
return type.getImgResource();
}
}
DAO classes:
PersonDao.java
#Dao
public interface PersonDao {
#Query("SELECT * FROM people")
List<Person> getAll();
#Query("SELECT name FROM people")
List<String> getNames();
}
RecipeDao.java
#Dao
public interface RecipeDao {
#Query("SELECT * FROM recipes")
List<Recipe> getAll();
#Query("SELECT name FROM recipes")
List<String> getNames();
}
And here is the code which does not compile (under java (generated)):
RecipeDatabase_Impl.java
#Override
protected void onCreate(SupportSQLiteDatabase _db) {
if (mCallbacks != null) {
for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
mCallbacks.get(_i).onCreate(_db);
}
}
}
...
#Override
protected RoomOpenHelper.ValidationResult onValidateSchema(SupportSQLiteDatabase _db) {
final HashMap<String, TableInfo.Column> _columnsPeople = new HashMap<String, TableInfo.Column>(2);
_columnsPeople.put("person_id", new TableInfo.Column("person_id", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
_columnsPeople.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
final HashSet<TableInfo.ForeignKey> _foreignKeysPeople = new HashSet<TableInfo.ForeignKey>(0);
final HashSet<TableInfo.Index> _indicesPeople = new HashSet<TableInfo.Index>(0);
final TableInfo _infoPeople = new TableInfo("people", _columnsPeople, _foreignKeysPeople, _indicesPeople);
final TableInfo _existingPeople = TableInfo.read(_db, "people");
if (! _infoPeople.equals(_existingPeople)) {
return new RoomOpenHelper.ValidationResult(false, "people(com.szabolcst.recipes.model.Person).\n"
+ " Expected:\n" + _infoPeople + "\n"
+ " Found:\n" + _existingPeople);
}
final HashMap<String, TableInfo.Column> _columnsRecipes = new HashMap<String, TableInfo.Column>(3);
_columnsRecipes.put("recipe_id", new TableInfo.Column("recipe_id", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
_columnsRecipes.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
_columnsRecipes.put("type", new TableInfo.Column("type", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
final HashSet<TableInfo.ForeignKey> _foreignKeysRecipes = new HashSet<TableInfo.ForeignKey>(0);
final HashSet<TableInfo.Index> _indicesRecipes = new HashSet<TableInfo.Index>(0);
final TableInfo _infoRecipes = new TableInfo("recipes", _columnsRecipes, _foreignKeysRecipes, _indicesRecipes);
final TableInfo _existingRecipes = TableInfo.read(_db, "recipes");
if (! _infoRecipes.equals(_existingRecipes)) {
return new RoomOpenHelper.ValidationResult(false, "recipes(com.szabolcst.recipes.model.Recipe).\n"
+ " Expected:\n" + _infoRecipes + "\n"
+ " Found:\n" + _existingRecipes);
}
return new RoomOpenHelper.ValidationResult(true, null);
}
}, "1c493a2b22e38e23def00f0336257ee2", "6829c4aff16c254527acbdc5d6cecc85");
final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(_openCallback)
.build();
final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
return _helper;
}
In both methods the problem is with the access modifer (protected) and the error goes: 'attempting to assign weaker access privileges; was public'
The methods that these override are:
RoomOpenHelper.kt
#RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
abstract class Delegate(#JvmField val version: Int) {
abstract fun dropAllTables(database: SupportSQLiteDatabase)
abstract fun createAllTables(database: SupportSQLiteDatabase)
abstract fun onOpen(database: SupportSQLiteDatabase)
abstract fun onCreate(database: SupportSQLiteDatabase)
...
#Suppress("DEPRECATION")
open fun onValidateSchema(db: SupportSQLiteDatabase): ValidationResult {
validateMigration(db)
return ValidationResult(true, null)
}
The full errors:
error: onCreate(SupportSQLiteDatabase) in <anonymous com.szabolcst.recipes.persistance.RecipeDatabase_Impl$1> cannot override onCreate(SupportSQLiteDatabase) in Delegate
protected void onCreate(SupportSQLiteDatabase _db) {
^
attempting to assign weaker access privileges; was public
error: onValidateSchema(SupportSQLiteDatabase) in <anonymous com.szabolcst.recipes.persistance.RecipeDatabase_Impl$1> cannot override onValidateSchema(SupportSQLiteDatabase) in Delegate
protected RoomOpenHelper.ValidationResult onValidateSchema(SupportSQLiteDatabase _db) {
^
attempting to assign weaker access privileges; was public
Naturally editing anything in the generated files does not help, and I can't find anything like this on any forums
Changing implementation "androidx.room:room-runtime:2.4.2" back to implementation "androidx.room:room-runtime:2.3.0" seemed to help as now RoomOpenHelper.java is used instead of the kotlin version and in the java version the access modifer is protected.

How can I reference variables of generic types?

I am trying to implement a "base DAO" interface for the Room library so as to avoid boilerplate code:
BaseEntity.kt
interface BaseEntity {
val entityName: String
}
Note.kt
#Entity
class Note : BaseEntity {
override val entityName: String = "note"
...
}
BaseDao.kt
interface BaseDao<T : BaseEntity> {
#Query("SELECT * FROM ${T.entityName}")
fun selectAll(): List<T>
...
}
NoteDao.kt
#Dao
interface NoteDao : BaseDao<Note> {
...
}
However, the expression ${T.entityName} is invalid. Is there a way to do this?
I don't believe that you can implement a "base DAO" interface. The reason is that Room creates each DAO implementation at compile time. And hence why you get the message An annotation argument must be a compile time-constant.
Room needs to know, from the annotation (for example), which table columns to map to which variables and the methods used to perform the mapping so that the underlying code can be generated.
As an example if the Entity and the Dao were :-
#Entity
class Note {
#PrimaryKey
var entityName: String = ""
}
and
#Dao
interface BaseDao {
#Query("SELECT * FROM Note")
fun selectAll(): List<Note>
}
Then the underlying generated java would be :-
public final class BaseDao_Impl implements BaseDao {
private final RoomDatabase __db;
public BaseDao_Impl(RoomDatabase __db) {
this.__db = __db;
}
#Override
public List<Note> selectAll() {
final String _sql = "SELECT * FROM Note";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _cursorIndexOfEntityName = CursorUtil.getColumnIndexOrThrow(_cursor, "entityName");
final List<Note> _result = new ArrayList<Note>(_cursor.getCount());
while(_cursor.moveToNext()) {
final Note _item;
_item = new Note();
final String _tmpEntityName;
_tmpEntityName = _cursor.getString(_cursorIndexOfEntityName);
_item.setEntityName(_tmpEntityName);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
}

Room (SQLite) WHERE clause with null arguments doesn't work

I have entity class Person and I want to query persons from database by feature which may or may not be null. When I set feature to non-null the query works but when the feature is null the query returns empty list. What I am doing wrong?
TEST
Context context = InstrumentationRegistry.getContext();
AppDb db = Room.inMemoryDatabaseBuilder(context, AppDb.class).allowMainThreadQueries().build();
PersonDao dao = db.personDao();
dao.insert(new Person("El Risitas", "Funny"));
dao.insert(new Person("Elon Musk", "Alien"));
dao.insert(new Person("Donald Trump", null));
assertEquals(3, dao.getAll().size());
assertEquals("Funny", dao.getByFeature("Funny").get(0).getFeature());
// fails because dao.getByFeature(null) = EMPTY LIST
assertEquals(null, dao.getByFeature(null).get(0).getFeature());
Person.java
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
#Entity(tableName = "person")
public class Person {
#ColumnInfo(name = "id") #PrimaryKey(autoGenerate = true) private int id;
#ColumnInfo(name = "name") private String name;
#ColumnInfo(name = "feature") private String feature;
public Person(String name, String feature) {
this.name = name;
this.feature = feature;
}
public void setId(int id) { this.id = id; }
public int getId() { return id; }
public String getName() { return name; }
public String getFeature() { return feature; }
}
PersonDao.java
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
#Dao
public interface PersonDao {
#Insert
void insert(Person person);
#Query("SELECT * FROM person")
List<Person> getAll();
#Query("SELECT * FROM person WHERE feature = :feature")
List<Person> getByFeature(String feature);
}
AppDb.java
import androidx.room.Database;
import androidx.room.RoomDatabase;
#Database(
version = 1,
exportSchema = false,
entities = {Person.class}
)
public abstract class AppDb extends RoomDatabase {
public abstract PersonDao personDao();
}
In SQL, nothing is ever equal to null. (Nothing is ever not equal to null, either.) You have to use is null. So your query could be something like (untested)
SELECT * FROM person WHERE feature = :feature or (feature is null and :feature is null)
You can also use IS operator instead of =.
SELECT * FROM person WHERE feature IS :feature
From SQLite documentation:
The IS and IS NOT operators work like = and != except when one or both
of the operands are NULL. In this case, if both operands are NULL,
then the IS operator evaluates to 1 (true) and the IS NOT operator
evaluates to 0 (false). If one operand is NULL and the other is not,
then the IS operator evaluates to 0 (false) and the IS NOT operator is
1 (true). It is not possible for an IS or IS NOT expression to
evaluate to NULL.

Can`t query using 'where' between two tables in one DB?

Today one problem came to me when I was learning Room in Android.
Everything is ok but I just can`t query with where between two tables.
Here are my codes:
......................................................................................................................................................
this code works well
#Query("select users.id,users.name from users where score > :score and assist > :assist")
List<UserSimple> getUserWithLimits(int score,int assist);
but the follow one can`t get anything:
#Query("select users.id,users.name from users,performs where users.id = performs.id and performs.score > :score and performs.assist > :assist")
List<UserSimple> getUserWithLimits(int score,int assist);
and here are my tables created using Room:
........................................................................................................................................................
user table:
#Entity(tableName = "users",
primaryKeys = {"id", "name"},
indices = {
#Index(value = "id", unique = true)
})
public class User {
#android.support.annotation.NonNull
#ColumnInfo(name = "id")
private String id;
#android.support.annotation.NonNull
#ColumnInfo(name = "name")
private String name;
#ColumnInfo(name = "position")
private String position;
#Embedded
private UserPerforms performs;
#Ignore
public User() {
}
public User(String id, String name, String position, UserPerforms performs) {
this.id = id;
this.name = name;
this.position = position;
this.performs = performs;
}
getters/setters/toString()...
}
........................................................................................................................................................
performs table:
#Entity(tableName = "performs",
primaryKeys = "p_id",
foreignKeys = #ForeignKey(entity = User.class
, parentColumns = "id"
, childColumns = "p_id")) //定义主键
public class UserPerforms {
#android.support.annotation.NonNull //
#ColumnInfo(name = "p_id")
private String p_id;
#ColumnInfo(name = "score")
private int score;
#ColumnInfo(name = "assist")
private int assist;
#Ignore
public UserPerforms() {
}
public UserPerforms(String p_id, int score, int assist) {
this.p_id = p_id;
this.score = score;
this.assist = assist;
}
...getters/setters/toString()..
}
........................................................................................................................................................
userSimple class:
public class UserSimple {
#ColumnInfo(name = "id")
private String id;
#ColumnInfo(name = "name")
private String name;
...getters/setters/toString()
public UserSimple(String id, String name) {
this.id = id;
this.name = name;
}
}
Anyone can help me?Thanks in advance.
You are joining wrong columns. Your id column in the performs table is p_id, not id. Try this:
#Query("select users.id,users.name from users,performs where users.id = performs.p_id and performs.score > :score and performs.assist > :assist")

How to delete an object where it consist with multiple objects in ORM-Lite

I am using ORM-lite for an Android application, Model class is as follows,
#DatabaseTable(tableName = "pageimage")
public class PageImage
{
#DatabaseField(generatedId = true)
private int pageimageId;
#DatabaseField(foreign = true, foreignAutoRefresh = true, uniqueCombo=true)
private Page page;
#DatabaseField(foreign = true, foreignAutoRefresh = true, uniqueCombo=true)
private Image image;
#DatabaseField
private int order;
//implementation
}
PageImage object is created with combination of Page and Image objects referenced to Page and Image tables. i have searched a lot but still unable to find a away to delete a PageImage object where Page id = "some value" and Image id = "some value" .
Appreciate any ideas.
You can use DeleteBuilder to do that.
private void deletePageItem(Page page, Image image) {
try {
Dao<PageItem, ?> dao = mOpenHelper.getDao(PageItem.class);
DeleteBuilder<PageItem, ?> deleteBuilder = dao.deleteBuilder();
deleteBuilder.where().eq(PageItem.COLUMN_IMAGE, image).and().eq(PageItem.COLUMN_PAGE, page);
deleteBuilder.delete();
} catch (SQLException e) {
e.printStackTrace();
}
}
where mOpenHelper is entity extended from OrmLiteSqliteOpenHelper
PageItem.java
#DatabaseTable(tableName = "pageimage")
public class PageItem {
public static final String COLUMN_PAGE = "page";
public static final String COLUMN_IMAGE = "image";
#DatabaseField(generatedId = true)
private int pageimageId;
#DatabaseField(columnName = COLUMN_PAGE, foreign = true, foreignAutoRefresh = true, uniqueCombo = true)
private Page page;
#DatabaseField(columnName = COLUMN_IMAGE, foreign = true, foreignAutoRefresh = true, uniqueCombo = true)
private Image image;
#DatabaseField
private int order;
}

Categories

Resources