SQLite — удаление строки, на которую ссылается внешний ключ

При попытке удалить строку, на которую ссылаются как на внешний ключ (в экспериментальных целях), я получаю исключение во время выполнения. Я понимаю, что пытаюсь нарушить ограничение внешнего ключа, меня просто смущает сообщение logcat, потому что оно относится к совершенно другой части моего кода.

Это мой метод SQLiteOpenHelper onCreate:

@Override
public void onCreate(SQLiteDatabase db) {
    this.db = db;

    final String SQL_CREATE_CATEGORIES_TABLE = "CREATE TABLE " +
            CategoriesTable.TABLE_NAME + "( " +
            CategoriesTable._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
            CategoriesTable.COLUMN_NAME + " TEXT " +
            ")";

    final String SQL_CREATE_QUESTIONS_TABLE = "CREATE TABLE " +
            QuestionsTable.TABLE_NAME + " ( " +
            QuestionsTable._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
            QuestionsTable.COLUMN_QUESTION + " TEXT, " +
            QuestionsTable.COLUMN_OPTION1 + " TEXT, " +
            QuestionsTable.COLUMN_OPTION2 + " TEXT, " +
            QuestionsTable.COLUMN_OPTION3 + " TEXT, " +
            QuestionsTable.COLUMN_ANSWER_NR + " INTEGER, " +
            QuestionsTable.COLUMN_DIFFICULTY + " TEXT, " +
            QuestionsTable.COLUMN_CATEGORY + " INTEGER, " +
            "FOREIGN KEY(" + QuestionsTable.COLUMN_CATEGORY + ") REFERENCES " +
            CategoriesTable.TABLE_NAME + "(" + QuestionsTable._ID + ")" + 
            ")";

    db.execSQL(SQL_CREATE_CATEGORIES_TABLE);
    db.execSQL(SQL_CREATE_QUESTIONS_TABLE);
    fillCategoriesTable();
    fillQuestionsTable();
}

Здесь я заполняю таблицу начальными вопросами, а затем пытаюсь удалить строку из таблицы категорий, чтобы намеренно нарушить ограничение внешнего ключа (для проверки):

private void fillQuestionsTable() {
    Question q1 = new Question("Easy: A is correct",
            "A", "B", "C", 1, Question.DIFFICULTY_EASY, 1);
    addQuestion(q1);
    Question q2 = new Question("Medium: B is correct",
            "A", "B", "C", 2, Question.DIFFICULTY_MEDIUM, 2);
    addQuestion(q2);
    Question q3 = new Question("Medium: C is correct",
            "A", "B", "C", 3, Question.DIFFICULTY_MEDIUM, 3);
    addQuestion(q3);
    Question q4 = new Question("Hard: A is correct",
            "A", "B", "C", 1, Question.DIFFICULTY_HARD, 4);
    addQuestion(q4);
    Question q5 = new Question("Hard: B is correct",
            "A", "B", "C", 2, Question.DIFFICULTY_HARD, 5);
    addQuestion(q5);
    Question q6 = new Question("Hard: C is correct",
            "A", "B", "C", 3, Question.DIFFICULTY_HARD, 6);
    addQuestion(q6);

    db.execSQL("DELETE FROM " + CategoriesTable.TABLE_NAME + " WHERE ID = 1");
}

Я получаю исключение во время выполнения. Обратите внимание, что я намеренно пытаюсь нарушить ограничение внешнего ключа и просто хочу понять исключение.

Мой первый вопрос:

1) Сбой из-за удаления, но почему logcat говорит только о добавлении вопросов в категории 4, 5 и 6 (которых нет в моей таблице категорий)?

Ошибка при вставке сложности = Hard option1 = A question = Hard: C правильно category_id = 6 answer_nr = 3 option3 = C option2 = B android.database.sqlite.SQLiteConstraintException: ограничение FOREIGN KEY не удалось (код 787)

Почему не говорится об удалении ссылочного столбца, как я ожидал?

