如何在 Join table 接受 "zero" 值中设置 Android 房间外键?

How do I setup Android Room Foreign Keys in a Join table accept "zero" values?

问题: 对于 Android Room,当输入零 (0) 时,我在连接 table 上收到外键约束错误。我仍在努力学习 Android Room,所以我确定我忽略了文档和教程中的某些内容。

我尝试过的方法: 我确实在使用 SQLite 的桌面版本中安装了此应用程序,我没有遇到任何问题,并已尝试对其进行相同的设置。我在这里和其他地方阅读了一些 posts 的例子,甚至删除了 return 零的外键声明,但后来我很难开始在不同的 [=37] 上触发模式=] 完全。我尝试了默认值,但设置为 null 但似乎无法正常工作。在写这个 post 时,我仍然会重新阅读一些内容,但我确定我只是忽略了一些东西 - 旧的 80/20 规则。

我正在尝试做的事情: 此联接 table 从 6 table 中捕获 ID。但是,根据用户条目,有 3 个外键,用户可能没有添加任何要引用的内容。由于 Android Room 本质上是包装 SQLite 以便于使用,到目前为止,我还无法确定如何让它接受外键上的零值。我想尝试使用 OnConflict 注释,但想寻求一些想法和建议。

实体

@Entity(tableName = "Notes", foreignKeys = {
        @ForeignKey(entity = Sources.class, parentColumns = "SourceID", childColumns = "SourceID"),
        @ForeignKey(entity = Comments.class, parentColumns = "CommentID", childColumns = "CommentID"),
        @ForeignKey(entity = Questions.class, parentColumns = "QuestionID", childColumns = "QuestionID"),
        @ForeignKey(entity = Quotes.class, parentColumns = "QuoteID", childColumns = "QuoteID"),
        @ForeignKey(entity = Terms.class, parentColumns = "TermID", childColumns = "TermID"),
        @ForeignKey(entity = Topics.class, parentColumns = "TopicID", childColumns = "TopicID")},
        indices = {@Index("SourceID"), @Index("CommentID"), @Index("QuestionID"), @Index("QuoteID"),
                @Index("TermID"), @Index("TopicID")})
public class Notes {

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "NoteID")
    private int noteID;
    @ColumnInfo(name = "SourceID")
    private int sourceID;
    @ColumnInfo(name = "CommentID")
    private int commentID;
    @ColumnInfo(name = "QuestionID", defaultValue = "0")
    private int questionID;
    @ColumnInfo(name = "QuoteID", defaultValue = "0")
    private int quoteID;
    @ColumnInfo(name = "TermID", defaultValue = "0")
    private int termID;
    @ColumnInfo(name = "TopicID")
    private int topicID;
    @ColumnInfo(name = "Deleted", defaultValue = "0")
    private int deleted;

public Notes(int noteID, int sourceID, int commentID, int questionID, int quoteID, int termID, int topicID, int deleted){
    this.noteID = noteID;
    this.sourceID = sourceID;
    this.commentID = commentID;
    this.questionID = questionID;
    this.quoteID = quoteID;
    this.termID = termID;
    this.topicID = topicID;
    this.deleted = deleted;
}

