tldr;

У тебя класс без data!

Да, неплохой такой класс

Использовать data class для JPA сущности оправдано, если id записи генерится на стороне приложения и избыточно, если id генерится на стороне базы данных, так как придется переопределять методы equals и hashcode.

Подробности

Есть класс, отмеченный аннотацией @Entity и @Table. Нужно ли добавлять data перед class?

@Table
@Entity
class Entity(
   @Id
   val id: SomeIdentityType,
   @Column
   val name: String
)

Ответ на вопрос зависит от того, где генерится id, но вначале разберемся с data class. Data class были созданы для тех, кому лень переопределять методы. Ниже написан data class и представлен bytecode.

data class D(val name: String)

Из bytecode я удалил "лишнее" и оставил только необходимое для дальнейших рассуждений.

public static final class D {
   @NotNull
   private final String name;

   @NotNull
   public final String getName() {
      return this.name;
   }

   @NotNull
   public String toString() {
      return "D(name=" + this.name + ")";
   }

   public int hashCode() {
      String var10000 = this.name;
      return var10000 != null ? var10000.hashCode() : 0;
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Scratch_7.D) {
            Scratch_7.D var2 = (Scratch_7.D)var1;
            if (Intrinsics.areEqual(this.name, var2.name)) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

Таким образом, data перед class это указание компилятору переопределить методы toString(), equals(), hashCode() на основании данных полей конструктора.

JPA Entity это объекты, которые отображают поля объекта на строки в таблице базе данных. На первый взгляд data класс это то, что нужно, но тут есть несколько моментов (не нюансов). По сути все крутится вокруг поля, которое отображается на "первичный ключ" (id). Id может быть сгенерирован как со стороны приложения, так и со стороны базы данных. В первом случае нужен тип, который однозначно идентифицирует запись и примером такого типа может быть UUID.

@Table
@Entity
data class EntityApp(
    @Id
    val id: UUID,
    @Column
    var name: String
)

@Test
fun `test app generated id`() {
    val name = "имя"
    val id = UUID.randomUUID()
    val (entity1, entity2) = EntityApp(id, name) to EntityApp(id, name)

    val m = mutableMapOf<EntityApp, String>()
    m[entity1] = "Найден"

    println("До сохранения в б.д.")
    println("${entity1 == entity2}") // true

    val result = entityAppRepository.save(entity1)

    println("После сохранения в б.д.")
    println("${entity1 == result}") // true
    println("${entity1 == entity2}") // true
    println(m[entity1] ?: "Не найден") // Найден
}

Получается, что написать data перед class для JPA Entity оправдано, если id генерится на стороне приложения.

Теперь рассмотрим случай, когда id генерится на стороне базы данных.

@Table
@Entity
data class EntityDb(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    @Column
    var name: String
)

@Test
fun `test db generated id`() {
    val name = "имя"
    val (entityDb1, entityDb2) = EntityDb(name = name) to EntityDb(name = name)
    
    val m = mutableMapOf<EntityDb, String>()
    m[entityDb1] = "Найден"

    println("До сохранения в б.д.")
    println("${entityDb1 == entityDb2}") // true

    val result = entityDbRepository.save(entityDb1)

    println("После сохранения в б.д.")
    println("${entityDb1 == result}") // true
    println("${entityDb1 == entityDb2}") // false
    println(m[entityDb1] ?: "Не найден") // Не найден
}

Объект оказался изменён после сохранения и поэтому entityDb1 != entityDb2. Автоматически переопределенный метод equals содержал проверку на id == other.id. До сохранения id был null, а после принял значение равное id из базы. entityDb1 добавлен в hashmap, но после сохранения в б.д. hashcode изменился (так как изменился id) и теперь entityDb1 не найти в hashmap после сохранения. Оба метода требуют переопределения и указание data class для JPA Entity избыточно. Кстати, автоматически переопределённый метод toString наглядно демонстрирует side эффект, когда id для объекта не задан до сохранения в базу данных и id получает значение после сохранения в базу данных.

Использовать data class для JPA сущности оправдано, если id записи генерится на стороне приложения и избыточно, если id генерится на стороне базы данных, так как придётся переопределять методы equals и hashcode.

Комментарии (2)


  1. Kinski
    20.11.2023 21:02

    Честно говоря сложно понять, в чем проблема, когда нет результатов вывода набора println.

    Использовать data class для JPA сущности ... избыточно ... так как придётся переопределять методы equals и hashcode.

    Не совсем верно. В котлине дата классы придумали для создания неизменяемых классов (что видно в приведенном Вами декомпилированном коде - финальное поле и отсутствие сеттера).

    В первом случае (генерации id на стороне приложения) - поля экземпляра инициализируются в момент создания класса и больше не изменяются. Во втором случае - поле id заполняется в только в момент сохранения в БД, и это уже другой экземпляр, у него даже ссылка будет другая.

    Таким образом основная проблема в Вашем случае - не необходимость переопределять equals и hashCode, а наличие неконтролируемого числа экземпляров дата класса, что может привести к утечкам памяти.


    1. ViktorZ Автор
      20.11.2023 21:02

      >>Честно говоря сложно понять, в чем проблема, когда нет результатов вывода набора println.

      Семен Семенович. Пока я воевал с форматированием кода комментарии похерились. Спасибо за указание.

      >>Не совсем верно. В котлине дата классы придумали для создания неизменяемых классов (что видно в приведенном Вами декомпилированном коде - финальное поле и отсутствие сеттера).

      В декомпилированном коде отсуствует setter так поле отмечено как val. Если изименить на var появится public final setter.

      >Таким образом основная проблема в Вашем случае - не необходимость переопределять equals и hashCode, а наличие неконтролируемого числа экземпляров дата класса, что может привести к утечкам памяти.

      Это уже про сериализацию, я обозначил в статье как "side эффект". Дело в том, что переопределив equals и hashcode я могу сделать так, чтобы объект до сохранения и после были равны, например исключив id из equals и взяв другой ключ для сравнения (например isbn, инн, др идентификатор). Но зачем указывать data и потом переопределять методы, которые data под капотом автоматически переопределяет.