7、类型转换器:@TypeConverter注解、自定义转换器、存储Date/List等复杂类型
Room 数据库能存什么?说白了,它只能存基本类型——Int、Long、String、Boolean 这些。但咱们实际开发中,谁还没个 Date、List、Map 要存呢?
我刚开始用 Room 时,就踩过这个坑。当时想把一个用户收藏的商品 ID 列表存进数据库,心想 Room 这么智能,应该能自动处理吧?结果编译直接报错,提示我「不知道如何把 List 存进 SQLite」。嗯,那时候我才意识到,Room 不是万能的,它需要咱们帮它一把。
这个「帮一把」的工具,就是类型转换器(TypeConverter)。
核心思想:类型转换器就像一座桥,把 Kotlin 的复杂类型「翻译」成 Room 能存的基本类型,读取时再「翻译」回来。
7.1 @TypeConverter 注解:转换器的入口
@TypeConverter 是 Room 提供的一个注解,用来标记「转换方法」。你想想看,一个方法负责把复杂类型转成基本类型(比如 Date → Long),另一个方法负责把基本类型转回复杂类型(Long → Date)。
我个人习惯把转换器定义成静态方法,放在一个单独的类里。这样结构清晰,也好维护。
class Converters {
@TypeConverter
fun fromDateToLong(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromLongToDate(timestamp: Long?): Date? {
return timestamp?.let { Date(it) }
}
}
注意看,这里用了可空类型。为什么?因为数据库字段可能是 NULL,你总不能让转换器在遇到 null 时崩溃吧。
定义好转换器类之后,还需要告诉 Room 去用它。在 @Database 注解里加上 typeConverters 参数:
@Database(
entities = [User::class],
version = 1,
typeConverters = [Converters::class]
)
abstract class AppDatabase : RoomDatabase() {
// ...
}
这一步很容易忘。我记得有次重构项目,加了个新转换器,结果忘了注册,Room 直接报错说找不到转换方法。排查了半天才发现是注册漏了。
小技巧:转换器类可以定义在数据库类内部,也可以单独文件。我个人推荐单独文件,因为随着项目变大,转换器会越来越多,单独文件更好管理。
7.2 自定义转换器:存储 Date 类型
Date 类型是咱们最常遇到的复杂类型。存 Date 有几种方案?我简单列一下:
| 方案 | 存储方式 | 优点 | 缺点 |
|---|---|---|---|
| 时间戳(Long) | 毫秒值 | 精度高、排序方便 | 可读性差 |
| 字符串(String) | yyyy-MM-dd HH:mm:ss | 可读性好 | 排序需转换、有格式问题 |
| ISO 8601 字符串 | 2025-01-15T10:30:00Z | 国际化友好 | 长度较长 |
我个人最常用的是时间戳方案。为什么?因为排序快、精度高、没有时区问题。你想想看,如果存字符串,不同时区的用户存进去的时间格式可能不一样,查起来就麻烦了。
上面已经给出了 Date ↔ Long 的转换代码,这里再补充一个带时区处理的版本:
class DateConverters {
@TypeConverter
fun fromDateToLong(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun fromLongToDate(timestamp: Long?): Date? {
return timestamp?.let {
// 这里可以加时区转换逻辑
Date(it)
}
}
}
嗯,这里要注意:时间戳是 UTC 的,如果你的业务需要特定时区显示,建议在 ViewModel 或 UI 层做转换,不要在转换器里做。转换器只负责「存」和「取」,不负责「展示」。
7.3 存储 List 类型:最常用的复杂类型
List 怎么存?Room 不认识 List,但认识 String。所以思路很简单:把 List 转成 JSON 字符串存进去,取出来时再解析回 List。
这里需要引入 Gson 或 Moshi 这样的 JSON 库。我个人习惯用 Gson,轻量、够用。
class ListConverters {
private val gson = Gson()
@TypeConverter
fun fromStringList(list: List<String>?): String? {
return list?.let { gson.toJson(it) }
}
@TypeConverter
fun toStringList(json: String?): List<String>? {
return json?.let {
val type = object : TypeToken<List<String>>() {}.type
gson.fromJson(it, type)
}
}
}
这段代码里有个关键点:TypeToken。为什么需要它?因为 Java 的泛型在运行时会被擦除,Gson 不知道你要解析成 List<String> 还是 List<Integer>。TypeToken 就是用来保留这个类型信息的。
我在项目中遇到过一个问题:存 List 时一切正常,但取出来时发现类型不对,报 ClassCastException。排查后发现是 TypeToken 写错了,泛型参数没对上。从那以后,我每次写 List 转换器都会再三检查 TypeToken 的类型。
避坑指南:千万不要在转换器里做复杂的业务逻辑。我曾经见过有人把网络请求也写在转换器里,结果数据库读取时卡了好几百毫秒。转换器应该只做「格式转换」,不做「业务处理」。
7.4 存储 Map 类型:键值对也能存
Map 的存储思路和 List 一样,也是转 JSON。但 Map 有个特殊之处:它的 key 和 value 类型都可能变化。
class MapConverters {
private val gson = Gson()
@TypeConverter
fun fromStringMap(map: Map<String, String>?): String? {
return map?.let { gson.toJson(it) }
}
@TypeConverter
fun toStringMap(json: String?): Map<String, String>? {
return json?.let {
val type = object : TypeToken<Map<String, String>>() {}.type
gson.fromJson(it, type)
}
}
}
如果你要存 Map<Int, String> 或者 Map<String, List<Int>>,只需要改 TypeToken 里的泛型参数就行。但要注意,JSON 的 key 只能是 String,所以如果你的 key 是 Int,Gson 会自动转成字符串,取出来时再转回 Int。
7.5 多个转换器如何组织?
当项目变大,转换器会越来越多。Date 一个、List 一个、Map 一个、自定义实体又一个……全堆在一个类里会变得很乱。
我的做法是按类型分组:
DateConverters:处理所有日期相关ListConverters:处理所有列表相关MapConverters:处理所有映射相关CustomTypeConverters:处理自定义实体类
然后在 @Database 注解里一次性注册:
@Database(
entities = [User::class, Order::class],
version = 1,
typeConverters = [
DateConverters::class,
ListConverters::class,
MapConverters::class,
CustomTypeConverters::class
]
)
abstract class AppDatabase : RoomDatabase() {
// ...
}
这样每个类职责单一,改一个不会影响另一个。而且新同事接手代码时,一眼就能看出有哪些转换器。
7.6 性能与注意事项
类型转换器虽然方便,但也不是没有代价。每次读写数据,Room 都会调用转换器方法。如果转换器里做了耗时操作(比如 JSON 解析大对象),就会拖慢数据库操作。
我建议你注意这几点:
- 避免大对象:不要在实体里存一个巨大的 List 或 Map。如果数据量超过几百条,建议单独建表。
- 避免嵌套太深:List<List<String>> 这种嵌套结构,JSON 解析会变慢,而且可读性差。
- 考虑使用 Room 的 @Embedded:如果复杂类型是固定的几个字段,用 @Embedded 比用转换器更高效。
- 测试转换器:写单元测试验证转换逻辑,尤其是 null 和空列表的情况。
个人经验:我曾经把一个用户的所有操作日志(上千条)存成 JSON 字符串放在一个字段里。结果查询时卡得不行。后来改成单独建表,用外键关联,性能提升了 10 倍。所以,转换器虽好,但别滥用。
7.7 本章知识体系
下面这张图帮你梳理了类型转换器的核心逻辑:
类型转换器是 Room 中一个非常实用的功能。它让咱们能用 Kotlin 的丰富类型来建模,同时又不用牺牲 SQLite 的存储效率。只要记住「存时转成基本类型,取时转回复杂类型」这个核心原则,大部分场景都能轻松应对。
嗯,这一章的内容就到这里。如果你在实际项目中遇到什么奇怪的转换问题,欢迎来交流。
公众号:蓝海资料掘金营,微信deep3321