架构部分

    "tableName": "Notes",
    "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NoteID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `SourceID` INTEGER NOT NULL, `CommentID` INTEGER NOT NULL, `QuestionID` INTEGER NOT NULL DEFAULT 0, `QuoteID` INTEGER NOT NULL DEFAULT 0, `TermID` INTEGER NOT NULL DEFAULT 0, `TopicID` INTEGER NOT NULL, `Deleted` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`SourceID`) REFERENCES `Sources`(`SourceID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`CommentID`) REFERENCES `Comments`(`CommentID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`QuestionID`) REFERENCES `Questions`(`QuestionID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`QuoteID`) REFERENCES `Quotes`(`QuoteID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`TermID`) REFERENCES `Terms`(`TermID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`TopicID`) REFERENCES `Topics`(`TopicID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
    "fields": [
      {
        "fieldPath": "noteID",
        "columnName": "NoteID",
        "affinity": "INTEGER",
        "notNull": true
      },
      {
        "fieldPath": "sourceID",
        "columnName": "SourceID",
        "affinity": "INTEGER",
        "notNull": true
      },
      {
        "fieldPath": "commentID",
        "columnName": "CommentID",
        "affinity": "INTEGER",
        "notNull": true
      },
      {
        "fieldPath": "questionID",
        "columnName": "QuestionID",
        "affinity": "INTEGER",
        "notNull": true,
        "defaultValue": "0"
      },
      {
        "fieldPath": "quoteID",
        "columnName": "QuoteID",
        "affinity": "INTEGER",
        "notNull": true,
        "defaultValue": "0"
      },
      {
        "fieldPath": "termID",
        "columnName": "TermID",
        "affinity": "INTEGER",
        "notNull": true,
        "defaultValue": "0"
      },
      {
        "fieldPath": "topicID",
        "columnName": "TopicID",
        "affinity": "INTEGER",
        "notNull": true
      },
      {
        "fieldPath": "deleted",
        "columnName": "Deleted",
        "affinity": "INTEGER",
        "notNull": true,
        "defaultValue": "0"
      }
    ],
    "primaryKey": {
      "columnNames": [
        "NoteID"
      ],
      "autoGenerate": true
    },
    "indices": [
      {
        "name": "index_Notes_SourceID",
        "unique": false,
        "columnNames": [
          "SourceID"
        ],
        "createSql": "CREATE INDEX IF NOT EXISTS `index_Notes_SourceID` ON `${TABLE_NAME}` (`SourceID`)"
      },
      {
        "name": "index_Notes_CommentID",
        "unique": false,
        "columnNames": [
          "CommentID"
        ],
        "createSql": "CREATE INDEX IF NOT EXISTS `index_Notes_CommentID` ON `${TABLE_NAME}` (`CommentID`)"
      },
      {
        "name": "index_Notes_QuestionID",
        "unique": false,
        "columnNames": [
          "QuestionID"
        ],
        "createSql": "CREATE INDEX IF NOT EXISTS `index_Notes_QuestionID` ON `${TABLE_NAME}` (`QuestionID`)"
      },
      {
        "name": "index_Notes_QuoteID",
        "unique": false,
        "columnNames": [
          "QuoteID"
        ],
        "createSql": "CREATE INDEX IF NOT EXISTS `index_Notes_QuoteID` ON `${TABLE_NAME}` (`QuoteID`)"
      },
      {
        "name": "index_Notes_TermID",
        "unique": false,
        "columnNames": [
          "TermID"
        ],
        "createSql": "CREATE INDEX IF NOT EXISTS `index_Notes_TermID` ON `${TABLE_NAME}` (`TermID`)"
      },
      {
        "name": "index_Notes_TopicID",
        "unique": false,
        "columnNames": [
          "TopicID"
        ],
        "createSql": "CREATE INDEX IF NOT EXISTS `index_Notes_TopicID` ON `${TABLE_NAME}` (`TopicID`)"
      }
    ],
    "foreignKeys": [
      {
        "table": "Sources",
        "onDelete": "NO ACTION",
        "onUpdate": "NO ACTION",
        "columns": [
          "SourceID"
        ],
        "referencedColumns": [
          "SourceID"
        ]
      },
      {
        "table": "Comments",
        "onDelete": "NO ACTION",
        "onUpdate": "NO ACTION",
        "columns": [
          "CommentID"
        ],
        "referencedColumns": [
          "CommentID"
        ]
      },
      {
        "table": "Questions",
        "onDelete": "NO ACTION",
        "onUpdate": "NO ACTION",
        "columns": [
          "QuestionID"
        ],
        "referencedColumns": [
          "QuestionID"
        ]
      },
      {
        "table": "Quotes",
        "onDelete": "NO ACTION",
        "onUpdate": "NO ACTION",
        "columns": [
          "QuoteID"
        ],
        "referencedColumns": [
          "QuoteID"
        ]
      },
      {
        "table": "Terms",
        "onDelete": "NO ACTION",
        "onUpdate": "NO ACTION",
        "columns": [
          "TermID"
        ],
        "referencedColumns": [
          "TermID"
        ]
      },
      {
        "table": "Topics",
        "onDelete": "NO ACTION",
        "onUpdate": "NO ACTION",
        "columns": [
          "TopicID"
        ],
        "referencedColumns": [
          "TopicID"
        ]
      }
    ]
  },

