2

User Code:

@Entity
public class User {
    @PrimaryKey(autoGenerate = true)
    public Integer uid;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;
}

Dao:

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
        "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    void insertAll(User user);

    @Delete
    void delete(User user);
}

Activity:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DataBase db = Room.databaseBuilder(getApplicationContext(), DataBase.class, "DB")
                .allowMainThreadQueries().build();

        User user = new User();
        user.firstName = "Ste";
        user.lastName = "Kor";

        UserDao userDao = db.userDao();
        userDao.insertAll(user);
    }
}

As I understood from the documentation, @PrimaryKey(autoGenerate = true) should generate a new identifier for the uid. But when I add a new object, it is assigned the ID 0 and the application crashes.

If you set the value manually, then everything works, but I need it to be set by itself

kirialium
  • 31
  • 4
  • Primary keys should be non-null - try changing the type from `Integer` to `int`. – dominicoder Apr 26 '23 at 16:28
  • Try `public Integer uid = null;` (note the `= null`) portion. See [_"How to auto generate primary id, without using "autoGenerate = true""_](https://stackoverflow.com/a/71073465/1762224). As @dominicoder states, you can alternatively use the primitive `int`. The preferred type for a non-null primary ID key is typically `long`. – Mr. Polywhirl Apr 26 '23 at 16:29
  • I changed the data types. I tried int, Integer, long, Long. id is zero anyway. After all, I just need the data to be added to a new row in the table. – kirialium Apr 26 '23 at 17:00
  • @Mr.Polywhirl Explicit use of `= null` should be unnecessary, because that is already the default value of an Object field like `Integer`. – Mark Rotteveel Apr 28 '23 at 10:53

1 Answers1

2

If using Integer then Room expects the uid to be null for a value to be generated. If a value, such as 0 is used then that value will be used for the uid.

As the insertAll method doesn't specify an onConflict strategy, then the default strategy is ABORT. As such the use of 0, more than once will result in a crash due to the UNIQUE constraint conflict (the value as it is a Primary Key must be unique).

  • an onConflict strategy of IGNORE (e.g. @Insert(onConflict = OnConflictStrategy.IGNORE)) will not result in a crash if there is a duplicate, but the row will not be inserted.

  • an onConflict strategy of REPLACE (e.g. @Insert(onConflict = OnConflictStrategy.REPLACE)), will replace the already existing row, by deleting the row and inserting another (as opposed to updating the row)

  • it is often useful to know what has been done. The @Insert function can return a long/Long value.

    • If a row has been inserted then the value will be the rowid (for an integer type primary key, the column is an alias of the special, normally hidden rowid column)
    • If a row has not been inserted (i.e. ignored) then the value will be -1 (note that it is in fact possible to specify -1 as a value, although )

As I understood from the documentation, @PrimaryKey(autoGenerate = true) should generate a new identifier for the uid.

This documentation is misleading/false. If you were to use autoGenerate false or as it is the default just @PrimaryKey then the uid will still be generated when a null uid is specified.

However, there is a subtle difference in that when autoGenerate is true, the SQLite AUTOINCREMENT keyword is included in the table definition.

The AUTOINCREMENT (autoGenerate = true) keyword adds a constraint(rule) that the generated value MUST be greater than any used value (even if the row with the highest value has been deleted). SQLite needs a way to keep a record of the highest ever used value, it does this by using a system table called sqlite_sequence which will be created by SQLite if required.

The sqlite_sequence table will have a row per table that has AUTOINCREMENT and that is where the value is stored. There are overheads accessing and maintaining this additional table. In fact the SQLite documentation says The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed.

I would suggest that the most efficient/correct option is to use:-

@Entity
public class User {
    @PrimaryKey
    public Long uid;

    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    public String lastName;
}
  • Long as Integer does not have the capacity to hold a 64bit signed value, Long does.

And to always ensure that the uid is null when inserting a new row.

Demonstration

Perhaps consider the following that demonstrates much of what has been stated. Noting that autoGenerate has been left to the default of false (which is suggested as being better for majority of cases)

As such the User class as above has been used throughout.

However, the UserDao interface has been expanded to cater for using the 3 discussed/mentioned conflict strategies (ABORT, IGNORE and REPLACE):-

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
            "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);

    @Insert
    Long insertAll(User user);
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    long insertAllWithReplace(User user);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    long insertAllWithIgnore(User user);

    @Delete
    void delete(User user);
}

To facilitate using the above an @Database annotated abstract class TheDatabase :-

@Database(entities = {User.class}, exportSchema = false, version = 1)
abstract class TheDatabase extends RoomDatabase {
    abstract UserDao getUserDao();

    private static volatile TheDatabase instance;
    static TheDatabase getInstance(Context context) {
        if (instance==null) {
            instance= Room.databaseBuilder(context,TheDatabase.class,"the_database.db")
                    .allowMainThreadQueries()
                    .build();
        }
        return instance;
    }
}

Lastly some activity code to demonstrate various inserts with functions to :-

  • write the data currently in the database to the log logAllUsers, and
  • to insert a row, specifying the conflict strategy and capturing any failure and also writing the result (returned rowid value) of the insert to the log.

