Search code examples
scalaplayframeworkenumsscalikejdbc

Using case class to parse data containing enum in ScalikeJdbc and ScalaJson


I need to read certain enum values (apart from other regular stuff, including some optionally null values) from MySQL database in Scala. I'm using ScalikeJdbc for querying MySQL and parsing the result-set. I cannot find any mention of parsing enum types in it's docs (maybe I need to dig deeper).

But my target application needs to have both ways of loading data: database and JSON files. Therefore I'm also using ScalaJson (Play Framework) to parse Json files for reading exactly the same data as I would from MySQL database.

I'm aware that both ScalikeJdbc and ScalaJson support parsing data by using Scala's case classes for automatic conversion.


My questions are:

  • Does ScalikeJdbc support parsing Enum values like ScalaJson?
  • Can the same case class be used for parsing data in both ScalikeJdbc and ScalaJson or do they need to be different? My case class in question would contain parameters of custom types, which in turn are case classes themselves. These case classes accept parameters of following types apart from Enum values (basically, they don't contain parameter of exactly same types, but the level of complexity would be identical):
    • Int, String
    • Option[Int], Option[String]
    • Option[Seq[ (Int, String, Option[Int], Option[String]) ]]
  • Enums can be totally done away with by adding extra manual validation, although the end result might not be nearly as neat. That said, in general, is it a good idea to use Enums at all (particularly when reading data from database or JSON / XML files) or overhead of adding enums is too much to justify their benefits?

Solution

  • As told in comment on the question, I did away with reading Enums directly from MySQL database or JSON. Obviously, those fields are now simple VARCHARs in the database and I moved the validation logic into my Scala code. Since I'm performing validation separately, the case classes that I'm using to hold data read from MySQL or JSON no longer have any Enum.Value type fields.

    I would say that it would've made a lot more sense to have Enums directly in the database; but in lack of simple solution, I chose this workaround. It is still an open question and I will update the answer once I find the solution.


    Coming to the other part of my question

    Can the same case class be used for parsing data in both ScalikeJdbc and ScalaJson or do they need to be different?

    Yes

    I created case classes with companion objects and I'm able to use them for automatic-conversion in both ScalikeJdbc as well as ScalaJson. I'm posting a complete case class along with it's companion object that serves this dual purpose.

    Note: This code sample comes from a framework that aims to move MySQL tables from one-location to another. [actual production-code]

    Case-class:

    case class Table(dbId: Option[Int] = None,
                     id: Option[Int] = None,
                     name: String,
                     shouldCopy: Option[Boolean] = None,
                     lastUpdated: Option[DateTime] = None
                    )
    

    Companion-object:

    object Table {
    
      private def mapResultToTable(rs: WrappedResultSet): Table = Table(
        dbId = rs.intOpt("db_id"),
        id = rs.intOpt("id"),
        name = rs.string("name"),
        shouldCopy = rs.booleanOpt("should_copy"),
        lastUpdated = rs.dateTimeOpt("last_updated").flatMap { zonedDateTime: ZonedDateTime =>
          Some(DateTime.parse(zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).plusMinutes(330))
        }
      )
    
      def getTables(db: Db)(implicit session: DBSession): Seq[Table] = {
        sql"""
           SELECT
             *
           FROM
             db_name.tables
           WHERE
             db_id = ${db.id} AND
             should_copy = b'1'
           ORDER BY
             name ASC
        """.
          map(mapResultToTable).
          list().
          apply()
      }
    
      // for Play-Json [Scala-Json]
      implicit val dateTimeJsReader = JodaReads.jodaDateReads("yyyyMMddHHmmss")
      implicit val dateTimeWriter = JodaWrites.jodaDateWrites("dd/MM/yyyy HH:mm:ss")
    
      implicit val reads = Json.reads[Table]
      implicit val writes = Json.writes[Table]
    }
    

    Here's explanation for specific parts of the code:

    • def mapResultToTable(rs: WrappedResultSet)

      This method reads the result-set of database query and builds an object of the case class

    • def getTables(db: Db)

      This method queries MySQL database is using ScalikeJdbc

    • implicit val dateTimeJsReader = JodaReads.jodaDateReads("yyyyMMddHHmmss")

    • implicit val dateTimeJsReader = JodaReads.jodaDateReads("yyyyMMddHHmmss")

      These are Joda DateTime read-write converters (used by ScalaJson) for the Joda DateTime parameter of case class

    • implicit val reads = Json.reads[Table]

    • implicit val writes = Json.writes[Table]

      These are read-write converters for the given case class


    Finally, here's the code-snippet for reading data of this case class from Json file

    val tableOpt: Option[Table] = try {
      val rawJsonString: String = Source.fromFile("path/to/file/fileName.json").mkString
      Some(Json.parse(rawJsonString).as[Table])
    } catch {
      case ex: Throwable =>
        println(s"Exception while reading Json file: '$ex'")
        None
    }
    

    One final piece of advice for noobs like me: Unless you are comfortable with ScalaJson (and JSON parsing, in general), avoid using the Json.parse(jsonString).asOpt[T] method (the above snippet uses .as[T] method) in order to overcome subtle mistakes like this.


    EDIT-1

    Does ScalikeJdbc support parsing Enum values like ScalaJson?

    Getting Enum.Value really has nothing to do with ScalikeJdbc: once you have a String, how difficult is it to convert to Enum.Value?

    Here's a small example:

    // Enum
    object CloudProvider extends Enumeration {
    
      val AWS: Value = Value("aws")
      val MicrosoftAzure: Value = Value("microsoft-azure")
      val GoogleCloud: Value = Value("google-cloud")
    }
    
    // Case Class
    case class SiteInfo(website: String, provider: CloudProvider.Value)
    
    // Mapper method
    def mapResult(rs: WrappedResultSet): SiteInfo = {
      val website: String = rs.string("website")
      val provider: CloudProvider.Value = CloudProvider.values.
        find(_.toString == rs.string("provider")).
        getOrElse(CloudProvider.AWS)
    
      SiteInfo(website, provider)
    }
    

    It's difficult to understand why I couldn't see the answer when it was right there in front of me: (mapResult method)


    ..in general, is it a good idea to use Enums at all..

    Yes, Absolutely

    The entire point of having an Enum type is to restrict the range of values a variable can take. I can't see an easier way to enforce such rules other than an Enum.

    Also once you have the sufficient know-how of language / library that your'e using (I didn't know much Scala & ScalikeJdbc at that time), there's absolutely no overhead (from coding perspective) for using Enum; in fact they make code much clearer. Needless to say even from performance perspective, using Enums is far better that using String.