skinny-framework / skinny-framework

:monorail: "Scala on Rails" - A full-stack web app framework for rapid development in Scala

Home Page:https://skinny-framework.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SkinnyRecord#save() with TimestampsFeature doesn't update updatedAtField

riverpuro opened this issue · comments

SkinnyRecord object has companion with TimestampsFeature doesn't update its updatedAtField by save() method. Is it correct?

// There are SkinnyRecord object and companion object with TimestampsFeature.
case class Skill(
    id: Long,
    name: String,
    createdAt: DateTime,
    updatedAt: DateTime,
    lockVersion: Long
  ) extends SkinnyRecord[Skill] {
  def skinnyCRUDMapper = Skill
}

object Skill extends SkinnyCRUDMapper[Skill]
    with TimestampsFeature[Skill]
    with OptimisticLockWithVersionFeature[Skill] {

  override val tableName = "skills"
  override val defaultAlias = createAlias("s")
  override def extract(rs: WrappedResultSet, s: ResultName[Skill]): Skill = autoConstruct(rs, s)
}

// My expectations:
describe("TimestampsFeature") {
  it("should update updatedAtField by SkinnyRecord#save()") { implicit session =>
    val id1 = Skill.createWithAttributes('name -> "Scala")
    val skill = Skill.findById(id1).get

    Thread.sleep(100L)

    skill.copy(name = "Scala Programming").save()

    val updatedSkill = Skill.findById(id1).get
    updatedSkill.updatedAt should not equal (skill.updatedAt)
  }
}

// Test failed:
// [info] - should update updatedAtField by SkinnyRecord#save() *** FAILED ***
// [info]   2014-12-20T23:14:32.076+09:00 equaled 2014-12-20T23:14:32.076+09:00 (SkinnyORMSpec.scala:443) 

// skinny's implementation:
trait TimestampsFeature[Entity]
  ...
  override def updateBy(where: SQLSyntax): UpdateOperationBuilder = {
    val builder = super.updateBy(where)
    builder.addAttributeToBeUpdated(column.field(updatedAtFieldName) -> DateTime.now) // <- updatedAtField pair added
    builder
  }

trait NoIdCUDFeature[Entity]
  ...
    // attributesToBeUpdated has updatedAtField. But it may be updated by SkinnyRecord's attributesToPersist.
    protected def mergeNamedValues(namedValues: Seq[(SQLSyntax, Any)]): Seq[(SQLSyntax, Any)] = {
      namedValues.foldLeft(attributesToBeUpdated) {
        case (xs, (column, newValue)) =>
          if (xs.exists(_._1 == column)) xs.map { case (c, v) => if (c == column) (column -> newValue) else (c, v) }
          else xs.+=(column -> newValue)
      }.toSeq
    }

The reason of this bug is that TimestampsFeature places priority on passed updatedAt value. The following code does that.

https://github.com/skinny-framework/skinny-framework/blob/1.3.8/orm/src/main/scala/skinny/orm/feature/TimestampsFeatureBase.scala#L23

As you mentioned, currently SkinnyRecord always tries to persist existing updatedAt value. I guess the simplest fix is adding new configuration to SkinnyRecord to know the skinnyCRUDMapper is a TimestampsFeature wired one or not when creating attributesToPersist and filter it's attributes.

@riverpuro #218 fix will be out in 1.3.9. Are you in a hurry to fix this issue? If so, we can release the new version as soon as possible.

Thank you! I will wait for 1.3.9 release, don't hurry!