2) Почему он выдает исключение времени выполнения, тогда как при попытке добавить вопрос с категорией, которой нет в таблице категорий, просто игнорируется команда SQLite, но не происходит сбой приложения? Является ли исключение времени выполнения ожидаемым поведением при нарушении ограничения внешнего ключа при попытке удалить указанный столбец?


person Florian Walther    schedule 11.04.2018    source источник


Ответы (1)


1) Сбой из-за удаления, но почему logcat говорит только о добавлении вопросов в категории 4, 5 и 6 (которых нет в моей таблице категорий)?

Нет. Сбой или, по крайней мере, показанное сообщение происходит из-за того, что в нем говорится: нарушение ограничения FOREIGN KEY, которое происходит до удаления (в addQuestion(q6);), и поэтому удаление никогда не происходит.

Объяснить ограничение FOREIGN KEY и почему оно не удалось, немного сложно, потому что кажется, что вы перепутали столбцы таблицы. Однако я предполагаю, что это само по себе не является проблемой.

В частности, вы определяете ограничение FOREIGN KEY, используя

FOREIGN KEY(column_in_this_table_which_you_have_right)
    REFERENCES the_other_table_which_you_have_right
        (column_in_the_other_table_????)

Для столбца в другой таблице у вас есть QuestionsTable._ID, он должен быть CategoriesTable._ID. Однако они, вероятно, называются одинаково. Так что это не вызывает проблемы.

Сообщение об ошибке включает category_id=6.

Предполагая, что они оба разрешают идентифицировать, вы фактически говорите

Разрешить вставку строки в таблицу questions только в том случае, если столбец category_id* таблицы **questions совпадает (может ссылаться) на строку в categories< /strong>, где столбец ID совпадает с идентификатором_категории (например, 6).

Короче говоря, отображаемые сообщения говорят, что нет категории с идентификатором 6.


Почему не говорится об удалении ссылочного столбца, как я ожидал?

Потому что он разбился, не дойдя до db.execSQL("DELETE FROM " + CategoriesTable.TABLE_NAME + " WHERE ID = 1");


2) Почему он выдает исключение времени выполнения, тогда как при попытке добавить вопрос с категорией, которой нет в таблице категорий, просто игнорируется команда SQLite, но не происходит сбой приложения?

Потому что, по всей вероятности, ваше приложение вылетит во многих местах без него. Вы говорите, что это очень важно для дизайна. Я почти не сомневаюсь, что если бы опция игнорирования была таковой, вы бы гораздо больше противоречили этому (т. Е. Наилучший доступный вариант, никто не заставляет вас определять ограничение). Хотя, если это то, что вы хотите, вы можете проверить 4.2 Отложенные ограничения внешнего ключа


Является ли исключение времени выполнения ожидаемым поведением при нарушении ограничения внешнего ключа при попытке удалить указанный столбец?

Повторюсь, вы не зашли так далеко.

Однако вы можете проверить 4.3 Действия ON DELETE и ON UPDATE

Изменить - Объяснение путаницы.

Короче говоря, сбой на самом деле происходит из-за ограничения DELETE. Ограничения INSERT фиксируются, но отображаются, поскольку используется стандартный метод insert. Журнал, если вы посмотрите на него внимательно, покажет это, как объяснено ниже.

Вот пример, основанный на вашем коде, который я собрал: -

04-11 21:47:10.241 1927-1927/? E/SQLiteDatabase: Error inserting option1=A category_id=6 option2=B option3=C difficulty=Hard answer_nr=3 question=Hard: C is correct
    android.database.sqlite.SQLiteConstraintException: foreign key constraint failed (code 19)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:775)
        at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
        at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
        at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1469)
        at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1339)
        at askit.questionaire.DatabaseHelper.addQuestion(DatabaseHelper.java:70)
        at askit.questionaire.DatabaseHelper.fillQuestionsTable(DatabaseHelper.java:97)
        at askit.questionaire.DatabaseHelper.onCreate(DatabaseHelper.java:52)
        at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:252)
        at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:164)
        at askit.questionaire.DatabaseHelper.<init>(DatabaseHelper.java:17)
        at askit.questionaire.MainActivity.onCreate(MainActivity.java:14)
        at android.app.Activity.performCreate(Activity.java:5008)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
        at android.app.ActivityThread.access$600(ActivityThread.java:130)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:137)
        at android.app.ActivityThread.main(ActivityThread.java:4745)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:511)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
        at dalvik.system.NativeStart.main(Native Method)
