使用 Kotlin 在 Android 房间数据库中插入对象列表

Inserting a List of Objects in Android Room Database with Kotlin

Android 的初学者正在努力解决房间数据库的这一限制。我正在处理两张桌子,服装和服装。用户可以通过插入呈现给他们的值来创建 Outfit。然后在一个单独的页面上,用户可以插入一个带有他们之前在 Clothing.kt 中创建的服装的服装。为了应用程序,关系将只是一对多的,这意味着我只需要使用许多 Clo​​thing Items 创建一个 Outfit。到目前为止,这是我的代码:

Clothing.kt

@Parcelize
@Entity(foreignKeys = [
    ForeignKey(entity = Outfit::class,
        parentColumns = ["id"],
        childColumns = ["outfitRefFK"]
        )
    ]
)
data class Clothing (
    //Sets all attributes and primary key
    @PrimaryKey(autoGenerate = true) val id: Int,
    val type: String,
    val color: String,
    val style: String,
    val description: String,
    val dateAdded: Date = Date(),
    val brand: String,
    val theme: String,
    val image: String,
    @Nullable val outfitRefFK: Int
    ): Parcelable

Outfit.kt

@Parcelize
@Entity
data class Outfit (
    @PrimaryKey(autoGenerate = true) val id: Int,
    val outfitName: String,
    @Ignore
    val ClothingItems: List<Clothing>

):Parcelable

我看过很多 Android 开发者文档,他们都提到了如何使用相同的服装列表查询服装,但没有提到如何使用 List 对象插入新服装。

据我所知,SQLite 无法处理列表。因此,我尝试的一种方法是使用类型转换器,但是,我很难将其实现到我的代码中,主要是因为我是 GSON 的新手。

一个例子,来自 Google Android 我一直在尝试实现的文档,对我来说不太有意义,但似乎可以在 POJO 之后插入对象列表:

Google 插入示例:

@Dao
public interface MusicDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  public fun insertSongs(varargs songs: Song)

  @Insert
  public fun insertBoth(song1: Song, song2: Song)

  @Insert
  public fun insertAlbumWithSongs(album: Album, songs: List<Song>);
}

我假设我的目标是用类似的方法复制它,从 List 创建 Outfit。据我所知,Google Docs 使用 3 Tables(音乐、专辑和歌曲),所以我一直在努力寻找可以修改我的数据库的地方。我应该创建第三个 Table 吗?有没有人对 Kotlin 得出类似的结论? 如果你们中的任何人已经解决了这个问题或者接近了,非常感谢任何建议。

这里的其他来源是我的 Tables 的道,还没有完成,因为我想不出存储服装项目的方法。

Clothing.Dao

@Dao
interface ClothingDao {

    //Ignores when the exact same data is put in
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addClothing(clothing: Clothing)

    @Update
    suspend fun updateClothing(clothing: Clothing)

    @Delete
    suspend fun deleteClothing(clothing: Clothing)

    @Query("DELETE FROM Clothing")
    suspend fun deleteAllClothing()

    @Query("SELECT * FROM Clothing ORDER BY id ASC")
    fun readAllData(): LiveData<List<Clothing>>

    @Query("SELECT * FROM Clothing WHERE type='Top' ORDER BY id ASC")
    fun selectClothingTops(): LiveData<List<Clothing>>

    //Called in ListFragment Searchbar. Queries Clothing Type or Clothing Color.
    @Query("SELECT * FROM Clothing WHERE type LIKE :searchQuery OR color LIKE :searchQuery")
    fun searchDatabase(searchQuery: String): LiveData<List<Clothing>>

}

OutfitDao.kt

@Dao
interface OutfitDao {

    // Grabs data from Outfit Table, necessary for each other Query to read
    // from in the Outfit Repository class

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun addOutfit(outfit: Outfit)


    @Query("SELECT * FROM Outfit ORDER BY id ASC")
    fun readAllData(): LiveData<List<Outfit>>


}

To my knowledge, SQLite cannot handle Lists. So, one approach I tried was to use a Type Converter, however, I struggled to implement this into my code, mostly because I'm new to GSON.

