You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PowerSync对接Supabase时CRUD上传序列化错误求助

问题

通过PowerSync将移动端应用对接Supabase,已完成登录且曾成功上传过数据,但目前客户端创建/更新数据时无法同步至Supabase,报错:

Error uploading crud: Serializer for subclass 'SerializedRow' is not found in the polymorphic scope of 'SqliteRow'.
Check if class with serial name 'SerializedRow' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'SerializedRow' has to be '@Serializable', and the base class 'SqliteRow' has to be sealed and '@Serializable'.

推测是序列化问题,尝试将id、userId、date从UUID/LocalDate改为String未解决,相关代码如下:

序列化对象代码

object UUIDSerializer : KSerializer<UUID> {
    override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): UUID {
        return UUID.fromString(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: UUID) {
        encoder.encodeString(value.toString())
    }
}

object LocalDateSerializer : KSerializer<LocalDate> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("_root_ide_package_.java.time.LocalDate", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: LocalDate) {
        encoder.encodeString(value.toString())
    }

    override fun deserialize(decoder: Decoder): LocalDate =
        LocalDate.parse(decoder.decodeString())

}

Task实体类代码

@Serializable
data class Task (
//    @Serializable(with = UUIDSerializer::class)
//    val id: UUID,
    val id: String,
    // 2026-04-21 08:15:34.082732+00 - this is always stored at UTC time zone
    @SerialName("created_at")
    val createdAt: String? = null,
    @SerialName("user_id")
//    @Serializable(with = UUIDSerializer::class)
//    val userId: UUID,
    val userId: String,
//    @Serializable(with = LocalDateSerializer::class)
//    val date: LocalDate? = null,
    // 2026-04-22
    val date: String? = null,
    @SerialName("start_time")
    // 10:14:00+02 - this is stored with the local time zone
    var startTime: String? = null,
    @SerialName("end_time")
    // 10:58:00+02
    var endTime: String? = null,
    var title: String,
    var description: String? = null,
    var priority: Int? = null,
    val pinned: Boolean = false
)

TaskDAO代码

@Singleton
class TaskDAO @Inject constructor(
    private val dataSource: OnlineSyncDataSource
) {
    suspend fun insertTask(task: Task) {
        try {
            dataSource.getDatabase().writeTransaction { transaction ->
                transaction.execute(
                    sql = "INSERT INTO tasks (id, created_at, user_id, date, start_time, end_time, title, description, priority, pinned) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                    parameters = listOf(
                        task.id,
                        task.createdAt,
                        task.userId,
                        task.date,
                        task.startTime,
                        task.endTime,
                        task.title,
                        task.description,
                        task.priority,
                        task.pinned
                    )
                )
            }
        } catch (e: Exception) {
            Log.e("TaskDAO", e.printStackTrace().toString())
        }
    }

    suspend fun updateTask(task: Task) {
        try {
            dataSource.getDatabase().execute(
                sql = "UPDATE tasks SET date = ?, start_time = ?, end_time = ?, title = ?, description = ?, priority = ?, pinned = ? WHERE id = ?",
                parameters = listOf(
                    task.date,
                    task.startTime,
                    task.endTime,
                    task.title,
                    task.description,
                    task.priority,
                    task.pinned,
                    task.id
                )
            )
        } catch (e: Exception) {
            Log.e("TaskDAO", e.printStackTrace().toString())
        }
    }
}

TasksRepository代码

@Singleton
class TasksRepository @Inject constructor(
    private val dao: TaskDAO
) {
    suspend fun insertTask(task: Task): Unit {
        return dao.insertTask(task)
    }

    suspend fun updateTask(task: Task): Unit {
        return dao.updateTask(task)
    }
}

PowerSync数据库Schema代码

val AppSchema: Schema = Schema(
    listOf(
        Table(
            name = "tasks",
            columns = listOf(
                Column.text("created_at"),
                Column.text("user_id"),
                Column.text("date"),
                Column.text("start_time"),
                Column.text("end_time"),
                Column.text("title"),
                Column.text("description"),
                Column.text("priority"),
                Column.text("pinned")
            )
        )
    )
)

