Android Room数据库:如何在实体中处理ArrayList?

144

我刚刚实现了离线数据保存的Room功能。但是在一个Entity类中,我遇到了以下错误:

Error:(27, 30) error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.

以下是该类:

@Entity(tableName = "firstPageData")
public class MainActivityData {

    @PrimaryKey
    private String userId;

    @ColumnInfo(name = "item1_id")
    private String itemOneId;

    @ColumnInfo(name = "item2_id")
    private String itemTwoId;

    // THIS IS CAUSING THE ERROR... BASICALLY IT ISN'T READING ARRAYS
    @ColumnInfo(name = "mylist_array")
    private ArrayList<MyListItems> myListItems;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public ArrayList<MyListItems> getMyListItems() {
        return myListItems;
    }

    public void setCheckListItems(ArrayList<MyListItems> myListItems) {
        this.myListItems = myListItems;
    }

}

基本上我想要将ArrayList保存在数据库中,但我没有找到任何相关的内容。您能指导我如何使用Room保存数组吗?

注意:MyListItems Pojo类目前包含2个字符串。

21个回答

169

类型转换器是专门为此而设计的。在您的情况下,您可以使用下面给出的代码片段将数据存储在数据库中。

public class Converters {
    @TypeConverter
    public static ArrayList<String> fromString(String value) {
        Type listType = new TypeToken<ArrayList<String>>() {}.getType();
        return new Gson().fromJson(value, listType);
    }

    @TypeConverter
    public static String fromArrayList(ArrayList<String> list) {
        Gson gson = new Gson();
        String json = gson.toJson(list);
        return json;
    }
}

并在您的 Room DB 中像这样提及此类

@Database (entities = {MainActivityData.class},version = 1)
@TypeConverters({Converters.class})

更多信息在这里


2
有人能帮我用Kotlin和List实现相同的功能吗?在Java中它可以正常工作,但是当我将其转换为Kotlin时就无法工作了。 - Ozeetee
3
如何从那个ArrayList中查询数据? - Sanjog Shrestha
@CoolMind,请参考答案中提供的文章链接,以获取有关此转换器详细使用的信息。 - Amit Bhandari
@AshikAzeez 抱歉,我不知道,从未研究过。 - Amit Bhandari
1
@bompf 感谢您的建议。虽然这个例子只是举例说明。但通常我们总是在应用程序级别保留一个gson实例。 - Amit Bhandari
显示剩余14条评论

103
选项#1:将 MyListItems 设为一个实体@Entity,就像 MainActivityData 一样。在这种情况下,MyListItems 将设置一个回指到 MainActivityData@ForeignKey。然而,在 Room 中,实体不能引用其他实体,所以 MainActivityData 不能有私有的 ArrayList<MyListItems>。虽然如此,视图模型或类似的 POJO 结构可以拥有 MainActivityData 及其相关的 ArrayList<MyListItems>
选项#2:建立一对@TypeConverter 方法来转换 ArrayList<MyListItems> 到某个基本类型(例如,一个 String,比如使用 JSON 作为存储格式)。现在,MainActivityData 可以直接拥有它的 ArrayList<MyListItems>。然而,MyListItems 没有单独的表格,因此无法很好地查询 MyListItems

@TusharGogna:关于关系,请查看Room文档,而"实体不直接引用其他实体"的部分也在Room文档中有介绍。 - CommonsWare
@CommonsWare 是 Relation 在这种情况下的第三个选项吗?关系文档 - FeleMed
@FeleMed:并不是这样的。@Relation 只用于从数据库中获取数据,与将数据存入数据库无关。 - CommonsWare
1
只是提醒一下,如果你要持久化一个 Int 列表,那么对于选项 2,你需要将其序列化为字符串。这会使查询变得更加复杂。我宁愿选择选项 1,因为它不太依赖于“类型”。 - reixa
你可以阅读以下替代方案:https://android.jlelse.eu/android-architecture-components-room-relationships-bf473510c14a - Jose Pose S
7
将来有时您可能需要查询您的物品,因此我通常会选择选项#1。 - Jeffrey

78

Kotlin类型转换器版本:

 class Converters {

    @TypeConverter
    fun listToJson(value: List<JobWorkHistory>?) = Gson().toJson(value)

    @TypeConverter
    fun jsonToList(value: String) = Gson().fromJson(value, Array<JobWorkHistory>::class.java).toList()
}

我使用了JobWorkHistory对象来达到我的目的,你可以使用自己的对象。