1).将 Gson 库添加到您的项目中,例如在你的build.gradle(模块)中:-

 implementation 'com.google.code.gson:gson:2.9.0'

2).添加数据 class 例如 ClothingList :-

data class ClothingList(
    val clothingList: List<Clothing>
)

3).修改 Outfit class 以使用 ClothingList 而不是 List 并删除 @Ignore 注释,例如:-

@Entity
data class Outfit (
    @PrimaryKey(autoGenerate = true) val id: Int, /* more correct to use Long */
    val outfitName: String,
    //@Ignore
    val ClothingItems: ClothingList
)
  • 自动生成的列更正确地是 Long 的而不是 Int 的,因为理论上存储的值最多可以有 64 位符号。

4).为 TypeConverters 添加一个新的 class 例如MyTypeConverters :-

class MyTypeConverters {

    @TypeConverter
    fun fromDateToLong(date: Date): Long {
        return date.time
    }
    @TypeConverter
    fun fromLongToDate(date: Long): Date {
        return Date(date)
    }
    @TypeConverter
    fun fromClothingToJSON(clothinglist: ClothingList): String {
        return Gson().toJson(clothinglist)
    }
    @TypeConverter
    fun fromJSONToClothing(json: String): ClothingList {
        return Gson().fromJson(json,ClothingList::class.java)
    }
}

5).修改 @Database 注释 class (具有最高范围)以具有 @TypeConverters 注释,例如

