I havea weird problem in my android application which is related to database relations on foreign keys. The following codes describe my simple database's structure,
i have used SQLiteOpenHelper as a super class for handle database operation
private static final String CATEGORIES_TABLE = "CREATE TABLE __CATEGORIES_TBL(_id INTEGER PRIMARY KEY AUTOINCREMENT, _name TEXT NOT NULL, _desc TEXT NULL,"
+ "_create_date INTEGER NOT NULL, _update_date INTEGER NULL"
+ ", _parent_id INTEGER NULL, FOREIGN KEY(_parent_id) REFERENCES __CATEGORIES_TBL(_id) ON DELETE RESTRICT);";
private static final String CARDS_TABLE = "CREATE TABLE __CARDS_TBL(_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "_value TEXT NOT NULL, _play_count INTEGER NULL, "
+ "_category_id INTEGER NOT NULL, FOREIGN KEY(_category_id) REFERENCES __CATEGORIES_TBL(_id) ON DELETE RESTRICT);";
#Override
public void onCreate(SQLiteDatabase db) {
try {
db.execSQL("PRAGMA foreign_keys = ON;");
db.execSQL(CATEGORIES_TABLE);
db.execSQL(CARDS_TABLE);
Logger.i("DB-INIT-DONE!");
} catch (Exception ex) {
Logger.e("Database on create error", ex);
}
}
as you see everything seems to be O.K and it is; I can insert, edit , select row(s) to/from both tables but unfortunately i can delete rows which they have child rows.
as i expected. because i set the FK (foreign-key) relation between the tow tables with ON DELETE RESTRICT mode therefore i expect to get an exception when i try to delete a row from parent table (__CATEGORIES_TBL) , actually the parent record is deleting and no exception happens,
by theory sqlite must prevent deleting any row in __CATEGORIES_TBL when it has one or more child row(s) in __CARDS_TBL or any child row(s) in __CATEGORIES_TBL but in my application i can delete rows when it has a parent-child relationship rows,
consider the following code (this is the deleting code)
private SQLiteDatabase db;
public long delete(long objId) {
try {
// TABLE_NAME can be __CATEGORIES_TBL or __CARDS_TBL based on program flow
return db.delete(TABLE_NAME, COLUMN_ROWID + "=" + objId, null);
} catch (Exception e) {
Logger.d("Unable to delete category <" + objId + ">.", e);
return -123456;
}
}
every call to db.delete returns 1 (means 1 row is deleted by this command) this code is executing under android 2.3 ;
Thanks in advance.
I get the behavior I'd expect from SQLite 3.7.9.
sqlite> CREATE TABLE __CATEGORIES_TBL (
...> _id INTEGER PRIMARY KEY AUTOINCREMENT,
...> _name TEXT NOT NULL,
...> _desc TEXT NULL,
...> _create_date INTEGER NOT NULL,
...> _update_date INTEGER NULL,
...> _parent_id INTEGER NULL,
...> FOREIGN KEY(_parent_id)
...> REFERENCES __CATEGORIES_TBL(_id) ON DELETE RESTRICT
...> );
sqlite>
sqlite> CREATE TABLE __CARDS_TBL(
...> _id INTEGER PRIMARY KEY AUTOINCREMENT,
...> _value TEXT NOT NULL,
...> _play_count INTEGER NULL,
...> _category_id INTEGER NOT NULL,
...> FOREIGN KEY(_category_id)
...> REFERENCES __CATEGORIES_TBL(_id) ON DELETE RESTRICT
...> );
sqlite>
sqlite> insert into __categories_tbl values
...> (1, 'name', 'desc',current_date,current_date,1);
sqlite> insert into __cards_tbl values (1, 'value',3, 1);
sqlite> pragma foreign_keys=on;
sqlite> select * from __categories_tbl;
1|name|desc|2012-08-25|2012-08-25|1
sqlite> delete from __categories_tbl;
Error: foreign key constraint failed
If I were you, I'd try to visually inspect the SQLite database after each step to see whether what you expect to happen is actually happening. IIRC, not every failure will raise an exception.
Foreign key support was introduced in version 3.6.19. There are compile-time settings that allow SQLite to parse foreign key constraints, but don't allow it to actually enforce them. (SQLITE_OMIT_TRIGGER defined, SQLITE_OMIT_FOREIGN_KEY not defined.) You should be able to tell whether your build of SQLite can enforce foreign keys by trying to create a trigger.
If creating a trigger fails with a parse error, then SQLite will parse foreign key statements, but won't enforce them. You'll need to recompile your build of 3.6.19, or upgrade to a newer version. (And check those settings before you compile.)
A column declared NOT NULL can appear to be empty if you've inserted an empty string. By default, NULL and empty strings look identical on output. You can tell whether a column contains nulls by selecting them.
select * from table_name where column_name is null;
about this problem i have put a db.execSQL("pragma foreign_keys=on;"); before delete statement in my app code then the problem solved, but i think this is unnecessary code maybe my phone's installed sqlite configs are wrong and needs to be reconfigured , is it possible to reconfigure the sqlite on phone ?
the test device is a HTC wildfire s (running sqlite version is 3.7.2)
Related
I have an application, where I am detecting the type of a particular column at run-time, on page load. Please refer the below code:
public String fncCheckColumnType(String strColumnName){
db = this.getWritableDatabase();
String strColumnType = "";
Cursor typeCursor = db.rawQuery("SELECT typeof (" + strColumnName +") from tblUsers, null);
typeCursor.moveToFirst();
strColumnType = typeCursor.getString(0);
return strColumnType;
}
The above method simply detects the type of column with column Name 'strColumnName'. I am getting the type of column in this case.
Now, I want to change the column type to TEXT if I am receiving INTEGER as the column type. For this, I tried the below code:
public String fncChangeColumnType(String strColumnName){
db = this.getWritableDatabase();
String newType = "";
Cursor changeCursor = db.rawQuery("ALTER TABLE tblUsers MODIFY COLUMN " + strColumnName + " TEXT", null);
if (changeCursor != null && changeCursor.moveToFirst()){
newType = changeCursor.getString(0);
}
return newType;
}
But while executing the 'fncChangeColumnType' method, I am getting this error, android.database.sqlite.SQLiteException: near "MODIFY": syntax error (code 1): , while compiling: ALTER TABLE tblUsers MODIFY COLUMN UserID TEXT
NOTE: I also replaced 'MODIFY' with 'ALTER', but still getting the same error.
Please check if this is the right method to change the type dynamically.
Please respond back if someone has a solution to this.
Thanks in advance.
In brief, the solution could be :-
Do nothing (i.e. take advantage of SQLite's flexibility)
you could utilise CAST e.g. CAST(mycolumn AS TEXT) (as used below)
Create a new table to replace the old table.
Explanations.
With SQLite there are limitations on what can be altered. In short you cannot change a column. Alter only allows you to either rename a table or to add a column. As per :-
SQL As Understood By SQLite - ALTER TABLE
However, with the exception of a column that is an alias of the rowid column
one defined with ?? INTEGER PRIMARY KEY or ?? INTEGER PRIMARY KEY AUTOINCREMENT or ?? INTEGER ... PRIMARY KEY(??) (where ?? represents a valid column name)
you can store any type of value in any type of column. e.g. consider the following (which stores an INTEGER, a REAL, a TEXT, a date that ends up being TEXT and a BLOB) :-
CREATE TABLE IF NOT EXISTS example1_table (col1 BLOB);
INSERT INTO example1_table VALUES (1),(5.678),('fred'),(date('now')),(x'ffeeddccbbaa998877665544332211');
SELECT *, typeof(col1) FROM example1_table;
The result is :-
As such is there a need to change the column type at all?
If the above is insufficient then your only option is to create a new table with the new column definitions, populate it if required from the original table, and to then replace the original table with the new table ( a) drop original and b)rename new or a) rename original, b) rename new and c) drop original)
e.g. :-
DROP TABLE IF EXISTS original;
CREATE TABLE IF NOT EXISTS original (mycolumn INTEGER);
INSERT INTO original VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(0);
-- The original table now exists and is populated
CREATE TABLE IF NOT EXISTS newtable (mycolumn TEXT);
INSERT INTO newtable SELECT CAST(mycolumn AS TEXT) FROM original;
ALTER TABLE original RENAME TO old_original;
ALTER TABLE newtable RENAME TO original;
DROP TABLE IF EXISTS old_original;
SELECT *,typeof(mycolumn) FROM original;
The result being :-
i think the sql query statement is wrong ,try
ALTER TABLE tblUsers MODIFY COLUMN id TYPE integer USING (id::integer);
instead of id use column name....
hope this helps....
EDIT:
"ALTER TABLE tblUsers MODIFY COLUMN "+strColumnName+" TYPE integer USING ("+strColumnName+"::integer);"
We have a requirement where some fields in a table need to have the same value as their ID. Unfortunately, we currently have to insert a new record and then, if needed, run another update to set the duplicate field (ID_2) value to equal the ID.
Here is the Android Sqlite code:
mDb.beginTransaction();
// ... setting various fields here ...
ContentValues contentValues = new ContentValues();
contentValues.put(NAME, obj.getName());
// now insert the record
long objId = mDb.insert(TABLE_NAME, null, contentValues);
obj.setId(objId);
// id2 needs to be the same as id:
obj.setId2(objId);
// but we need to persist it so we update it in a SECOND call
StringBuilder query = new StringBuilder();
query.append("update " + TABLE_NAME);
query.append(" set " + ID_2 + "=" + objId);
query.append(" where " + ID + "=" + objId);
mDb.execSQL(query.toString());
mDb.setTransactionSuccessful();
As you can see, we are making a second call to set ID_2 to the same value of ID. Is there any way to set it at INSERT time and avoid the second call to the DB?
Update:
The ID is defined as follows:
ID + " INTEGER PRIMARY KEY NOT NULL ," +
The algorithm used for autoincrementing columns is documented, so you could implement it manually in your code, and then use the new value for the INSERT.
This is quite a ugly hack, but it may be possible :
with id_table as (
select coalesce(max(seq), 0) + 1 as id_column
from sqlite_sequence
where name = 'MY_TABLE'
)
insert into MY_TABLE(ID_1, ID_2, SOME, OTHER, COLUMNS)
select id_column, id_column, 'SOME', 'OTHER', 'VALUES'
from id_table
It only works if the table ID is an AUTOINCREMENT, and is therefore managed via the documented sqlite_sequence table.
I also have no idea what happen in case of concurrent executions.
You could use an AFTER INSERT TRIGGER e.g.
Create your table (at least for this example) so that ID_2 is defined as INTEGER DEFAULT -1 (0 or any negative value would be ok)
e.g. CREATE TABLE IF NOT EXISTS triggertest (_id INTEGER PRIMARY KEY ,name TEXT ,id_2 INTEGER DEFAULT -1);
Then you could use something like (perhaps when straight after the table is created, perhaps create it just before it's to be used and drop it after done with it ) :-
CREATE TRIGGER triggertesting001
AFTER INSERT ON triggertest
BEGIN
UPDATE triggertest SET id_2 = `_id`
WHERE id_2 = -1;
END;
Drop using DROP TRIGGER IF EXISTS triggertesting001;
Example usage (testing):-
INSERT INTO triggertest (name) VALUES('Fred');
INSERT INTO triggertest (name) VALUES('Bert');
INSERT INTO triggertest (name) VALUES('Harry');
Result 1 :-
Result 2 (trigger dropped inserts run again ):-
Result 3 (created trigger) same as above.
Result 4 (ran inserts for 3rd time) catch up i.e. 6 rows updated id_2 with _id.
I'd strongly suggest reading SQL As Understood By SQLite - CREATE TRIGGER
Alternative solution
An alternative approach could be to simply use :-
Before starting transaction, retrieve mynextid from table described below
ContentValues contentValues = new ContentValues();
contentValues.put(ID,mynextid);
contentvalues.put(ID_2,mynextid++);
contentValues.put(NAME, obj.getName());
Then at end of the transactions update/store the value of mynextid in a simple single column, single row table.
i.e. you are managing the id's (not too dissimilar to how SQLite manages id's when 'AUTOINCREMENT' is specified)
Don't immediately flag me for a duplicate question. My issue is different because I have a correctly formatted SQL query.
public static final String TABLE_NAME = "log";
public static final String COLUMN_ID = "_id";
public static final String LOG_TEXT = "logtext";
private static final String TABLE_CREATE = "CREATE TABLE " + TABLE_NAME + " (" +
COLUMN_ID + " integer primary key autoincrement, " +
LOG_TEXT + " TEXT not null);";
#Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TABLE_CREATE);
}
and I query here
String[] columns = {LOG_TEXT,COLUMN_ID};
Cursor cursor = helper.getReadableDatabase().query(TABLE_NAME, columns, null, null, null, null, COLUMN_ID + " desc");
and I catch this the exception generated containing the sql query.
catch(Exception e){
Log.D("sql Exception",e.getMessage());}
and it returns
no such column: _id: , while compiling: SELECT logtext, _id FROM log ORDER BY _id desc
I'm familar with Oracle SQL and relational databases in general. Is it my ORDER BY clause? I was certain you can ALWAYS use order by. It doesn't have the same behavior as GROUP BY.
Any ideas on why the exception?
Incase anyone wants to see i'm updating with my ArrayAdaptor statements. I'm using the cursor in a listview
String[] data = query();
adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, android.R.id.text1, data);
listView.setAdapter(adapter);}
Rewrite
Whenever you change the schema in TABLE_CREATE you must inform you app of these changes, they will not happen automatically when you change TABLE_CREATE. The easiest way to do this is to increment your database_version in your extended SQLiteOpenHelper class. You discovered you can also uninstall / reinstall the app, for the same results. If you are savvy with SQL you could ALTER the table. But whatever the method you must make sure that you app makes the schema changes before trying to access the new columns...
Also for SQLite:
_id integer primary key
is synonymous with:
_id integer primary key autoincrement not null
And queries use descending as the default order, so ORDER BY _id is the same as ORDER BY _id DESC.
Had the same problem, meaning it should have worked but didn't (had some typos in the create command that I fixed but that still didn't help). A colleague then told me to try clearing the data (just at AppInfo and then "Clear Data") which solved my problem, apparently the old database (that didn't work) was still there and had to be cleared out first.
I just put this answer here in case anybody else like me (android beginner) stumbles across this problem, because I went through dozens of stackoverflow threads with this problem but not one offered this possibility/solution and it bothered me for quite some time.
Did you add the definition of the _id column to your create statement later on, i.e. after the code had already been run once? Databases are persisted files, so if you modify the table structure in your code you need to make sure you clear your application's data so the database file can ge re-created with the correct table/column data.
I'm trying to build a database using 21 tables each bind with constraints. I have implemented the database in PHPmyAdmin and then download the structure script to import in my Android SQLite database.
But I would like to know what are the good practices in building relational databases with many tables in Android, and how to create them.
currently I have a class implementing SQLiteOpenHelper:
public class SqlSig extends SQLiteOpenHelper {
#Override
public void onCreate(SQLiteDatabase db) {
for(int i = 0; i != ConfigBDD.requetes.length; i++){
db.execSQL(ConfigBDD.requetes[i]);
}
}
}
ConfigBDD.requetes[i] fetch a string array containing tables creation requests and constraints requests. This code doesn't work at all, and logCat show an error with the PRIMARY KEY instruction:
05-29 15:13:51.992: E/Database(8187): Failure 1 (near "KEY": syntax error) on 0x15c4b8
when preparing 'CREATE TABLE IF NOT EXISTS `arret` ( `id` int(11) NOT NULL , `id_externe`
varchar(10) DEFAULT NULL, `id_pid` int(11) NOT NULL, `nom` varchar(50) NOT NULL,
`description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `id_pid` (`id_pid`) )
DEFAULT CHARSET=utf8 =1 ;'.
I am pretty sure there are other ways to create this database but did not found any explanations about multi tables Android SQLite database creation...
Your:
CREATE TABLE IF NOT EXISTS `arret` (
`id` int(11) NOT NULL ,
`id_externe` varchar(10) DEFAULT NULL,
`id_pid` int(11) NOT NULL,
`nom` varchar(50) NOT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_pid` (`id_pid`))
DEFAULT CHARSET=utf8 =1 ;
should probably look something like this if you want it to work in SQLite.
CREATE TABLE IF NOT EXISTS arret (
id INTEGER PRIMARY KEY NOT NULL ,
id_externe TEXT DEFAULT NULL,
id_pid INTEGER NOT NULL,
nom TEXT NOT NULL,
description TEXT
);
followed by a CREATE INDEX for your id_pid which is what KEY implies in MySQL.
I'm creating a dataBase to insert a *unique patient *(no more than one), so I just created a database that doesn't autoincrement its id like this:
CREATE TABLE IF NOT EXISTS PACIENTE(idPaciente INTEGER PRIMARY KEY, nombre VARCHAR(100) NOT NULL, apellidos VARCHAR(100), email VARCHAR(100), genero CHAR, edad INTEGER NOT NULL, peso INTEGER, kMedico INTEGER, pkHistorial INTEGER, pkConfProg INTEGER, altura INTEGER, FOREIGN KEY (pkMedico) REFERENCES MEDICO(idMedico), FOREIGN KEY (pkHistorial) REFERENCES HISTORIAL(idHistorial), FOREIGn KEY (pkConfProg) REFERENCES CONFPROGRAMA(idConf));
As you can see, the way to add a patient here is tell the database the idPaciente explicitly.
So I used this code to insert a patient:
public long addPaciente(BDPaciente pac)
{
ContentValues cv = new ContentValues();
cv.put("idPaciente", 1);
cv.put("nombre", pac.getNombre());
cv.put("edad", 26);
try
{
db.insert("PACIENTE", null, cv);
return -1;
}
catch (SQLiteConstraintException e)
{
return -100;
}
}
As you can see, what I'm trying to do is insert a patient, and then, if it is inserted before, catch the Exception and throw it to my parent Window. The thing is that, the exception is thrown, but not catched. And the program says:
Error inserting nombre=blabla edad=25 idPaciente=1
android.database.SQLiteConstraintException: error code 19: constraint failed
I know that it's something about the duplication on the primary key, but I wanna do so!
Flo, thanky you for your answer, but yes, I created the table, but what I didn't post, is that I have a method for erasing all databases and then creating them again whenever I press a button like this:
db.execSQL(DROP_TABLE_HISTORIAL);
db.execSQL(DROP_TABLE_MEDICO);
db.execSQL(DROP_TABLE_CONF);
db.execSQL(DROP_TABLE_PACIENTE);
So yes, I'm sure. But what Sarmand answered works for me, so thank you for your help :-)
Sorry, but I can't vote... Don't have enough points >_<
If you want to duplicate primary key then dont declare it as primary key. Do it like this
CREATE TABLE IF NOT EXISTS PACIENTE(idPaciente INTEGER , ....);