2

I have a a class like this:

case class SurveyTemplate(id:UUID,
                          name: String,
                          version: Int,
                          preDefinedChapter: Seq[Chapter]) {
}

I serialized this as binary in a DAO with this.

def save(input: SurveyTemplate) = Future{
    val target = toFile(input)
    val output = AvroOutputStream.binary[SurveyTemplate](target)
    output.write(input)
    output.flush()
    output.close()
    input
  }

Now I saved the file binary to the harddisk. All fine. Now I want change the SurveyTemplate with a new field like "dummy: String"

If i now load the files and try to deserialize I get a java.util.NoSuchElementException which means their is no file which can be deserialized. If I remove the additional field all works fine.

Here is the list method:

def list(name: Option[String] = None,
                    version: Option[Int] = None,
                    authorName: Option[String] = None,
                    drop: Int = 0,
                    take: Int = 100) = Future{
    storageDirectory.listFiles(new FilenameFilter {
      override def accept(dir: File, name: String): Boolean = name.endsWith(ending)
    }).map{e =>
      println("FOUND " + e.getName)    
      val i = AvroInputStream.binary[SurveyTemplate](e)
      println("BINARY " + i)
      println(i.records.size())
      println( Try(i.iterator().toSeq.head) )
      val p = i.iterator().toSeq.head
      i.close()
      println(p)
      println("------------------")
      Some(p)
    }.filter(_.isDefined).slice(drop,take).map(_.get)
  }

How can I add the possibilty of evolution?

Thank you.

Added: Result of print´s

BINARY com.sksamuel.avro4s.AvroBinaryInputStream@58d20d47
0
Failure(java.util.NoSuchElementException: head of empty stream)

My new SurveyTemplate:

case class SurveyTemplate(id:UUID,
                          name: String,
                          version: Int,
                          preDefinedChapter: Seq[Chapter], 
                          dummy: Option[String] = None) {
}

Here is now the minimal

import java.io.{File, FilenameFilter}
import java.util.UUID

import com.sksamuel.avro4s.{AvroInputStream, AvroOutputStream, AvroSchema, SchemaFor}

import scala.util.Try

object Main {

  def main(args: Array[String]): Unit ={
    val dao = new SurveyTemplateDAO
    //RUN THIS FIRST!
    val first = SurveyTemplate(UUID.randomUUID(),"TEST",0,"ICH",Seq(Chapter(1,1,"HELLO",Nil)))
    dao.save(first)
    //COMMENT OUT! Then uncomment the dummy in SurveyTemplate

    //RUN THIS AFTER FILE SAVED
    dao.list()

  }

}


case class Chapter(counter: Int,
                   level: Int,
                   title: String,
                   subChapters: Seq[Chapter]) {

}

case class SurveyTemplate(id:UUID,
                          name: String,
                          version: Int,
                          authorName: String,
                          preDefinedChapter: Seq[Chapter]/*, dummy: Option[String] = None*/) {

}

class SurveyTemplateDAO {

  private val ending = ".pst"
  private val storageDirectory = new File("C:/Users/André Schmidt/Desktop/tmp")

  def toFileName(x: SurveyTemplate): String = toFileName(x.id)
  def toFileName(x: UUID): String = s"$x$ending"
  def toFile(x: SurveyTemplate) = new File(storageDirectory.getCanonicalPath+File.separator+toFileName(x))

  private implicit val schemaForChapter = SchemaFor[Chapter]

  def save(input: SurveyTemplate) = {
    val target = toFile(input)
    val output = AvroOutputStream.binary[SurveyTemplate](target)
    output.write(input)
    output.flush()
    output.close()
    input
  }