@TypeConverters(value = [MyTypeConverters::class])
@Database(entities = [Clothing::class,Outfit::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
....
}

你可以给他们一份服装清单。然而,从关系数据库的角度来看,这并不是真正理想的方式,因为它会引入复杂性,因为整个衣服列表是一个单一的存储值。

你的第二次尝试(看起来是)将一件衣服与一件衣服联系在一起,所以你的“蓝色牛仔裤”如果用于多件衣服就必须重复。

建议的解决方案

我建议更好的解决方案是 many-many 关系,这样一套服装可以使用任意数量的服装,而一件服装可以由任意数量的服装使用。因此,您的“蓝色牛仔裤”将是一排​​。

要利用 many-many 关系,您需要一个中间 table 关系,它是服装和衣服之间的交叉引用。即服装 ID 列和服装项目 ID 列。然后就不需要类型转换器或存储列表

工作示例

考虑以下工作示例:-

OutFitclass

@Entity
data class Outfit(
    @PrimaryKey
    @ColumnInfo(name = "outfitId")
    val id: Long?=null,
    val outfitName: String
)

还有服装Class

@Entity
data class Clothing (
    //Sets all attributes and primary key
    @PrimaryKey/*(autoGenerate = true) inefficient not needed*/
    @ColumnInfo(name = "clothingId") /* suggest to have unique column names */
    val id: Long?=null, /* Long rather than Int */
    val type: String,
    val color: String,
    val style: String,
    val description: String,
    val dateAdded: Date = Date(),
    val brand: String,
    val theme: String,
    val image: String
)

many-many 关系的中间(映射、关联、引用和其他名称)table

@Entity(
    primaryKeys = ["outfitIdRef","clothingIdRef"],
    foreignKeys = [
        ForeignKey(
            entity = Outfit::class,
            parentColumns = ["outfitId"],
            childColumns = ["outfitIdRef"],
            onUpdate = ForeignKey.CASCADE,
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Clothing::class,
            parentColumns = ["clothingId"],
            childColumns = ["clothingIdRef"],
            onUpdate = ForeignKey.CASCADE,
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class OutFitClothingMappingTable (
    val outfitIdRef: Long,
    @ColumnInfo(index = true)
    val clothingIdRef: Long
)

A POJO class OutFitWithClothingList 用于获得带有相关服装列表的服装。

data class OutFitWithClothingList(
    @Embedded
    val outfit: Outfit,
    @Relation(
        entity = Clothing::class,
        parentColumn = "outfitId",
        entityColumn = "clothingId",
        associateBy = Junction(
            value = OutFitClothingMappingTable::class,
            parentColumn = "outfitIdRef",
            entityColumn = "clothingIdRef"
        )
    )
    val clothingList: List<Clothing>
)

POJO 与使用它的服装相反的方式

data class ClothingWithOutFitsList(
    @Embedded
    val clothing: Clothing,
    @Relation(
        entity = Outfit::class,
        parentColumn = "clothingId",
        entityColumn = "outfitId",
        associateBy = Junction(
            value = OutFitClothingMappingTable::class,
            parentColumn = "clothingIdRef",
            entityColumn = "outfitIdRef"
        )
    )
    val outfitList: List<Outfit>
)

A class 带有日期类型转换器(将日期存储为整数,即 Long):-

class TheTypeConverters {
    @TypeConverter
    fun fromDateToLong(date: Date): Long {
        return date.time
    }
    @TypeConverter
    fun fromLongToDate(date: Long): Date {
        return Date(date)
    }
}

A single (for brevity/convenience) @Dao annotated class Alldao 包括获取所有服装及其服装列表的查询所有使用过服装的服装项目,当然还有插入到 tables.

@Dao
interface AllDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun addOutfit(outfit: Outfit): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun addClothing(clothing: Clothing): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun addOutfitClothingMap(outFitClothingMappingTable: OutFitClothingMappingTable): Long /* value not of much use other than if 1 or greater insert, if -1 not inserted */

    @Query("SELECT * FROM clothing")
    fun getAllClothing(): List<Clothing>
    @Query("SELECT * FROM outfit")
    fun getAllOutfits(): List<Outfit>


    @Query("SELECT * FROM outfit")
    fun getAllOutfitsWithClothingList(): List<OutFitWithClothingList>

    @Query("SELECT * FROM clothing")
    fun getAllClothingWithOutfitList(): List<ClothingWithOutFitsList>
    
}

带注释的@Database class(为简洁起见使用.allowMainThreadQuesries

@TypeConverters(value = [TheTypeConverters::class])
@Database(entities = [Outfit::class,Clothing::class,OutFitClothingMappingTable::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDao(): AllDao

    companion object {
        @Volatile
        var instance: TheDatabase? = null
        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}
  • 在数据库级别(最高范围)定义的类型转换器

最后 activity 代码演示了插入服装、服装和映射以及提取所有服装和列表中的服装以及使用服装项目的服装列表中的所有服装。

class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: AllDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

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

        val outfit1 = dao.addOutfit(Outfit(outfitName = "Outfit1"))
        val outfit2 = dao.addOutfit(Outfit(outfitName = "Outfit2"))

        val clothing1 = dao.addClothing(Clothing(type = "Top", color = "Red", description = "Singlet",brand = "Fred's Clothing Inc", theme = "whatever", image = "image001", style = "style1"))
        val clothing2 = dao.addClothing(Clothing(type = "Bottom", color = "Blue", description = "Shorts",brand = "AC", theme = "whatever", image = "image002", style = "style2"))
        val clothing3 = dao.addClothing(Clothing(type = "Bottom", color = "White", description = "Skirt",brand = "AC", theme = "whatever", image = "image003", style = "style3"))
        val clothing4 = dao.addClothing(Clothing(type = "Hat", color = "Brown", description = "Hat with feather",brand = "AC", theme = "whatever", image = "image003", style = "style4"))
        // etc

        dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit1,clothing1))
        dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit1,clothing2))
        dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit2,clothing1))
        dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit2,clothing3))
        dao.addOutfitClothingMap(OutFitClothingMappingTable(outfit2,clothing4))


        for (owc in dao.getAllOutfitsWithClothingList()) {
            Log.d("DBINFO","Outfit is ${owc.outfit.outfitName} ID is ${owc.outfit.id}, it has ${owc.clothingList.size} Items of Clothing, they are:-")
            for (c in owc.clothingList) {
                Log.d("DBINFO","\tClothing Item desc is ${c.description} Date is ${c.dateAdded} Brand is ${c.brand} type is ${c.type} etc")
            }
        }


        for (cwo in dao.getAllClothingWithOutfitList()) {
            Log.d("DBINFO","Clothing is ${cwo.clothing.description} color is ${cwo.clothing.color} it is used by ${cwo.outfitList.size } Outfits, they are:-")
            for(o in cwo.outfitList) {
                Log.d("DBINFO","\tOutfit is ${o.outfitName} it's ID is ${o.id}")
            }
        }

    }
}

