Search code examples
scalaplayframeworkplayframework-2.1

iterate over case class data members


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...


Solution

  • 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