@Database(entities = arrayOf(JobDetailFile::class, JobResponse::class), version = 1)
@TypeConverters(Converters::class)
abstract class MyRoomDataBase : RoomDatabase() {
     abstract fun attachmentsDao(): AttachmentsDao
}

3
我认为,与其将数据反序列化为数组,然后再转换为List,不如像Amit在下面的答案中提到的那样使用List类型,例如:val listType = object : TypeToken<List<JobWorkHistory>>() {}.type。 - Sohayb Hassoun
3
另外,你可能想要从你的应用程序中的某个地方获取已缓存的Gson实例。在每次调用时初始化新的Gson实例可能会很昂贵。 - Apsaliya

37

使用 Kotlin 的序列化组件 kotlinx.serialization 实现本地 Kotlin 版本。

  1. 将 Kotlin 序列化 Gradle 插件和依赖项添加到您的 build.gradle 文件中:

apply plugin: 'kotlinx-serialization'

dependencies {
   ...
   implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
}
  1. 将类型转换器添加到您的Converter类中;
class Converters {
   @TypeConverter
   fun fromList(value : List<String>) = Json.encodeToString(value)

   @TypeConverter
   fun toList(value: String) = Json.decodeFromString<List<String>>(value)
}
  1. 将您的转换器类添加到数据库类中:
@TypeConverters(Converters::class)
abstract class YourDatabase: RoomDatabase() {...}

你搞定了!

额外资源:


2
如果自动导入无法使用,请添加以下代码:import kotlinx.serialization.json.Jsonimport kotlinx.serialization.decodeFromStringimport kotlinx.serialization.encodeToString - Fortran
6
您可能还需要 classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion") - Lennoard Silva

26
更好的List转换器
class StringListConverter {
    @TypeConverter
    fun fromString(stringListString: String): List<String> {
        return stringListString.split(",").map { it }
    }

    @TypeConverter
    fun toString(stringList: List<String>): String {
        return stringList.joinToString(separator = ",")
    }
}

26
注意不要使用逗号作为分隔符,因为有时你的字符串中可能会有相同的字符,这可能会导致混乱。 - emarshah

21

Kotlin答案

您需要完成以下3个步骤:

  1. 创建转换器类。
  2. 在数据库中添加转换器类。
  3. 只需在实体类中定义您想要使用的内容。

使用示例逐步说明:

步骤1:

 class Converters {

    @TypeConverter
    fun listToJsonString(value: List<YourModel>?): String = Gson().toJson(value)

    @TypeConverter
    fun jsonStringToList(value: String) = Gson().fromJson(value, Array<YourModel>::class.java).toList()
}

第二步:

@Database(entities = [YourEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class YourDatabase : RoomDatabase() {
     abstract fun yourDao(): YourDao
}

第三步:

注意:您不需要调用转换器的listToJsonString()jsonStringToList()函数。它们被Room在后台使用。

@Entity(tableName = "example_database_table") 
data class YourEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  @ColumnInfo(name = "your_model_list") var yourModelList: List<YourModel>,
)

1
这对我有用。谢谢@Caner - portfoliobuilder

16

个人建议不要使用@TypeConverters/序列化,因为它们会破坏数据库的正常形式规范。

对于这种情况,可以考虑使用定义关系来使用@Relation注解,它允许将嵌套实体查询到单个对象中,而无需声明@ForeignKey并手动编写所有SQL查询的复杂性:

@Entity
public class MainActivityData {
    @PrimaryKey
    private String userId;
    private String itemOneId;
    private String itemTwoId;
}

@Entity
public class MyListItem {
    @PrimaryKey
    public int id;
    public String ownerUserId;
    public String text;
}

/* This is the class we use to define our relationship,
   which will also be used to return our query results.
   Note that it is not defined as an @Entity */
public class DataWithItems {
    @Embedded public MainActivityData data;
    @Relation(
        parentColumn = "userId"
        entityColumn = "ownerUserId"
    )
    public List<MyListItem> myListItems;
}

/* This is the DAO interface where we define the queries.
   Even though it looks like a single SELECT, Room performs
   two, therefore the @Transaction annotation is required */
@Dao
public interface ListItemsDao {
    @Transaction
    @Query("SELECT * FROM MainActivityData")
    public List<DataWithItems> getAllData();
}

除了这个1-N的例子,也可以定义1-1和N-M的关系。


2
这里唯一明智的答案!不要违反第一范式! - Luke Needham
我喜欢使用1到N关系的这个解决方案。不过有一个问题,如果你有一个包含一些数据的JSON文件,并且想将其存储在数据库中,与使用Gson的ArrayList方法可以轻松创建持有数据的对象实例不同,你如何使用这种数据结构来实现呢? - Emmanuel Murairi
1
@EmmanuelMurairi 很抱歉,您不能这样做。为了在运行时实例化任意对象,Gson使用反射 - 您也可以使用- 但是,由于Room基于关系数据库(SQLite),它将数据结构化为具有预定义列的表格,因此您需要知道数据的结构并提前声明实体类。当您使用Gson时,您只需将一个巨大的字符串转储到单个列中,并在每次读取时解析它。这是一个不错的解决方法,但我尽量避免使用它。 - Albert Gomà
有时候你应该这样做,有时候你不应该这样做,这取决于你是否需要操作它并在查询中使用它。 - EpicPandaForce
1
@EpicPandaForce 当然,有时反规范化可以带来更好的性能,许多分布式系统也利用了它。然而,应该记住,应用要求可能会随着新版本的推出而发生变化(重新规范化非规范化的架构可能真的很麻烦),类型转换本身就是一个操作,如果不是严格必要的话,可能会耗费资源(和电池)。只有在确切知道自己在做什么的情况下才进行反规范化。 - Albert Gomà

11

这是我处理列表转换的方法

public class GenreConverter {
@TypeConverter
public List<Integer> gettingListFromString(String genreIds) {
    List<Integer> list = new ArrayList<>();

    String[] array = genreIds.split(",");

    for (String s : array) {
       if (!s.isEmpty()) {
           list.add(Integer.parseInt(s));
       }
    }
    return list;
}

@TypeConverter
public String writingStringFromList(List<Integer> list) {
    String genreIds = "";
    for (int i : list) {
        genreIds += "," + i;
    }
    return genreIds;
}}

然后在数据库中按照以下方式操作

@Database(entities = {MovieEntry.class}, version = 1)
@TypeConverters(GenreConverter.class)

下面是相同功能的 Kotlin 实现:

class GenreConverter {
@TypeConverter
fun gettingListFromString(genreIds: String): List<Int> {
    val list = mutableListOf<Int>()

    val array = genreIds.split(",".toRegex()).dropLastWhile {
        it.isEmpty()
    }.toTypedArray()

    for (s in array) {
        if (s.isNotEmpty()) {
            list.add(s.toInt())
        }
    }
    return list
}

@TypeConverter
fun writingStringFromList(list: List<Int>): String {
    var genreIds=""
    for (i in list) genreIds += ",$i"
    return genreIds
}}

我使用这个解决方案来处理简单类型(例如List<Integer>,List<Long>),因为它比基于gson的解决方案更轻巧。 - Julien Kronegg
3
这个解决方案缺少不幸的情况(例如,null 和空字符串,null 列表)。 - Julien Kronegg
是的,我犯了一个错误,复制粘贴了这个代码,并且浪费了至少一个小时在单元素列表上,因为它们使用单引号创建元素。我已经提交了一个修复这个问题的答案(使用 Kotlin)。 - Daniel Wilson

6

我看到了与上述相同的错误信息。

我想补充一点:如果您在@Query中遇到此错误消息,应在@Query注释上方添加@TypeConverters。

例如:

@TypeConverters(DateConverter.class)
@Query("update myTable set myDate=:myDate  where id = :myId")
void updateStats(int myId, Date myDate);

....

public class DateConverter {

    @TypeConverter
    public static Date toDate(Long timestamp) {
        return timestamp == null ? null : new Date(timestamp);
    }

    @TypeConverter
    public static Long toTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}

1
我尝试在查询注释上方添加@TypeConverters,但仍然出现相同的错误。 - zulkarnain shah

4

这篇回答使用Kotlin通过逗号进行拆分并构造逗号分隔的字符串。逗号需要放在除最后一个元素之外的所有元素的末尾,因此它也可以处理单元素列表。

object StringListConverter {
        @TypeConverter
        @JvmStatic
        fun toList(strings: String): List<String> {
            val list = mutableListOf<String>()
            val array = strings.split(",")
            for (s in array) {
                list.add(s)
            }
            return list
        }

        @TypeConverter
        @JvmStatic
        fun toString(strings: List<String>): String {
            var result = ""
            strings.forEachIndexed { index, element ->
                result += element
                if(index != (strings.size-1)){
                    result += ","
                }
            }
            return result
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接