Reputation: 793
I am writing a play2.1 application with mongodb, and my model object is a bit extensive. when updating an entry in the DB, i need to compare the temp object coming from the form with what's in the DB, so i can build the update query (and log the changes).
i am looking for a way to generically take 2 instances and get a diff of them. iterating over each data member is long, hard-coded and error prone (if a.firstName.equalsIgnoreCase(b.firstName)) so i am looking for a way to iterate over all data members and compare them horizontally (a map of name -> value will do, or a list i can trust to enumerate the data members in the same order every time).
any ideas?
case class Customer(
id: Option[BSONObjectID] = Some(BSONObjectID.generate),
firstName: String,
middleName: String,
lastName: String,
address: List[Address],
phoneNumbers: List[PhoneNumber],
email: String,
creationTime: Option[DateTime] = Some(DateTime.now()),
lastUpdateTime: Option[DateTime] = Some(DateTime.now())
)
all three solutions below are great, but i still cannot get the field's name, right? that means i can log the change, but not what field it affected...
Upvotes: 15
Views: 15542
Reputation: 1
If you want to have access to the field name you can use Java's Reflection API. In this case you can access the declared fields by using the getDeclaredFields
method and iterate over the fields. If you then want to access the field's value changes to the field you need to set it accessible (setAccessible
method) because by default all class parameters are implemented as private fields with public accessors.
val c = C(1, "C", 'c'))
for(field <- c.getClass.getDeclaredFields) {
println(field.getName)
field.get(c)
}
Upvotes: 0
Reputation: 15074
Expanding on @Malte_Schwerhoff's answer, you could potentially create a recursive diff method that not only generated the indexes of differences, but mapped them to the new value at that index - or in the case of nested Product types, a map of the sub-Product differences:
def diff(orig: Product, update: Product): Map[Int, Any] = {
assert(orig != null && update != null, "Both products must be non-null")
assert(orig.getClass == update.getClass, "Both products must be of the same class")
val diffs = for (ix <- 0 until orig.productArity) yield {
(orig.productElement(ix), update.productElement(ix)) match {
case (s1: String, s2: String) if (!s1.equalsIgnoreCase(s2)) => Some((ix -> s2))
case (s1: String, s2: String) => None
case (p1: Product, p2: Product) if (p1 != p2) => Some((ix -> diff(p1, p2)))
case (x, y) if (x != y) => Some((ix -> y))
case _ => None
}
}
diffs.flatten.toMap
}
Expanding on the use cases from that answer:
case class A(x: Int, y: String)
case class B(a: A, b: AnyRef, c: Any)
val a1 = A(4, "four")
val a2 = A(4, "Four")
val a3 = A(4, "quatre")
val a4 = A(5, "five")
val b1 = B(a1, null, 6)
val b2 = B(a1, null, 7)
val b3 = B(a2, a2, a2)
val b4 = B(a4, null, 8)
println(diff(a1, a2)) // Map()
println(diff(a1, a3)) // Map(0 -> 5)
println(diff(a1, a4)) // Map(0 -> 5, 1 -> five)
println(diff(b1, b2)) // Map(2 -> 7)
println(diff(b1, b3)) // Map(1 -> A(4,four), 2 -> A(4,four))
println(diff(b1, b4)) // Map(0 -> Map(0 -> 5, 1 -> five), 2 -> 8l
Upvotes: 9
Reputation: 12852
You can use the product iterator, and match on the elements if you want to use non-standard equality such as String.equalsIgnoreCase
.
def compare(p1: Product, p2: Product): List[Int] = {
assert(p1 != null && p2 != null, "Both products must be non-null")
assert(p1.getClass == p2.getClass, "Both products must be of the same class")
var idx = List[Int]()
for (i <- 0 until p1.productArity) {
val equal = (p1.productElement(i), p2.productElement(i)) match {
case (s1: String, s2: String) => s1.equalsIgnoreCase(s2)
case (x, y) => x == y
}
if (!equal) idx ::= i
}
idx.reverse
}
Use cases:
case class A(x: Int, y: String)
case class B(a: A, b: AnyRef, c: Any)
val a1 = A(4, "four")
val a2 = A(4, "Four")
val a3 = A(5, "five")
val b1 = B(a1, null, 6)
val b2 = B(a1, null, 7)
val b3 = B(a2, a2, a2)
println(compare(a1, a2)) // List()
println(compare(a1, a3)) // List(0, 1)
println(compare(b1, b2)) // List(2)
println(compare(b2, b3)) // List(0, 1, 2)
// println(compare(a1, b1)) // assertion failed
Upvotes: 6
Reputation: 18869
Maybe productIterator
is what you wanted:
scala> case class C(x: Int, y: String, z: Char)
defined class C
scala> val c1 = C(1, "2", 'c')
c1: C = C(1,2,c)
scala> c1.productIterator.toList
res1: List[Any] = List(1, 2, c)
Upvotes: 32