MainActivity :-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    UserDao dao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /* Prepare to use the database and daos */
        db = TheDatabase.getInstance(this);
        dao = db.getUserDao();

        /* Prepare the 1 User that will be used throughout */
        User newUser = new User();
        newUser.firstName="Fred";
        newUser.lastName="Bloggs";

        /* Stage 1 insert with generated uid and ABORT conflict strategy */
        dao.insertAll(newUser);
        logAllUsers("_STAGE1");

        /* AS uid is null then the following will add 3 new rows */
        for (int i = 0; i < 3; i++ ) {
            dao.insertAll(newUser);
        }
        logAllUsers("_STAGE2");

        /* SHOULD NOT REALLY BE USED as the uid value will be 0*/
        newUser.uid=0;
        dao.insertAll(newUser);
        logAllUsers("_STAGE3");


        /* More in-depth debugging of inserts */
        /* duplicate use of 0 uid with ABORT */
        logAttemptedInsert(newUser,"_STAGE4", OnConflictStrategy.ABORT);
        /* duplicate use of 0 uid with REPLACE */
        logAttemptedInsert(newUser,"_STAGE5", OnConflictStrategy.REPLACE);
        /* duplicate use of 0 uid with IGNORE */
        logAttemptedInsert(newUser,"_STAGE6", OnConflictStrategy.IGNORE);
        /* Negative uid  ~~~ BEST TO NOT USE ~~~ */
        newUser.uid = -100;
        logAttemptedInsert(newUser,"_STAGE_7",OnConflictStrategy.IGNORE);
    }

    void logAllUsers(String tagSuffix) {
        for(User u: dao.getAll()) {
            Log.d("DBINFO" + tagSuffix,"User ID is " + u.uid + " FirstName is " + u.firstName + " LastName is " + u.lastName);
        }
    }

    void logAttemptedInsert(User user, String tagSuffix, Integer onConflictStrategy) {
        long insertedRowid= -888;
        try {
            switch(onConflictStrategy) {
                case OnConflictStrategy.IGNORE:
                    insertedRowid = dao.insertAllWithIgnore(user);
                    break;
                case OnConflictStrategy.REPLACE:
                    insertedRowid = dao.insertAllWithReplace(user);
                    break;
                default:
                    insertedRowid = dao.insertAll(user);
                    break;
            }
        } catch (SQLiteException e) {
            Log.d("DBINFO_" + tagSuffix,"SQLITE EXCEPTION TRAPPED. Exception Message=\n\t" + e.getMessage());
            insertedRowid = -999;
        } finally {
            Log.d("DBINFO" + tagSuffix,"INSERT RESULT for USER WITH ID as " + user.uid + " FIRSTNAME as " + user.firstName + " and LASTNAME as " + user.lastName + " WAS " + insertedRowid);
            logAllUsers(tagSuffix);
        }
    }
}
  • Note that the above is designed to just be run the once.

Results of running the Demo

When run the output to the log includes:-

2023-04-27 11:02:00.108 D/DBINFO_STAGE1: User ID is 1 FirstName is Fred LastName is Bloggs


2023-04-27 11:02:00.133 D/DBINFO_STAGE2: User ID is 1 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.134 D/DBINFO_STAGE2: User ID is 2 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.134 D/DBINFO_STAGE2: User ID is 3 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.134 D/DBINFO_STAGE2: User ID is 4 FirstName is Fred LastName is Bloggs


2023-04-27 11:02:00.141 D/DBINFO_STAGE3: User ID is 0 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.141 D/DBINFO_STAGE3: User ID is 1 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.141 D/DBINFO_STAGE3: User ID is 2 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.141 D/DBINFO_STAGE3: User ID is 3 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.141 D/DBINFO_STAGE3: User ID is 4 FirstName is Fred LastName is Bloggs


2023-04-27 11:02:00.144 D/DBINFO__STAGE4: SQLITE EXCEPTION TRAPPED. Exception Message=
        UNIQUE constraint failed: User.uid (code 1555 SQLITE_CONSTRAINT_PRIMARYKEY)
2023-04-27 11:02:00.144 D/DBINFO_STAGE4: INSERT RESULT for USER WITH ID as 0 FIRSTNAME as Fred and LASTNAME as Bloggs WAS -999
2023-04-27 11:02:00.145 D/DBINFO_STAGE4: User ID is 0 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.145 D/DBINFO_STAGE4: User ID is 1 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.145 D/DBINFO_STAGE4: User ID is 2 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.145 D/DBINFO_STAGE4: User ID is 3 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.146 D/DBINFO_STAGE4: User ID is 4 FirstName is Fred LastName is Bloggs


2023-04-27 11:02:00.148 D/DBINFO_STAGE5: INSERT RESULT for USER WITH ID as 0 FIRSTNAME as Fred and LASTNAME as Bloggs WAS 0
2023-04-27 11:02:00.150 D/DBINFO_STAGE5: User ID is 0 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.150 D/DBINFO_STAGE5: User ID is 1 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.151 D/DBINFO_STAGE5: User ID is 2 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.151 D/DBINFO_STAGE5: User ID is 3 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.151 D/DBINFO_STAGE5: User ID is 4 FirstName is Fred LastName is Bloggs