@ForeginKey 是添加 SQLite FOREIGN KEY 子句的注解。 SQLite FOREIGN KEY 子句是一个 rule (约束),表示列的值必须是指定的 table/column(s) 中的现有值。如果相关 table 中没有 0 值,则无法插入该行,而是发生冲突。

FOREIGN KEY 不是形成关系所必需的,而是用于确保参照完整性,即没有孤立行。

如果您必须使用 0 表示没有相关项目(并非真正必需),那么您有两个选择。

  • by 不是真的需要见example/demo,输出包括0个关系。

选项是:-

  1. 不要使用外键,因此不存在阻止 0(或使用 null)或
  2. 的规则
  3. 或者在相关的 table 中有一行有 0 被相应地处理。

I thought about try an OnConflict annotation but wanted to reach out for some thoughts and suggestions.

使用 @OnConflict 不适用于外键冲突,但适用于由违反唯一非空检查和主键约束引起的冲突。

I tried default values but and setting up for null but can't seem to get it to work.

此外,使用 @ColumnInfodefaultValue 不会使用 @Insert 插入默认值,因为始终为所有列提供值(在某些情况下禁止主键) .要应用 defaultValue,您需要使用使用 INSERT INTO the_table (csv_list_of_columns) VALUES(csv_list_of_values_for_each_column)@Query,其中要默认的列不在列列表中,因此没有相应的值(所以将使用默认值)。


工作 example/demo(为简洁起见,只有一个关系注 > 来源)

该示例使用两个解决方案,第一个(实体 NotesV1/SourceV1)不使用外键,第二个使用(NotesV2/SourceV2)。两种解决方案都允许空值,后者根据 :-

利用 SQLite 中外键的空值处理

There is one exception: if the foreign key column in the track table is NULL, then no corresponding entry in the artist table is required. https://sqlite.org/foreignkeys.html

还应注意使用对象而不是基元,因此使用 Integer 而不是 int。但是,由于 id 高达 64 位签名,我使用了 Long/long。使用 Long/Integer 允许 null,基元不能为 null。

我还添加了一个名称列以允许显示描述。

所以两组实体:-

NotesV1:-

@Entity(
        indices = {
                @Index("SourceIDMap")
        }
)
class NotesV1 {
    @PrimaryKey
    @ColumnInfo(name = "NoteID")
    Long noteId = null;
    @ColumnInfo(name = "SourceIDMap",defaultValue = "0")
    Long sourceId = null;
    @ColumnInfo(name = "SourceName",defaultValue = "Not Given")
    String noteName;

    NotesV1(){}
    @Ignore
    NotesV1(Long noteId, Long sourceId, String noteName) {
        this.noteId = noteId;
        this.sourceId = sourceId;
        this.noteName = noteName;
    }
    @Ignore
    NotesV1(long sourceId, String noteName) {
        this(null,sourceId,noteName);
    }
    @Ignore
    NotesV1(String noteName) {
        this(null,null,noteName);
    }
}
  • 被忽略的构造函数提供了更大的灵活性

NotesV2(带外键):-

@Entity(
        foreignKeys = {
                @ForeignKey(entity = SourceV2.class,parentColumns = {"SourceID"}, childColumns = {"SourceIDMap"})
        },
        indices = {
                @Index("SourceIDMap")
        }
)
class NotesV2 {
    @PrimaryKey
    @ColumnInfo(name = "NoteID")
    Long noteId = null;
    // suggest to always have unique column names so SourceIDMap will map to SourceID
    @ColumnInfo(name = "SourceIDMap",defaultValue = "0")
    Long sourceId = null;
    @ColumnInfo(name = "SourceName",defaultValue = "Not Given")
    String noteName;
}
  • 没有添加额外的忽略构造函数