04-11 21:47:10.245 1927-1927/? D/AndroidRuntime: Shutting down VM
04-11 21:47:10.245 1927-1927/? W/dalvikvm: threadid=1: thread exiting with uncaught exception (group=0xa6294288)
04-11 21:47:10.245 1927-1927/? E/AndroidRuntime: FATAL EXCEPTION: main
    java.lang.RuntimeException: Unable to start activity ComponentInfo{askit.questionaire/askit.questionaire.MainActivity}: android.database.sqlite.SQLiteConstraintException: foreign key constraint failed (code 19)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2059)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084)
        at android.app.ActivityThread.access$600(ActivityThread.java:130)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:137)
        at android.app.ActivityThread.main(ActivityThread.java:4745)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:511)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
        at dalvik.system.NativeStart.main(Native Method)
     Caused by: android.database.sqlite.SQLiteConstraintException: foreign key constraint failed (code 19)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(SQLiteConnection.java:727)
        at android.database.sqlite.SQLiteSession.executeForChangedRowCount(SQLiteSession.java:754)
        at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
        at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1665)
        at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1594)
        at askit.questionaire.DatabaseHelper.fillQuestionsTable(DatabaseHelper.java:99)
        at askit.questionaire.DatabaseHelper.onCreate(DatabaseHelper.java:52)
        at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:252)
        at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:164)
        at askit.questionaire.DatabaseHelper.<init>(DatabaseHelper.java:17)
        at askit.questionaire.MainActivity.onCreate(MainActivity.java:14)
        at android.app.Activity.performCreate(Activity.java:5008)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2023)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2084) 
        at android.app.ActivityThread.access$600(ActivityThread.java:130) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195) 
        at android.os.Handler.dispatchMessage(Handler.java:99) 
        at android.os.Looper.loop(Looper.java:137) 
        at android.app.ActivityThread.main(ActivityThread.java:4745) 
        at java.lang.reflect.Method.invokeNative(Native Method) 
        at java.lang.reflect.Method.invoke(Method.java:511) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553) 
        at dalvik.system.NativeStart.main(Native Method) 

Вы видите эквивалент: -

04-11 21:47:10.241 1927-1927/? E/SQLiteDatabase: Error inserting option1=A category_id=6 option2=B option3=C difficulty=Hard answer_nr=3 question=Hard: C is correct
    android.database.sqlite.SQLiteConstraintException: foreign key constraint failed (code 19)

Однако это не приводит к сбою приложения.

  • Если вы запустите приложение после удаления базы данных с закомментированным DELETE и посмотрите журнал, вы увидите то же самое в журнале, даже если приложение не аварийно завершает работу.

Что вам нужно сделать, так это продолжить просмотр журнала, и вы найдете эквивалент: -

04-11 21:47:10.245 1927-1927/? D/AndroidRuntime: Shutting down VM
04-11 21:47:10.245 1927-1927/? W/dalvikvm: threadid=1: thread exiting with uncaught exception (group=0xa6294288)
04-11 21:47:10.245 1927-1927/? E/AndroidRuntime: FATAL EXCEPTION: main
    java.lang.RuntimeException: Unable to start activity ComponentInfo{askit.questionaire/askit.questionaire.MainActivity}: android.database.sqlite.SQLiteConstraintException: foreign key constraint failed (code 19)

Это сбой, то есть FATAL EXCEPTION:

Если вы продолжите, вы увидите вызвано, вот куда вы смотрите, так что у вас есть:-

 Caused by: android.database.sqlite.SQLiteConstraintException: foreign key constraint failed (code 19)
    at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)
    at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(SQLiteConnection.java:727)
    at android.database.sqlite.SQLiteSession.executeForChangedRowCount(SQLiteSession.java:754)
    at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
    at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1665)
    at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1594)
    at askit.questionaire.DatabaseHelper.fillQuestionsTable(DatabaseHelper.java:99)
    at askit.questionaire.DatabaseHelper.onCreate(DatabaseHelper.java:52)
    at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:252)
    at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:164)
    at askit.questionaire.DatabaseHelper.<init>(DatabaseHelper.java:17)
    at askit.questionaire.MainActivity.onCreate(MainActivity.java:14)
    at android.app.Activity.performCreate(Activity.java:5008) 

Если вы просмотрите журнал, вызванный, вы увидите, что первая строка, в которой упоминается ваш пакет:

at askit.questionaire.DatabaseHelper.fillQuestionsTable(DatabaseHelper.java:99)

Строка 99 (ваш номер строки будет другим, как и пакет) указывает на

введите здесь описание изображения

  • Примечание. Я изменил строку, чтобы использовать КОНСТАНТ (CategoriesTable._ID) для столбца идентификатора категории.
person MikeT    schedule 11.04.2018
comment
Но дело в том, что если я пропущу часть удаления, мое приложение НЕ вылетит при попытке добавить вопрос с несуществующей категорией. Он просто не добавляет вопрос (и поддерживает работу приложения). Вот что меня смущает. У меня нет этого исключения во время выполнения, если я не пытаюсь удалить категорию. - person Florian Walther; 11.04.2018
comment
Тогда то, что вы, вероятно, делаете, это не просмотр всей трассировки журнала/правого стека. Я предполагаю, что у вас есть вставки, завернутые в try/catch, поэтому вы перехватываете их, чтобы обработка продолжалась, и далее вы получаете фактическую трассировку стека сбоя. - person MikeT; 11.04.2018
comment
Это объясняет, почему, если вы удалите удаление, все будет хорошо. - person MikeT; 11.04.2018
comment
Имеет смысл, но у меня нет блоков try/catch - person Florian Walther; 11.04.2018
comment
Другой потенциальный фактор заключается в том, что onCreate выполняется внутри транзакции. Придется увидеть больше вашего кода, чтобы иметь возможность его запустить. - person MikeT; 11.04.2018
comment
Ну, я могу вставить SQLiteOpenHelper, но у него 200 строк. - person Florian Walther; 11.04.2018
comment
Мне также понадобится класс Question. Тем не менее, здесь уже поздно, так что в любом случае я не смогу изучить это до завтра. - person MikeT; 11.04.2018
comment
@FlorianWalther решил, по крайней мере, причину. Ответ был отредактирован. - person MikeT; 12.04.2018
comment
Большое спасибо за подробный ответ. Я прочитаю его более подробно как можно скорее. - person Florian Walther; 12.04.2018
comment
Таким образом, попытка добавить вопрос с несуществующей категорией будет просто проигнорирована, но не приведет к сбою, верно? Принимая во внимание, что попытка удалить категорию, на которую есть ссылка в 1 или нескольких вопросах, без каскадного удаления приводит к сбою. Ваши наблюдения совпадают? - person Florian Walther; 13.04.2018
comment
@FlorianWalther, как вы его настроили, тогда да. insert по умолчанию имеет значение insert or ignore (метод insertOrThrow не будет) и охватывает другие ограничения, например. NOT NULL, UNIQUE (нет реальной проблемы с точки зрения ссылочной целостности). Однако удаление будет проблемой, поскольку это нарушит ссылочную целостность. Так что да, такие же/похожие наблюдения. - person MikeT; 13.04.2018
comment
Большой! Я не могу отблагодарить вас за все ваши усилия! - person Florian Walther; 13.04.2018
comment
Я только что понял, почему приложение вылетело.. Потому что я написал WHERE ID = вместо WHERE _ID = - person Florian Walther; 07.08.2018