工作中存在一个基本问题,这是 JPA/Hibernate 固有的问题。对于此示例,假设我们有一个名为 User 的 db 表,并且我们有一个也名为 User 的类对其进行建模。
问题归结为:
java类User代表什么?它是代表'数据库表“用户”中的一行',还是代表一个用户?
根据您的回答,您对 equals 方法有完全不同的要求。根据您选择的 equals 方法,错误地回答此问题会导致代码错误。据我所知,没有真正的“标准”,人们只是在做某事,而大多数人并没有意识到这是一个基本问题。
代表数据库中的一行
这样的解释将建议您的 equals 方法的以下实现:
-
如果在数据库定义中模拟主键列的所有字段在两个实例之间都相等,那么它们是相等的,即使其他(非主键)字段不同。毕竟数据库是这样判断相等的,所以java代码应该匹配它。
-
Java 代码在处理 NULL 时应该像 SQL。也就是说,几乎所有的等式定义、equals方法代码生成器(包括lombok、intellij、和 eclipse),甚至Objects.equals方法,都非常不同,在这种模式下,null == null 应该为 FALSE,就像在 SQL 中一样! 具体来说,如果任何主键字段具有空值,则该对象不能等于任何其他对象,即使是抄本本身;为了遵守 java 规则,它可以(必须,真的)等于它自己的引用。
换句话说:
- 如果 [A] 它们实际上是同一个对象 (
this == other) 或 [B] 两个对象的 unid 字段已初始化且相等,则任何 2 个对象都是相等的。无论您使用 null 还是 0 来跟踪“尚未写入数据库”,该值都会立即取消该行与任何其他行相等的资格,即使是另一个具有 100% 相同值的行。
毕竟,如果您创建 2 个单独的新对象并 save() 两者,它们将变成 2 个单独的行。
它代表一个用户对象
然后发生的情况是,equals 规则执行 180。主键,假设它是 unid 风格的主键而不是自然主键,本质上是一个实现细节。想象一下,不知何故,在您的数据库中,您最终得到了完全相同的用户的 2 行(可能有人搞砸了,并且未能在用户名上添加 UNIQUE 约束)。在系统上用户的语义模型中,用户是由用户名唯一标识的,因此,相等性仅由用户名定义。 2 个具有相同用户名但不同 unid 值的对象仍然相等。
那我该拿哪一个?
我不知道。幸运的是,您的问题要求解释而不是答案!
IntelliJ 告诉您的是使用第一个解释(数据库中的行),甚至正确地应用了不可靠的 null 东西,所以在 intellij 中编写建议工具的人至少似乎理解发生了什么。
对于它的价值,我认为“代表数据库中的一行”是更“有用”的解释(因为不这样做涉及调用 getter,这会使相等性检查非常昂贵,因为它可能会导致数百次 SELECT 调用和一大堆堆内存,当你把一半的数据库拉进去时!),然而,“User 类的一个实例代表系统中的一个用户”是更像 java 的解释,也是大多数 java 程序员会的解释(错误的话,如果你在这里使用intellij的建议)默默地假设。
我已经在自己的编程工作中解决了这个问题,首先从不使用 hibernate/JPA,而是使用 JOOQ 或 JDBI 等工具。但是,缺点是通常你会得到更多的代码——你有时确实有一个对象,例如称为UserRow,代表一个用户行和一个对象,例如称为User,代表系统上的用户。
另一个技巧可能是决定将所有 Hibernate 模型类命名为 XRow。名称很重要,并且是最好的文档:这毫不含糊,并且在此代码的所有用户中都提供了有关如何解释其语义含义的线索:DB 中的行。因此,intellij 建议将是您的 equals 实现。
注意:Lombok 是 java 而不是特定于 Hibernate,因此它选择了“代表系统中的用户”。您可以通过告诉 lombok 仅使用 id 字段(在该字段上粘贴 @EqualsAndHashCode.Include)来尝试将 lombok 推向“DB 中的行”解释,但 lombok 仍会考虑 2 null 值 / 2 0 值相同,即使它不应该。这是在休眠状态,因为它违反了各种规则和规范。
(注意:由于对另一个答案的评论而添加)
为什么会调用.getClass()?
Java 对 equals 的含义有合理的规则。这是在 equals 方法的 javadoc 中,可以依赖这些规则(例如,HashSet 和 co)。规则是:
- 如果
aequals(b) 为真,a.hashCode() == b.hashCode() 也必须为真。
-
a.equals(a) 必须为真。
- 如果
a.equals(b) 则b.equals(a) 也必须为真。
- 如果
a.equals(b) 和b.equals(c) 则a.equals(c) 也必须为真。
明智而简单,对吧?
不。这实际上非常复杂。
假设您创建了ArrayList 的子类:您决定为列表赋予颜色。你可以有一个蓝色的字符串列表和一个红色的字符串列表。
现在 ArrayList 的相等方法检查 that 是否是一个列表,如果是,则比较元素。看起来很明智,对吧?我们可以看到它的实际效果:
List<String> a = new ArrayList<String>();
a.add("Hello");
List<String> b = new LinkedList<String>();
b.add("Hello");
System.out.println(a.equals(b));
这是真的。
现在让我们实现彩色数组列表:class ColoredList<T> extends ArrayList<T> { .. }。当然,红色的空列表不再等于蓝色的空列表,对吧?
不行,如果你这样做,你就违反了规则!
List<String> a = new ArrayList<String>();
List<String> b = new ColoredList<String>(Color.RED);
List<String> c = new ColoredList<String>(Color.BLUE);
System.out.println(a.equals(b));
System.out.println(a.equals(c));
System.out.println(b.equals(c));
打印 invalid 的 true/true/false。结论是,实际上不可能创建任何添加一些语义相关信息的列表子类。唯一可以存在的子类是那些主动破坏规范(坏主意),或者其添加对平等没有影响的子类。
有一种不同的观点认为你应该能够制作这样的课程。就像在 JPA/Hibernate 案例中一样,我们再次纠结于 equals 的含义。
equals 实现的一个更常见和更好的默认行为是简单地声明任何 2 个对象只有在它们具有完全相同的类型时才能相等:Dog 的实例不能等于 @ 的实例987654358@.
鉴于规则a.equals(b)?那么b.equals(a) 存在,是动物检查that 的类并返回false,如果它不完全是Animal。换句话说:
Animal a = new Animal("Betsy");
Cow c = new Cow("Betsy");
a.equals(c); // must return false!!
.getClass() 检查完成此操作。
Lombok 为您提供两全其美的体验。它不能创造奇迹,所以它不会取消在类型级别需要选择可扩展性的规则,但是 lombok 有canEqual 系统来处理这个问题:Animal 的 equals 代码会询问that 代码如果两者可以相等。在这种模式下,如果你有一些非语义不同的动物子类(例如ArrayList,它是 AbstractList 的子类,完全不改变语义,它只是添加了与相等性无关的实现细节),它可以说它可以相等,而如果你有一个语义不同的,比如你的彩色列表,它可以说没有。
换句话说,回到彩色列表,IF ArrayList 和 co 是用 lombok 的 canEqual 系统编写的,这本来可以解决的,你可以得到结果(a 是数组列表,b 是红色列表,c 是模糊列表):
a.equals(b); // false, even though same items
a.equals(c); // false, same reason.
b.equals(c); // false and now it's not a conflict.
Lombok 的默认行为是所有子类型都添加语义负载,因此任何 X 不能等于 Y 是 X 的子类的任何 Y,但您可以通过在 Y 中写出 canEqual 方法来覆盖它。如果你编写一个不增加语义负载的子类,你会这样做。
这对上面关于休眠的问题一点帮助也没有。
谁知道像平等这样看似简单的事情却隐藏着两篇棘手的哲学论文,嗯?
有关 canEqual 的更多信息,see lombok's @EqualsAndHashCode documentation。