SourceV1

@Entity
class SourceV1 {
    @PrimaryKey
    @ColumnInfo(name = "SourceID")
    Long sourceId = null;
    String sourceName;
}

SourceV2(除非 class 名称与 SourceV1 相同):-

@Entity
class SourceV2 {
    @PrimaryKey
    @ColumnInfo(name = "SourceID")
    Long sourceId = null;
    String sourceName;
}

通过满足 Notes 和 Source 之间的连接 (@Relation) 的两个 POJOS 的关系显示结果:-

NotesV1WithSources

class NotesV1WithSources {
    @Embedded
    NotesV1 notesV1;
    @Relation(
            entity = SourceV1.class,parentColumn = "SourceIDMap",entityColumn = "SourceID"
    )
    List<SourceV1> sourceV1List;
}

和基本相同的NotesV2WithSources :-

class NotesV2WithSources {
    @Embedded
    NotesV2 notesV2;
    @Relation(
            entity = SourceV2.class,parentColumn = "SourceIDMap",entityColumn = "SourceID"
    )
    List<SourceV2> sourceV2List;
}

单个 @DAO class AllDao (注意包括仅针对 NotesV1 使用 defaultValue 的插入查询) :-

@Dao
abstract class AllDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(NotesV1 notesV1);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(NotesV2 notesV2);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(SourceV1 sourceV1);
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    abstract long insert(SourceV2 sourceV2);

    /* Using @ColumnInfo's defaultValue examples */
    @Query("INSERT INTO notesV1 (SourceIdMap) VALUES(:sourceId)")
    abstract void insertNotesV1OnlyBySourceId(long sourceId);
    @Query("INSERT INTO notesV1 (SourceName) VALUES(:sourceName)")
    abstract void insertNotesV1OnlyBySourceName(String sourceName);

    @Transaction
    @Query("SELECT * FROM notesv1")
    abstract List<NotesV1WithSources> getAllNotesV1WithSources();
    @Transaction
    @Query("SELECT * FROM notesv2")
    abstract List<NotesV2WithSources> getAllNotesV2WithSources();
}

漂亮 standard/simple @Database class TheDatabase :-

@Database(entities = {NotesV1.class,NotesV2.class,SourceV1.class,SourceV2.class},version = 1)
abstract class TheDatabase extends RoomDatabase {
    abstract AllDao getAllDao();

    private static volatile TheDatabase instance = null;

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

最后一个 Activity 即 uses/demonstrates 以上,MainActivity :-

public class MainActivity extends AppCompatActivity {

    TheDatabase db;
    AllDao dao;
    private static final String TAGV1 = "NOTESV1INFO";
    private static final String TAGV2 = "NOTESV2INFO";

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

        db = TheDatabase.getInstance(this);
        dao = db.getAllDao();

        /* Without Foreign keys  and shows defaultValue */
        SourceV1 s1 = new SourceV1();
        s1.sourceName = "Source 1";
        s1.sourceId = dao.insert(s1);
        dao.insert(new NotesV1()); // All defaults
        dao.insert(new NotesV1()); /// and again
        dao.insert(new NotesV1("Just the Note Name")); // Via @Ignore'd constructor
        dao.insert(new NotesV1(1000L,s1.sourceId,"All provided")); // Via @Ignore'd constrcutor
        dao.insertNotesV1OnlyBySourceId(0); // Via @Query with 0 passed as SourceId (so name is default)
        dao.insertNotesV1OnlyBySourceName("Again just the note name"); //Via @Query with name passed (so SourceId is 0)

        for(NotesV1WithSources nv1: dao.getAllNotesV1WithSources()) {
            Log.d(TAGV1,
                    "Note ID is " + nv1.notesV1.noteId +
                            " Name is " + nv1.notesV1.noteName +
                            " SourceID is " + nv1.notesV1.sourceId +
                            "\nSources(" + nv1.sourceV1List.size() + ") Are:-");
            for (SourceV1 s: nv1.sourceV1List) {
                Log.d(TAGV1,"\tSource ID is " + s.sourceId + " Source Name is " + s.sourceName );
            }
        }

