Avatar ·

Ошибка "Cannot find setter for field" в Room с Kotlin: причины и решение

Привет, коллеги! Я новичок в Kotlin и пытаюсь использовать Room для локального хранения данных. У меня есть простая сущность User: ```kotlin @Entity(tableName = "users") data class User( @PrimaryKey val id: Int, val name: String, val age: Int ) ``` И DAO: ```kotlin @Dao interface UserDao { @Insert suspend fun insert(user: User) @Query("SELECT * FROM users WHERE id = :id") suspend fun getUser(id: Int): User? } ``` Но когда я запускаю приложение, получаю ошибку: ``` Cannot find setter for field - User.name ``` Я читал, что Room требует сеттеры для полей, но у меня же data class — они автоматически генерируются. В чем может быть проблема? Я использую Room версии 2.5.0 и Kotlin 1.8.0. Мой Gradle файл настроен с kapt, аннотации вроде бы подключены. Подскажите, пожалуйста, что я делаю не так? Кстати, если я меняю val на var — ошибка исчезает. Но я хочу, чтобы поля были неизменяемыми (immutable). Можно ли как-то обойти это без var? Заранее спасибо за помощь! <<>>

Почему возникает ошибка?

Проблема в том, что Room для работы с сущностями использует рефлексию и требует наличия сеттеров (setter methods) для всех полей, которые будут загружаться из базы данных. В Kotlin data class с val генерирует только геттеры, а сеттеры создаются только для var. Когда Room пытается восстановить объект из курсора (например, при выполнении SELECT), он ищет метод setName(...), но не находит его — отсюда и ошибка.

Решение: используйте var или конструктор с полями по умолчанию

Есть два основных подхода:

1. Использовать var (самый простой способ)

Room может работать с var-полями. Это безопасно, если вы не экспортируете объекты за пределы базы данных без копирования:

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    var name: String,  // var вместо val
    var age: Int       // var вместо val
)

2. Использовать val с конструктором по умолчанию (рекомендуемый способ)

Room может работать с неизменяемыми полями, если предоставить пустой конструктор или конструктор со значениями по умолчанию. Компилятор Kotlin + Room обработает это через рефлексию:

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    val name: String = "",  // значение по умолчанию
    val age: Int = 0        // значение по умолчанию
)

Room создаст экземпляр класса через пустой конструктор (который Kotlin генерирует автоматически, если все поля имеют значения по умолчанию), а затем заполнит поля через рефлексию, игнорируя сеттеры. Это работает, потому что Room напрямую пишет в backing field объекта.

Важные нюансы

  • Для @Ignore полей — если поле помечено @Ignore, Room не требует сеттера, так как не загружает его из БД.
  • Вложенные объекты — если используете @Embedded, то вложенный объект тоже должен иметь сеттеры или конструктор по умолчанию.
  • TypeConverters — если вы используете конвертер типов, то поле должно быть var, иначе Room не сможет присвоить конвертированное значение.

Полный рабочий пример

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    val name: String = "",
    val age: Int = 0
)

@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User)

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUser(id: Int): User?
}

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

Теперь ваш код будет работать без ошибок, а поля останутся неизменяемыми (immutable) после создания объекта. Room сам позаботится о заполнении полей через рефлексию.

<<>>

Альтернативный подход: использование @JvmField

Если вы хотите сохранить val и не задавать значения по умолчанию, можно использовать аннотацию @JvmField. Она заставляет Kotlin-компилятор генерировать поле без приватного модификатора, что позволяет Room обращаться к нему напрямую:

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    @JvmField val name: String,
    @JvmField val age: Int
)

Плюсы: поля остаются val (неизменяемыми), не нужно задавать значения по умолчанию.

Минусы:

  • @JvmField делает поле публичным (public), что может нарушить инкапсуляцию.
  • Не работает с некоторыми плагинами, которые анализируют байт-код (например, ProGuard/R8 может удалить поле).
  • Может вызвать проблемы с сериализацией (Gson, Moshi и т.д.), если они ожидают стандартные Kotlin-свойства.

Я рекомендую использовать первый вариант (значения по умолчанию) — он более идиоматичен для Kotlin и не имеет побочных эффектов. Но если вам критично избежать значений по умолчанию, @JvmField — рабочий вариант.