Reputation: 31
While exploring the capabilities of the Room library and the RoomDatabase class, I decided to test the query() method. Since the description in the official documentation does not contain any restrictions on its use, I decided to test the two simplest methods: SELECT and INSERT.
I'm testing queries with a simple database. My SELECT looked like this:
Cursor cursor = db.query("select * from artist”, null);
The artist table is described as follows:
@Entity
public class Artist {
@PrimaryKey
public int Artist_id;
public String Name;
}
This method worked well and returned me a Cursor with which I can view the table data.
But when I decided to try using the INSERT method as follows:
Cursor cursor = db.query("insert into artist (name) values('Candido');”, null);
then it turned out that the insertion of a new row into the table didn't occur. I decided to find out by debugging why the query() method executes select operation and performs insert, but could not find the place in the library code where the check occurs. Then I decided to perform SELECT and INSERT, but with the rawQuery() method from SQLiteDatabase class and result was the same.
During step-by-step debugging in the library class SQLiteDirectCursorDriver, when debugging, the program executes the following method:
public Cursor query(CursorFactory factory, String[] selectionArgs) {
final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
final Cursor cursor;
try {
query.bindAllArgsAsStrings(selectionArgs);
if (factory == null) {
cursor = new SQLiteCursor(this, mEditTable, query);
} else {
cursor = factory.newCursor(mDatabase, this, mEditTable, query);
}
} catch (RuntimeException ex) {
query.close();
throw ex;
}
mQuery = query;
return cursor;
}
This is the method that is called during the creation of the Cursor object, which is returned by the query() method that I am testing. When creating a SQLiteQuery, its constructor calls the constructor of the parent class SQLiteProgram, which contains the following lines:
db.getThreadSession().prepare(mSql,
db.getThreadDefaultConnectionFlags(assumeReadOnly),
cancellationSignalForPrepare, info);
I assumed that a query to the database occurs in this place, but this is probably not the case, since the description of the prepare() method of the SQLiteSession class says that method "Prepares a statement for execution but does not bind its parameters or execute it". If we return to the query() method, the code of which I described above, then you should pay attention to the following line
cursor = new SQLiteCursor(this, mEditTable, query);
The constructor description says: "Execute a query and provide access to its result set through a Cursor interface."
But if I open the code of the class constructor and the code of the parent class constructor, then I did not find the code there that executes my sql in the database.
I can’t understand where the SELECT/INSERT query to the database is executed, in what place, in what class and method does this happen? Where is the data that the cursor then works with? Please help me understand the internal structure of this process.
The code was tested on the Android 13 API 33 Emulator arm64-v8a
Upvotes: 0
Views: 86
Reputation: 57073
I can’t understand where the SELECT/INSERT query to the database is executed
I don't believe that you need to (in short do what needs to be done in the most convenient way and perhaps heeding there is no way to make changes and return a value in a single execution of an SQL statement).
Where is the data that the cursor then works with?
and
then it turned out that the insertion of a new row into the table didn't occur.
There is no data. An INSERT does not return any data.
You may then say but the @Insert
does return the artist_id, this is because the underlying code of the insert (which is the SQLite API which Room invokes) follows the insert with another API call that returns the rowid (which artist_id is an alias of).
the official documentation does not contain any restrictions on its use Well it implies restrictions when it says Convenience method to query the database with arguments.. The implication is that it is not intended for operations that change the database. However, see the demo below.
To insert without resorting to and using your own SQL should be done using the SupportSQLiteDatabase
's execSQL
method.
However, you may well wish to use the convenience @Insert
in an @Dao
annotated interface or abstract class. The artist_id will then be returned.
@PrimaryKey
field is and integer type (e.g. int, long. Integer, Long) then it will be a special column in that it will be an alias of the rowid and if, when inserting, and no value is provided then the value will be generated.autoGenerate
parameter of the @PrimaryKey
annotation is 0 then the it does not pass the 0 value and thus the value for the column is generated.
autogenerate
is false (the default if not coded), then if the value is null, it does not pass the value. However, the value of 0 is passed and either 0 will be used or a UNIQUE conflict will be issued (i.e. the row will not be inserted and the result is dependent upon the OnConflictStrategy
(demo uses IGNORE)).autoGenerate=true
is, in most cases, a waste/inefficient (see https://www.sqlite.org/autoinc.html as to why).As such perhaps consider the following demo, which tries to explain:-
First some @Entity
annotated classes basically the same but with nuances:-
@Entity
public class ArtistOriginal {
@PrimaryKey
public int Artist_id;
public String Name;
}
autogenerate
means that artist_id will be 0 if left to default and after 1 insert will have a UNIQUE conflict)and,
@Entity
public class ArtistOther1 {
@PrimaryKey(autoGenerate = true)
public int Artist_id;
public String Name;
}
autoGenerate=true
addedand
@Entity
public class ArtistOther2 {
@PrimaryKey
public Integer Artist_id;
public String Name;
}
autoGenerate
defaults to falseSome methods in an @Dao
annotated abstract class (could be an interface instead of abstract class but the latter allows methods with bodies which can be useful):-
@Dao
abstract class AllDAOs {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(ArtistOriginal artistOriginal);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(ArtistOther1 artistOther1);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract long insert(ArtistOther2 artistOther2);
}
Finally some activity code:-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDAOs dao;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = TheDatabase.getInstance(this);
dao = db.getAllDAOs();
SupportSQLiteDatabase sdb = db.getOpenHelper().getWritableDatabase();
/* Ignored as no result will be returned */
Cursor cursor = sdb.query("INSERT INTO ArtistOriginal (name) VALUES ('Candido_via_query');");
DatabaseUtils.dumpCursor(cursor);
cursor.close();
/* artist_id will be generated (1??? for the first run) */
sdb.execSQL("INSERT INTO ArtistOriginal (name) VALUES('Candido_via_execsql');");
/* In this case the artist_id is 0 as int cannot be null */
ArtistOriginal ao = new ArtistOriginal();
ao.Name = "Candidi_via_@Insert";
dao.insert(ao);
/* In this case the artist_id is 0 as int cannot be null */
ArtistOther1 ao1 = new ArtistOther1();
ao1.Name = "Candido_via_@Insert";
dao.insert(ao1);
/* In this case the artist_id member is null as it is an Integer Object */
ArtistOther2 ao2 = new ArtistOther2();
ao2.Name = "Candido_via_@Insert";
dao.insert(ao2);
Cursor csr = db.query("" +
"SELECT 'ArtistOriginal',* FROM ArtistOriginal " +
"UNION ALL SELECT 'ArtistOther1',* FROM ArtistOther1 " +
"UNION ALL SELECT 'ArtistOther2',* FROM ArtistOther2",
null
);
DatabaseUtils.dumpCursor(csr);
csr.close();
}
}
Demo Results
The log (dumped cursors); first Cursor:-
2024-02-01 10:18:13.013I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@8c94956
2024-02-01 10:18:13.013I/System.out: <<<<<
Then, the second Cursor (split into the 3 tables UNION'ed):-
2024-02-01 10:18:13.042I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@3574073
2024-02-01 10:18:13.042I/System.out: 0 {
2024-02-01 10:18:13.042I/System.out: 'ArtistOriginal'=ArtistOriginal
2024-02-01 10:18:13.043I/System.out: Artist_id=0
2024-02-01 10:18:13.043I/System.out: Name=Candidi_via_@Insert
2024-02-01 10:18:13.043I/System.out: }
2024-02-01 10:18:13.043I/System.out: 1 {
2024-02-01 10:18:13.043I/System.out: 'ArtistOriginal'=ArtistOriginal
2024-02-01 10:18:13.043I/System.out: Artist_id=1
2024-02-01 10:18:13.043I/System.out: Name=Candido_via_query
2024-02-01 10:18:13.043I/System.out: }
2024-02-01 10:18:13.043I/System.out: 2 {
2024-02-01 10:18:13.043I/System.out: 'ArtistOriginal'=ArtistOriginal
2024-02-01 10:18:13.043I/System.out: Artist_id=2
2024-02-01 10:18:13.043I/System.out: Name=Candido_via_execsql
2024-02-01 10:18:13.043I/System.out: }
then, as expected, the single row in the Other1 and Other2 table with appropriately generated artist_id values
2024-02-01 10:18:13.043I/System.out: 3 {
2024-02-01 10:18:13.043I/System.out: 'ArtistOriginal'=ArtistOther1
2024-02-01 10:18:13.043I/System.out: Artist_id=1
2024-02-01 10:18:13.043I/System.out: Name=Candido_via_@Insert
2024-02-01 10:18:13.043I/System.out: }
2024-02-01 10:18:13.044I/System.out: 4 {
2024-02-01 10:18:13.044I/System.out: 'ArtistOriginal'=ArtistOther2
2024-02-01 10:18:13.044I/System.out: Artist_id=1
2024-02-01 10:18:13.044I/System.out: Name=Candido_via_@Insert
2024-02-01 10:18:13.044I/System.out: }
2024-02-01 10:18:13.044I/System.out: <<<<<
Using App Inspection, then:-
and
More
If the dao.insert(...) are changed to retrieve the returned long as per:-
long aiid = dao.insert(ao);
....
long ao1id = dao.insert(ao1);
....
long ao2id = dao.insert(ao2);
And the App is rerun (not uninstalled and rerun) in debug mode with a breakpoint at where the 2nd Cursor/Query is executed then (actually 2nd rerun):-
autoGenerate
being false.@Inserts
return a positive value (3).The convenience methods not only execute the core insert/delete or update but additional use the API to gather the respective data as per:-
PRAGMA count_changes; PRAGMA count_changes = boolean;
Query or change the count-changes flag. Normally, when the count-changes flag is not set, INSERT, UPDATE and DELETE statements return no data. When count-changes is set, each of these commands returns a single row of data consisting of one integer value - the number of rows inserted, modified or deleted by the command. The returned change count does not include any insertions, modifications or deletions performed by triggers, any changes made automatically by foreign key actions, or updates caused by an upsert.
Another way to get the row change counts is to use the sqlite3_changes() or sqlite3_total_changes() interfaces. There is a subtle different, though. When an INSERT, UPDATE, or DELETE is run against a view using an INSTEAD OF trigger, the count_changes pragma reports the number of rows in the view that fired the trigger, whereas sqlite3_changes() and sqlite3_total_changes() do not.
Additional re comments
I read what you write and was very surprised that the insertion of data with sdb.query(...) and sdb.execSQL(...) only happened after the dao.insert(ao) method was called;
The inserts are done when when the SQL is executed but there is nothing reports this. If the following method is added:-
void logDataInDatabase(SupportSQLiteDatabase sdb,String tag_suffix) {
Log.d("CSRDUMP_" + tag_suffix,"Dumping Cursor");
Cursor csr = db.query("" +
"SELECT 'ArtistOriginal' AS t,* FROM ArtistOriginal " +
"UNION ALL SELECT 'ArtistOther1',* FROM ArtistOther1 " +
"UNION ALL SELECT 'ArtistOther2',* FROM ArtistOther2",
null);
DatabaseUtils.dumpCursor(csr);
csr.close();
}
And this is called after each insert:-
Cursor cursor = sdb.query("INSERT INTO ArtistOriginal (name) VALUES ('Candido_via_query');");
DatabaseUtils.dumpCursor(cursor);
cursor.close();
logDataInDatabase(sdb,"QRYINS");
/* artist_id will be generated (1??? for the first run) */
sdb.execSQL("INSERT INTO ArtistOriginal (name) VALUES('Candido_via_execsql');");
logDataInDatabase(sdb,"EXEINS");
/* In this case the artist_id is 0 as int cannot be null */
ArtistOriginal ao = new ArtistOriginal();
ao.Name = "Candidi_via_@Insert";
long aiid = dao.insert(ao);
logDataInDatabase(sdb,"DAOINS");
Then a first run's log will include:-
2024-02-02 05:28:44.284 D/CSRDUMP_QRYINS: Dumping Cursor
2024-02-02 05:28:44.287 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@8f4e6c4
2024-02-02 05:28:44.287 I/System.out: 0 {
2024-02-02 05:28:44.287 I/System.out: t=ArtistOriginal
2024-02-02 05:28:44.287 I/System.out: Artist_id=1
2024-02-02 05:28:44.287 I/System.out: Name=Candido_via_query
2024-02-02 05:28:44.287 I/System.out: }
2024-02-02 05:28:44.287 I/System.out: <<<<<
2024-02-02 05:28:44.288 D/CSRDUMP_EXEINS: Dumping Cursor
2024-02-02 05:28:44.289 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@e2e29ad
2024-02-02 05:28:44.290 I/System.out: 0 {
2024-02-02 05:28:44.290 I/System.out: t=ArtistOriginal
2024-02-02 05:28:44.290 I/System.out: Artist_id=1
2024-02-02 05:28:44.290 I/System.out: Name=Candido_via_query
2024-02-02 05:28:44.290 I/System.out: }
2024-02-02 05:28:44.290 I/System.out: 1 {
2024-02-02 05:28:44.290 I/System.out: t=ArtistOriginal
2024-02-02 05:28:44.290 I/System.out: Artist_id=2
2024-02-02 05:28:44.291 I/System.out: Name=Candido_via_execsql
2024-02-02 05:28:44.291 I/System.out: }
2024-02-02 05:28:44.291 I/System.out: <<<<<
2024-02-02 05:28:44.294 D/CSRDUMP_DAOINS: Dumping Cursor
2024-02-02 05:28:44.294 I/System.out: >>>>> Dumping cursor android.database.sqlite.SQLiteCursor@3574073
2024-02-02 05:28:44.295 I/System.out: 0 {
2024-02-02 05:28:44.296 I/System.out: t=ArtistOriginal
2024-02-02 05:28:44.296 I/System.out: Artist_id=0
2024-02-02 05:28:44.296 I/System.out: Name=Candidi_via_@Insert
2024-02-02 05:28:44.296 I/System.out: }
2024-02-02 05:28:44.296 I/System.out: 1 {
2024-02-02 05:28:44.296 I/System.out: t=ArtistOriginal
2024-02-02 05:28:44.296 I/System.out: Artist_id=1
2024-02-02 05:28:44.296 I/System.out: Name=Candido_via_query
2024-02-02 05:28:44.296 I/System.out: }
2024-02-02 05:28:44.296 I/System.out: 2 {
2024-02-02 05:28:44.296 I/System.out: t=ArtistOriginal
2024-02-02 05:28:44.296 I/System.out: Artist_id=2
2024-02-02 05:28:44.296 I/System.out: Name=Candido_via_execsql
2024-02-02 05:28:44.296 I/System.out: }
2024-02-02 05:28:44.296 I/System.out: <<<<<
What surprised me even more is that, as you write, int is a primitive type and conflict will occur when inserting another more into the database...
Only if autoGenerate
is false (or omitted and thus defaults to false) in which case the underlying generated insert SQL used is:-
"INSERT OR IGNORE INTO `ArtistOriginal` (`Artist_id`,`Name`) VALUES (?,?)"
If autoGenerate
is true then Room generates the insert SQL as:-
"INSERT OR IGNORE INTO `ArtistOther1` (`Artist_id`,`Name`) VALUES (nullif(?, 0),?)"
i.e.an artist_id that is 0 will be taken as being null and thus the artist_id will be generated
@Database
annotated class(es) and the @Dao
annotated interfaces/abstract class(es). The INSERT SQL above was copied from those classes.The artist_id, being the primary key must be unique, so if it is a primitive and left to default to 0 only one such insert will work, subsequent inserts will result in a UNIQUE conflict.
autoGenerate = true
, which results in the AUTOINCREMENT
keyword/clause being used, which is rarely needed and is inefficient.simply do not use methods for insertion that are not intended for this
Unless there is good reason not to and then if there is use an appropriate way. Typically the appropriate Room way, would be to use an @Query
where the SQL would be the "special" INSERT (more likely for UPDATE or DELETE as @Delete
and @Update
rely upon the primary key to ascertain the row to be actioned upon).
Perhaps the "rule" should be only use a SupportSQliteDatabase if necessary (which is pretty rare).
Upvotes: 2