  def list(name: Option[String] = None,
           version: Option[Int] = None,
           authorName: Option[String] = None,
           drop: Int = 0,
           take: Int = 100) = {

    storageDirectory.listFiles(new FilenameFilter {
      override def accept(dir: File, name: String): Boolean = name.endsWith(ending)
    }).take(1).foreach{e =>
      println("OTHER WAY")
      val i = AvroInputStream.binary[SurveyTemplate](e)
      println("BINARY " + i)
      println(i.records.size())
      //println( Try(i.iterator().toList.head) )
      val p = Try(i.iterator().toList.head).toOption
      i.close()
      println(p)
      println("OTHER WAY ------------------")
      println("OTHER WAY ------------------")
      println("OTHER WAY ------------------")
    }


    storageDirectory.listFiles(new FilenameFilter {
      override def accept(dir: File, name: String): Boolean = name.endsWith(ending)
    }).map{e =>
      println("FOUND " + e.getName)
      //      val schema = SchemaBuilder.builder()
      //      val r = Schema.createRecord(java.util.Arrays.asList())
      //      val t = new AvroBinaryInputStream[SurveyTemplate](new SeekableFileInput(e),readerSchema = Some(r))
      //      println(t)

      val i = AvroInputStream.binary[SurveyTemplate](e,AvroSchema[SurveyTemplate]/*,AvroSchema[OldSurveyTemplate]*/)
      println("BINARY " + i)
      println(i.records.size())
      //println( Try(i.iterator().toList.head) )
      val p = Try(i.iterator().toList.head).toOption
      i.close()
      println(p)
      println("------------------")
      println("ENDE")
      p
      //      if( authorName.map(_.equalsIgnoreCase(p.authorName)).getOrElse( name.map(_.equalsIgnoreCase(p.name) ).getOrElse( version.forall(_ == p.version) ) ) ){
      //        Some(p)
      //      } else None
    }.filter(_.isDefined).slice(drop,take).map(_.get)
  }

}

My sbt:

name := "avroTest"

version := "0.1"

scalaVersion := "2.12.4"

libraryDependencies ++= Seq(
  "com.sksamuel.avro4s" %% "avro4s-core" % "1.8.1"
)
André
  • 125
  • 1
  • 8

2 Answers2

0

In order for SurveyTemplate to be backwards compatible, we must make sure that new readers can read data from old writers. That means that the new parameter must have a default value, since old writer can’t provide it. That means:

case class SurveyTemplate(id:UUID,
                          name: String,
                          version: Int,
                          preDefinedChapter: Seq[Chapter],
                          dummyString: String = “”)

Or making dummyString an Option[String].

Another thing is, you mustn’t change the order of the parameters, unless you're providing the writer schema to the serializer.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • Okay. I changed the dummy from String to Option[String] with no default value. I get again a NSE Exception. Even if I declare the default value in the case class. – André Jan 29 '18 at 19:25
  • @Andre You need to fill it with `None` as a default value – Yuval Itzchakov Jan 29 '18 at 19:27
  • Also done. Same error. Using Idea and execute only run. Do I have execute an sbt command for new generation or something else? – André Jan 29 '18 at 19:31
  • @Andre Are you sure you’re writing properly? What is the file size? – Yuval Itzchakov Jan 29 '18 at 19:42
  • The files was written. If i remove the dummy I get the result. – André Jan 29 '18 at 19:46
  • In my DAO I have defined only this as Schema: `private implicit val schemaForChapter = SchemaFor[Chapter]` I did this because Chapter as a recursvie structure. Could this be the problem? – André Jan 29 '18 at 19:47
  • @Andre Can you post an [MCVE]? Ill give it a try – Yuval Itzchakov Jan 29 '18 at 19:54
  • Okay, I will build a minimal example it takes a few minutes – André Jan 29 '18 at 19:58
  • @André I get a Stackoverflow exception while trying to deserialize your example. – Yuval Itzchakov Jan 30 '18 at 05:18
  • I tested on another machine. No Stackoverflow. `OTHER WAY BINARY com.sksamuel.avro4s.AvroBinaryInputStream@1f3f4916 0 None OTHER WAY ------------------ OTHER WAY ------------------ OTHER WAY ------------------ FOUND af3c9f13-c917-457b-9d17-2bf440e44780.pst BINARY com.sksamuel.avro4s.AvroBinaryInputStream@48e4374 0 None ------------------ ENDE` But same error :( – André Jan 30 '18 at 08:02
0

Change the AvroInputStream.binary tp AvroInputStream.data allows me to add evolution! That is not very nice, because a binary representation of my file is better for me task.

But in binary all tests failed!

André
  • 125
  • 1
  • 8
  • Data encodes the schema inside each file, that has a significant overhead. – Yuval Itzchakov Jan 31 '18 at 20:07
  • Ok, good to know. But binary is incompatible for evolution in my case. Do you have a running example? – André Jan 31 '18 at 21:17
  • didn't explicitly specifying the writer schema work, i.e.: `val i = AvroInputStream.binary[SurveyTemplate](e, AvroSchema[OldSurveyTemplate])`? – s4nk Jul 20 '18 at 17:19