问题
Squeryl
defines a trait KeyedEntity
which overrides equals
, checking for several conditions in an if and calling super.equals
in the end. Since super
is Object
, it will always fail.
Consider:
trait T { override def equals(z: Any):Boolean = super.equals(z)} }
case class A(a: Int) extends T
val a = A(1); val b = A(1)
a==b // false
Thus, if you declare
case class Record(id: Long, name: String ...) extends KeyedEntity[Long] { ... }
-- and you create several Record
instances but do not persist them, their comparison will break. I found this by implementing both Salat
and Squeryl
back ends for the same class, and then all Salat
tests fail since isPersisted
from KeyedEntity
is false.
Is there a design whereby KeyedEntity
will preserve case class equality if mixed into a case class? I tried self-typing and parameterizing BetterKeyedEntity[K,P] { self: P => ... }
for the case class type as P but it causes infinite recursion in equals.
As things stand right now, super
is Object
so the final branch of the overridden equals in KeyedEntity
will always return false.
回答1:
The structural equality check usually generated for case classes seems not to be generated if there is an equals
override. However some subtleties have to be noted.
Mixing the concept of id-based equality falling back to structural equality might not be a good idea, since I can imagine that it may lead to subtle bugs. For example:
x: A(1)
and andy: A(1)
are not yet persisted, so they are equal- then they get persisted, and since they are separate objects, the persistence framework may persist them as separate entities (I don't know Squeryl, maybe not an issue there, but this is a thin line to walk)
- after persisting, they are suddenly not equal since the id differs.
Even worse, if x
and y
get persisted to the same id, the hashCode
will differ before and after persisting (the source shows that if persisted it is the hashCode of the id). This breaks immutability, and will lead to very bad behavior (when put in maps for example). See this gist in which I demonstrate the assert failing.
So don't mix structural and id-based equality implicitly. Also see this explained in the context of Hibernate.
Typeclasses
It have to be noted that others pointed out (ref needed) that the concept of method-based equality is flawed, for such reasons (there is not only one way two thing can be equal). Therefore you can define a typeclass which describes Equality:
trait Eq[A] {
def equal(x: A, y: A): Boolean
}
and define (possibly multiple) instances of that typeclass for your classes:
// structural equality
implicit object MyClassEqual extends Eq[MyClass] { ... }
// id based equality
def idEq[K, A <: KeyedEntity[K]]: Eq[A] = new Eq[A] {
def equal(x: A, y: A) = x.id == y.id
}
then you can request that things are members of the Eq typeclass:
def useSomeObjects[A](a: A, b: A)(implicit aEq: Eq[A]) = {
... aEq.equal(a, b) ...
}
So you can decide which notion of equality to use by importing the appropriate typeclass in scope, or passing the typeclass instance directly as in useSomeObjects(x, y)(idEq[Int, SomeClass])
Note that you might also need a Hashable
typeclass, similarly.
Autogenerating Eq instances
This situation is pretty similar to the Scala stdlib's scala.math.Ordering
typeclass. Here is an example for auto-deriving structural Ordering instances for case classes using the excellent shapeless library.
The same would easy to be done for Eq
and Hashable
.
Scalaz
Note that scalaz has Equal typeclass, with nice pimp patterns with which you can write x === y
instead of eqInstance.equal(x, y)
. I'm not aware it has Hashable
typeclass, yet.
来源:https://stackoverflow.com/questions/12434798/prevent-mixin-overriding-equals-from-breaking-case-class-equality