结果(输出到日志)

2022-05-01 08:55:15.287 D/DBINFO: Outfit is Outfit1 ID is 1, it has 2 Items of Clothing, they are:-
2022-05-01 08:55:15.294 D/DBINFO:   Clothing Item desc is Singlet Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is Fred's Clothing Inc type is Top etc
2022-05-01 08:55:15.294 D/DBINFO:   Clothing Item desc is Shorts Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is AC type is Bottom etc
2022-05-01 08:55:15.294 D/DBINFO: Outfit is Outfit2 ID is 2, it has 3 Items of Clothing, they are:-
2022-05-01 08:55:15.294 D/DBINFO:   Clothing Item desc is Singlet Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is Fred's Clothing Inc type is Top etc
2022-05-01 08:55:15.294 D/DBINFO:   Clothing Item desc is Skirt Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is AC type is Bottom etc
2022-05-01 08:55:15.295 D/DBINFO:   Clothing Item desc is Hat with feather Date is Sun May 01 08:55:15 GMT+10:00 2022 Brand is AC type is Hat etc


2022-05-01 08:55:15.298 D/DBINFO: Clothing is Singlet color is Red it is used by 2 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO:   Outfit is Outfit1 it's ID is 1
2022-05-01 08:55:15.298 D/DBINFO:   Outfit is Outfit2 it's ID is 2
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Shorts color is Blue it is used by 1 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO:   Outfit is Outfit1 it's ID is 1
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Skirt color is White it is used by 1 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO:   Outfit is Outfit2 it's ID is 2
2022-05-01 08:55:15.298 D/DBINFO: Clothing is Hat with feather color is Brown it is used by 1 Outfits, they are:-
2022-05-01 08:55:15.298 D/DBINFO:   Outfit is Outfit2 it's ID is 2

通过 AppInspection 即存储在数据库中的数据

和映射table

额外重新分级@Relation

当您使用@Relation 时,将检索所有子项,而不管对象是什么,它们将按照适合查询优化器的顺序排列。如果您指定了 ORDER 或 WHERE 子句,这可以是 frustrating/confusing。

下面是一些示例查询,展示了

  • a) 你的查询很好,如果说在创建服装时你只想 select 上衣

  • b) 一个查询,您只想找到有上衣的服装并列出所有衣服(通过@Relation) -c) 一个查询,你想在其中找到有上衣的服装,但只列出上衣的服装(演示如何绕过@Relation get all children and get only some children)

  • 除了额外的 @Dao 函数和用于演示它们的 activity 代码外没有变化

所以额外的@Dao 函数是

@Transaction
@Query("SELECT * FROM outfit " +
        " JOIN outfitclothingmappingtable ON outfit.outfitId = outfitclothingmappingtable.outfitIdRef " +
        " JOIN clothing ON clothingIdRef = clothingId " +
        "WHERE clothing.type LIKE :searchQuery OR color LIKE :searchQuery")
fun getOutfitsWithClothingSearchingClothing(searchQuery: String): List<OutFitWithClothingList>
/* NOTE */
/* As this uses @Relation the outfits returned will contain ALL related clothing items */


/* Things can get a little complicated though due to @Relation */
/* Say you wanted a List of the Outfits that include  specific clothing and to only list those clothing items not ALL */
/* Then 2 queries and a final function that invokes the 2 queries is easiest */
/* However the first query (the actual SQL) has all the data but would need a loop to select apply the clothing to the outfits */
@Query("SELECT * FROM outfit " +
        " JOIN outfitclothingmappingtable ON outfit.outfitId = outfitclothingmappingtable.outfitIdRef " +
        " JOIN clothing ON clothingIdRef = clothingId " +
        "WHERE clothing.type LIKE :searchQuery OR color LIKE :searchQuery")