        /* With Foreign Keys */
        // SPECIAL ROW 0  INDICATIVE OF NO RELATION
        SourceV2 s2 = new SourceV2();
        s2.sourceId = 0L;
        s2.sourceName = "I SHOULD BE IGNORED";
        Log.d(TAGV2,"1. Special Source row Inserted with ID = " + dao.insert(s2));
        Log.d(TAGV2,"2. NotesV2 insert ID was " + dao.insert(new NotesV2()) + " (note -1 then not inserted)");
        NotesV2 n2 = new NotesV2();
        n2.sourceId = 0L;
        n2.noteName ="references special source";
        Log.d(TAGV2,"3. NotesV2 insert ID was " + dao.insert(n2) + " (note -1 then not inserted)");
        /*
        //OUCH NO such Source2  so Foreign Key conflict NOT trapped by IGNORE so abends
        e.g. android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
        n2.sourceId = 100L;
        Log.d(TAGV2,"4. NotesV2 insert ID was " + dao.insert(n2) + " (note -1 then not inserted)");
         */
        s2 = new SourceV2();
        s2.sourceName = "I am a normal source";
        long s2Id = -99;
        Log.d(TAGV2,"5. Normal Source row Inserted with ID = " + (s2Id = dao.insert(s2)));
        n2.sourceId = s2Id;

        for(NotesV2WithSources nv2: dao.getAllNotesV2WithSources()) {
            Log.d(TAGV2,
                    "Note ID is " + nv2.notesV2.noteId +
                            " Name is " + nv2.notesV2.noteName +
                            " SourceID is " + nv2.notesV2.sourceId +
                            "\nSources(" + nv2.sourceV2List.size() + ") Are:-");
            for (SourceV2 s: nv2.sourceV2List) {
                Log.d(TAGV2,"\tSource ID is " + s.sourceId + " Source Name is " + s.sourceName );
            }
        }
    }
}
  • 注意以上设计为运行一次而已
  • 运行 在主线程上为简洁起见
  • 上半场用V1的下半场用V2的
  • 请注意注释掉的插入,它会崩溃,因为 IGNORE 不会忽略外键约束冲突。
    • 评论包含此类崩溃的摘录

当运行输出为:-

D/NOTESV1INFO: Note ID is 1 Name is null SourceID is null
    Sources(0) Are:-
D/NOTESV1INFO: Note ID is 2 Name is null SourceID is null
    Sources(0) Are:-
D/NOTESV1INFO: Note ID is 3 Name is Just the Note Name SourceID is null
    Sources(0) Are:-
D/NOTESV1INFO: Note ID is 1000 Name is All provided SourceID is 1
    Sources(1) Are:-
D/NOTESV1INFO:  Source ID is 1 Source Name is Source 1
D/NOTESV1INFO: Note ID is 1001 Name is Not Given SourceID is 0
    Sources(0) Are:-
D/NOTESV1INFO: Note ID is 1002 Name is Again just the note name SourceID is 0
    Sources(0) Are:-
D/NOTESV2INFO: 1. Special Source row Inserted with ID = 0
D/NOTESV2INFO: 2. NotesV2 insert ID was 1 (note -1 then not inserted)
D/NOTESV2INFO: 3. NotesV2 insert ID was 2 (note -1 then not inserted)
D/NOTESV2INFO: 5. Normal Source row Inserted with ID = 1
D/NOTESV2INFO: Note ID is 1 Name is null SourceID is null
    Sources(0) Are:-
D/NOTESV2INFO: Note ID is 2 Name is references special source SourceID is 0
    Sources(1) Are:-
D/NOTESV2INFO:  Source ID is 0 Source Name is I SHOULD BE IGNORED
  • 可以看出关系的存在不需要外键(即 V1 根本没有定义 FK)。
  • 还可以看出,空值存在并且不会导致任何问题。

使用 Application Inspector(以前称为 Database Inspector)然后 :-