2023-04-27 11:02:00.153 D/DBINFO_STAGE6: INSERT RESULT for USER WITH ID as 0 FIRSTNAME as Fred and LASTNAME as Bloggs WAS -1
2023-04-27 11:02:00.155 D/DBINFO_STAGE6: User ID is 0 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.155 D/DBINFO_STAGE6: User ID is 1 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.156 D/DBINFO_STAGE6: User ID is 2 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.156 D/DBINFO_STAGE6: User ID is 3 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.156 D/DBINFO_STAGE6: User ID is 4 FirstName is Fred LastName is Bloggs


2023-04-27 11:02:00.157 D/DBINFO_STAGE_7: INSERT RESULT for USER WITH ID as -100 FIRSTNAME as Fred and LASTNAME as Bloggs WAS -100
2023-04-27 11:02:00.160 D/DBINFO_STAGE_7: User ID is -100 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.161 D/DBINFO_STAGE_7: User ID is 0 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.161 D/DBINFO_STAGE_7: User ID is 1 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.161 D/DBINFO_STAGE_7: User ID is 2 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.161 D/DBINFO_STAGE_7: User ID is 3 FirstName is Fred LastName is Bloggs
2023-04-27 11:02:00.161 D/DBINFO_STAGE_7: User ID is 4 FirstName is Fred LastName is Bloggs
  • Stage 1 has inserted the single row and the uid has been generated with a value of 1 as explained/expected.
  • Stage 2 has inserted the additional 3 rows with generated uid values (2-4 inclusive), as explained/expected.
  • Stage 3 has inserted another row with a specific uid value of 0, as explained/expected.
  • Stage 4 has NOT inserted another row with 0 as the specified uid value, the default onConflict strategy of ABORT resulted in an exception which was trapped and reported on. The 4 existing rows (0-4 inclusive) are the only rows that exist, as per the code -999 was returned to indicate the trapped exception.
  • Stage 5 has REPLACED row 0 returning 0 as the inserted rowid (note not -1 indicating nothing inserted). As such again all 5 rows exist.
  • Stage 6 has IGNORED the insert as a row with a uid of 0 already exists. Unlike Stage 4 there was no underlying exception so -1 has been returned, typically indicating nothing inserted. Still just the 5 rows exist.
  • Stage 6 has inserted a row with a negative value of -100 and is the value returned from the insert (if -1 were used then you would not know if the row were inserted or not according to the returned -1). 6 rows now exist. App Inspection

Using App Inspection then the end result appears to confirm the end result of 6 rows as per:-

enter image description here

using a New Query from App Inspection the absence of sqlite_master can be seen as per:-

enter image description here

  • if autoGenerate true were used then the resource costly sqlite_squence table could be seen as per:-

  • enter image description here

  • slqite_sequence itself would contain:-

  • enter image description here

    • i.e the highest ever used value of uid (4).

To show this hidden aliased rowid then:- enter image description here

Notes

  • With Room only tables with rowid are supported via annotations. However, SQLite does support WITHOUT ROWID tables, in which case AUTOINCREMENT cannot be used.
  • AUTOINCREMENT aka autoGenerate=true, due to the rule that a generated row MUST be greater than any existing row, will result in an SQLITE_FULL exception if the highest value allowable for the generated value (9223372036854775807), whilst without AUTOINCREMENT the algorithm will attempt to use an unused value (if a negative rowid has been used then this can be a negative value) thus it is unlikely that an SQLITE_FULL exception will result.
    • With current storage devices that number of rows is impossible. However, if 9223372036854775807 were specified as a value, even if there are only a few rows, then the exception, due to the rule, would eventuate if AUTOINCREMENT were used.

      • e.g. :- enter image description here
      • and then:- ![enter image description here
      • with the Event Log showing 11:48 am Database Inspector: An error occurred while running the statement: database or disk is full (code 13 SQLITE_FULL), even though there are only 7 rows
      • Without AUTOINCREMENT then using the same 2 queries results in:-
      • enter image description here
      • As can be seen an 8th row with New as the first_name and ASIF as the last name has been inserted with an available unused generated value rather than crashing.
MikeT
  • 51,415
  • 16
  • 49
  • 68
  • There is a button in my application, when you click on it, a new element is added to the ListView. To save all this, I need a database, so I need new values to be added to the row after the last one. My application is designed for more than one discovery. – kirialium Apr 27 '23 at 12:27
  • Strategies are certainly an interesting discovery for me, but not what I need – kirialium Apr 27 '23 at 12:28
  • @kirialium Unless you go messing with specific uid's then ensuring uid is null when inserting will allow up to 9223372036854775807 inserts where the generated uid is greater than the previous uid (with or without `autoGenerate=true` ). – MikeT Apr 27 '23 at 12:33
  • Thanks a lot, I figured it out and everything worked! – kirialium Apr 28 '23 at 09:29