fun getOutfitsOnlySearchingClothing(searchQuery: String): List<Outfit>
@Query("SELECT * FROM outfitclothingmappingtable JOIN clothing ON clothingIdRef = clothingId WHERE (type LIKE :searchQuery OR color LIKE :searchQuery) AND outfitIdRef=:outfitId")
fun getClothingThatMatchesSearchForAnOutfit(searchQuery: String, outfitId: Long): List<Clothing>

@Transaction
@Query("")
fun getOutfitsWithOnlyClothingsThatMatchSearch(searchQuery: String): List<OutFitWithClothingList> {
    val rv = mutableListOf<OutFitWithClothingList>()
    val outfits = getOutfitsOnlySearchingClothing(searchQuery)
    for (o in outfits) {
        rv.addAll(listOf(OutFitWithClothingList(o,getClothingThatMatchesSearchForAnOutfit(searchQuery,o.id!!))))
    }
    return rv
}
  • 请注意,tablename.column 已被使用但并未普遍使用,仅当列名不明确时才需要 tablename.column(因此 @ColumnInfo(name = ??) 用于id 列,因此它们不是模棱两可的。
    • 如果列名不明确并且您使用 tablename.column 名称,则提取的列名将具有相同的名称,并且 Room 将 select 只有最后一个,因此 outfit.id 将是与 clothing.id 相同的值,再次通过使用唯一的列名来避免。
  • 所以 tablename.column 只是用来展示它的用途。

activity,为了演示,然后可以包括:-

    /* Your Query */
    for (c in dao.searchDatabase("Top")) {
        Log.d("SRCHINFO1","Clothing is ${c.description} ....")
    }

    /* @Relation Limited Search  complete outfit (all clothing) that has type of Top */
    for(owc in dao.getOutfitsWithClothingSearchingClothing("Top")) {
        Log.d("SRCHINFO2","Outfit is ${owc.outfit.outfitName}")
        for (c in owc.clothingList) {
            Log.d("SRCHINFO2c","Clothing is ${c.description} ....")
        }
    }

    /* Only the Outfits that match the search with the clothing that fits the search NOT ALL CLothing*/
    for(owc in dao.getOutfitsWithOnlyClothingsThatMatchSearch("Top")) {
        Log.d("SRCHINFO3","Outfit is ${owc.outfit.outfitName}")
        for (c in owc.clothingList) {
            Log.d("SRCHINFO3c","Clothing is ${c.description} ....")
        }
    }

输出将是(第一个运行):-

2022-05-01 13:31:52.485 D/SRCHINFO1: Clothing is Singlet ....


2022-05-01 13:31:52.488 D/SRCHINFO2: Outfit is Outfit1
2022-05-01 13:31:52.488 D/SRCHINFO2c: Clothing is Singlet ....
2022-05-01 13:31:52.488 D/SRCHINFO2c: Clothing is Shorts ....

2022-05-01 13:31:52.489 D/SRCHINFO2: Outfit is Outfit2
2022-05-01 13:31:52.489 D/SRCHINFO2c: Clothing is Singlet ....
2022-05-01 13:31:52.489 D/SRCHINFO2c: Clothing is Skirt ....
2022-05-01 13:31:52.489 D/SRCHINFO2c: Clothing is Hat with feather ....


2022-05-01 13:31:52.494 D/SRCHINFO3: Outfit is Outfit1
2022-05-01 13:31:52.494 D/SRCHINFO3c: Clothing is Singlet ....

2022-05-01 13:31:52.494 D/SRCHINFO3: Outfit is Outfit2
2022-05-01 13:31:52.494 D/SRCHINFO3c: Clothing is Singlet ....
  • 您的查询找到单峰
  • @Relation 查询找到 2 件使用 Singlet 的服装并列出所有服装
  • 最后一个查询找到了 2 件使用 Singlet 的 OutFits,但只列出了 Singlet 而不是所有其他服装(按需要)