Home > Software design >  Data type false when saving to Room
Data type false when saving to Room

Time:05-06

I am writing an android application in Kotlin. Trying to save data to local storage using room. But I have an error: "Cannot figure out how to save this field into database. You can consider adding a type converter for it".

I tried to make a converter to a string according to the documentation and with the help of examples on Stack Overflow. But still I have the same error. Please tell me how can I avoid it. This is my class where I form the structure:

    data class Request(
    val method: String?,
    val url: String?,
    val headers: Map<String, List<String>>?,
    val body: String,
    val size: Int,
)

data class Response(
    val code: Int?,
    val headers: Map<String, List<String>>?,
    val body: String?,
    val size: Int?,
)

@Entity
data class Packet(
    @PrimaryKey val id: Int,
    val userId: String,
    val deviceId: String,
    val sessionId: String,
    val timestamp: Long?,
    val duration: Int?,
    val protocol: String?,
    @Embedded val request: Request?,
    @Embedded val response: Response?,
)

And this is how I save the data

    @Database(
    version = 1,
    entities = [Packet::class]
)

abstract class NetworkDatabase : RoomDatabase() {
    abstract fun packetDao(): PacketDao
}

CodePudding user response:

SQLite (the relational database to which room is a wrapper) is limited to a table (@Entity annotated class) have columns that can contain a single value which is limited to a small set of types (TEXT(String) INTEGER(Int,Long,....) REAL(Float,Double,....) BLOB(ByteArray) and NUMERIC (not usable by Room))

As Such you Map<String, List<String>> cannot be stored directly. You need to convert it to a single value or more correctly, in the case of a list, in database terms another table. However, it is often that, using a table is, forsaken for the more convenient conversion to a single value and typically a JSON string that represents the object(s) as a string.

Room facilitates the use of TypeConverters by utilising two annotations @TypeConverter and @TypeConverters

  • The former precedes functions that do the conversion from an object to a supported value and always a complimentary function that does the reverse.
  • The second defines the classes that contain the function and their scope.

So you need to have @TypeConvert annotated functions and an @TypeConverters with suitable scope (at the @Database level gives the most encompassing scope).

As you are Embedding the Request and Response classes within the Packet (table). You would also face issues with column naming as headers, body and size variables/fields have the same name.

Furthermore, List,Maps and Arrays are a little more difficult to handle if using the most convenient conversion using JSON Strings. A holding class can simplify matters.

1). As JSON is going to be utilised to convert from objects to a JSON string, you should add a dependency for com.google.code.gson e.g. implementation 'com.google.code.gson:gson:2.9.0' to your build gradle (Module).

  • there are alternatives

2). Amend the classes so that variable/field names will not duplicate column names and additionally introduce a holding class for the Maps (one would suit for both Response and Request classes). So the classes could be :-

/* Holding Class */
data class Mapping (
    val header: Map<String, List<String>>
)

/* Note changed column names */
data class Request(
    val requestMethod: String?,
    val requestUrl: String?,
    val requestHeaders: Mapping?,
    val requestBody: String,
    val requestSize: Int
)

/* Note changed column names */
data class Response(
    val responseCode: Int?,
    val responseHeaders: Mapping?,
    val responseBody: String?,
    val responseSize: Int?
)

@Entity
data class Packet(
    @PrimaryKey val id: Int,
    val userId: String,
    val deviceId: String,
    val sessionId: String,
    val timestamp: Long?,
    val duration: Int?,
    val protocol: String?,
    @Embedded val request: Request?,
    @Embedded val response: Response?
)

3). Add a class for the TypeConverter functions that convert the holding (Mapping) class to/from a JSON string representation of the object e.g. :-

class RoomTypeConverters {

    @TypeConverter
    fun fromMappingToJSONString(mapping: Mapping): String {
        return Gson().toJson(mapping)
    }
    @TypeConverter
    fun fromJSONStringToMapping(jsonString: String): Mapping {
        return Gson().fromJson(jsonString,Mapping::class.java)
    }
}

4). Add the @TypeConverters annotation with suitable scope, full scope is at the @Database level, so the @Database annotated class could be :-

@TypeConverters(value = [RoomTypeConverters::class]) //<<<<<<<<<< defines the classes that include the @TypeConverter annotated functions
@Database(entities = [Packet::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    ....
}

Using the above then the build succeeds e.g.

Executing tasks: [:app:assembleDebug] in project E:\AndroidStudioApps\SO72128161KotlinRoomTypeConverters

> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:kaptGenerateStubsDebugKotlin
> Task :app:kaptDebugKotlin
> Task :app:compileDebugKotlin
> Task :app:compileDebugJavaWithJavac
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:dexBuilderDebug
> Task :app:mergeProjectDexDebug
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect UP-TO-DATE
> Task :app:assembleDebug

BUILD SUCCESSFUL in 3s
33 actionable tasks: 7 executed, 26 up-to-date

Build Analyzer results available

And additionally you can see that the table packet table will be created using the SQL (as generated by Room in the generated java code) as per :-

_db.execSQL("CREATE TABLE IF NOT EXISTS `Packet` (`id` INTEGER NOT NULL, `userId` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `sessionId` TEXT NOT NULL, `timestamp` INTEGER, `duration` INTEGER, `protocol` TEXT, `requestMethod` TEXT, `requestUrl` TEXT, `requestHeaders` TEXT, `requestBody` TEXT, `requestSize` INTEGER, `responseCode` INTEGER, `responseHeaders` TEXT, `responseBody` TEXT, `responseSize` INTEGER, PRIMARY KEY(`id`))");
  • Related