OnlineSyncDataSource代码

@OptIn(DelicateCoroutinesApi::class)
@Singleton
class OnlineSyncDataSource @Inject constructor(
    private val powerSyncDatabase: PowerSyncDatabase,
    private val supabaseClient: SupabaseClient
) {
    val dotenv = dotenv()

    init {
        GlobalScope.launch {
            powerSyncDatabase.connect(SupabaseConnector(
                supabaseClient,
                dotenv["POWERSYNC_URL"]
            ))
        }
    }
    fun getDatabase(): PowerSyncDatabase {
        return powerSyncDatabase
    }
}
解决方案

这个错误的核心是PowerSync同步时的序列化配置缺失,加上Schema与数据类型不匹配导致的,按以下步骤修正:

1. 注册多态序列化模块

PowerSync依赖Kotlinx Serialization处理数据同步,需显式注册SerializedRow到多态序列化模块。在应用初始化代码(如Application类)中添加:

import androidx.powersync.sqlite.SerializedRow
import androidx.powersync.sqlite.SqliteRow
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import kotlinx.serialization.json.Json

// 全局序列化模块
val appSerializersModule = SerializersModule {
    polymorphic(SqliteRow::class) {
        subclass(SerializedRow::class)
    }
}

// 配置Json序列化器
val json = Json {
    serializersModule = appSerializersModule
    ignoreUnknownKeys = true
}

创建PowerSyncDatabase时传入该序列化器:

val powerSyncDatabase = PowerSyncDatabase.Builder(context, AppSchema)
    .serializer(json)
    .build()

2. 修正PowerSync Schema的类型与结构

你的Schema存在两个关键错误:

  • 缺失主键id列,PowerSync需要主键跟踪数据变更
  • priority、pinned字段类型与实体类不匹配

修改后的Schema:

val AppSchema: Schema = Schema(
    listOf(
        Table(
            name = "tasks",
            columns = listOf(
                Column.text("id"),
                Column.text("created_at").nullable(),
                Column.text("user_id"),
                Column.text("date").nullable(),
                Column.text("start_time").nullable(),
                Column.text("end_time").nullable(),
                Column.text("title"),
                Column.text("description").nullable(),
                Column.integer("priority").nullable(), // 匹配实体类Int?类型
                Column.boolean("pinned") // 匹配实体类Boolean类型
            ),
            primaryKey = listOf("id") // 明确主键
        )
    )
)

3. 修正DAO中的参数空值处理

避免传入null导致SQL执行异常,在DAO的参数列表中显式处理空值:

// insertTask的parameters修改示例
parameters = listOf(
    task.id,
    task.createdAt,
    task.userId,
    task.date,
    task.startTime,
    task.endTime,
    task.title,
    task.description,
    task.priority,
    task.pinned
)

如果数据库不允许某些字段为null,可设置默认值(如task.description ?: ""),同时对应Schema去掉.nullable()。

4. 优化PowerSync初始化逻辑

避免使用GlobalScope,改用应用级别CoroutineScope:

@OptIn(DelicateCoroutinesApi::class)
@Singleton
class OnlineSyncDataSource @Inject constructor(
    private val powerSyncDatabase: PowerSyncDatabase,
    private val supabaseClient: SupabaseClient,
    private val applicationScope: CoroutineScope
) {
    val dotenv = dotenv()

    init {
        applicationScope.launch {
            try {
                powerSyncDatabase.connect(SupabaseConnector(
                    supabaseClient,
                    dotenv["POWERSYNC_URL"]
                ))
            } catch (e: Exception) {
                Log.e("PowerSync", "连接失败", e)
            }
        }
    }
    fun getDatabase(): PowerSyncDatabase {
        return powerSyncDatabase
    }
}

5. 重置数据库(可选)

若之前的Schema错误导致本地数据库结构异常,可卸载应用重新安装,或在开发环境调用powerSyncDatabase.reset()重置数据库,确保新Schema生效。

内容的提问来源于stack exchange,提问作者Joyce Opio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 17:14:53