Persisting a recursive data model with SORM

85 Views Asked by At

For my project, I would like to make a tree model; let's say it's about files and directories. But files can be in multiple directories at the same time, so more like the same way you add tags to email in gmail. I want to build a model for competences (say java, scala, angular, etc) and put them in categories. In this case java and scala are languages, agila and scrum are ways of working, angular is a framework / toolkit and so forth. But then we want to group stuff flexibly, ie play, java and scala are in a 'backend' category and angular, jquery, etc are in a frontend category.

I figured I would have a table competences like so:

case class Competence (name: String, categories: Option[Category])

and the categories as follows:

case class Category ( name: String, parent: Option[Category] )

This will compile, but SORM will generate an error (from activator console):

scala> import models.DB
import models.DB
scala> import models.Category
import models.Category
scala> import models.Competence
import models.Competence
scala> val cat1 = new Category ( "A", None )
cat1: models.Category = Category(A,None)
scala> val sav1 = DB.save ( cat1 )
sorm.Instance$ValidationException: Entity 'models.Category' recurses at 'models.Category'
  at sorm.Instance$Initialization$$anonfun$2.apply(Instance.scala:216)
  at sorm.Instance$Initialization$$anonfun$2.apply(Instance.scala:216)
  at scala.Option.map(Option.scala:146)
  at sorm.Instance$Initialization.<init>(Instance.scala:216)
  at sorm.Instance.<init>(Instance.scala:38)
  at models.DB$.<init>(DB.scala:5)
  at models.DB$.<clinit>(DB.scala)
  ... 42 elided

Although I want the beautiful simplicity of sorm, will I need to switch to Slick for my project to implement this? I had the idea that link tables would be implicitly generated by sorm. Or could I simply work around the problem by making a:

case class Taxonomy ( child: Category, parent: Category )

and then do parsing / formatting work on the JS side? It seems to make the simplicity of using sorm disappear somewhat.

To give some idea, what I want is to make a ajaxy page where a user can add new competences in a list on the left, and then link/unlink them to whatever category tag in the tree he likes.

1

There are 1 best solutions below

1
cuz On

I encountered the same question. I needed to define an interation between two operant, which can be chained(recursive). Like:

case class InteractionModel(
    val leftOperantId: Int,
    val operation: String ,
    val rightOperantId: Int,
    val next: InteractionModel)

My working around: change this case class into Json(String) and persist it as String, when retreiving it, convert it from Json. And since it's String, do not register it as sorm Entity.

import spray.json._
case class InteractionModel(
    val leftOperantId: Int,
    val operation: String ,
    val rightOperantId: Int,
    val next: InteractionModel) extends Jsonable {
  def toJSON: String = {
    val js = this.toJson(InteractionModel.MyJsonProtocol.ImJsonFormat)
    js.compactPrint
  }
}

//define protocol
object InteractionModel {
  def fromJSON(in: String): InteractionModel = {
    in.parseJson.convertTo[InteractionModel](InteractionModel.MyJsonProtocol.ImJsonFormat)
  }

  val none = new InteractionModel((-1), "", (-1), null) {
    override def toJSON = "{}"
  }

  object MyJsonProtocol extends DefaultJsonProtocol {
    implicit object ImJsonFormat extends RootJsonFormat[InteractionModel] {
      def write(im: InteractionModel) = {
        def recWrite(i: InteractionModel): JsObject = {
          val next = i.next match {
            case null  => JsNull
            case iNext => recWrite(i.next)
          }
          JsObject(
            "leftOperantId" -> JsNumber(i.leftOperantId),
            "operation" -> JsString(i.operation.toString),
            "rightOperantId" -> JsNumber(i.rightOperantId),
            "next" -> next)
        }
        recWrite(im)
      }
      def read(value: JsValue) = {
        def recRead(v: JsValue): InteractionModel = {
          v.asJsObject.getFields("leftOperantId", "operation", "rightOperantId", "next") match {
            case Seq(JsNumber(left), JsString(operation), JsNumber(right), nextJs) =>
              val next = nextJs match {
                case JsNull => null
                case js     => recRead(js)
              }
              InteractionModel(left.toInt, operation, right.toInt, next)
            case s => InteractionModel.none
          }
        }
        recRead(value)
      }
    }
  }
}