IRQ
IRQ

Reputation: 326

How to model schema.org in Scala?

Schema.org is markup vocabulary (for the web) and defines a number of types in terms of properties (no methods). I am currently trying to model parts of that schema in Scala as internal model classes to be used in conjunction with a document-oriented database (MongoDB) and a web framework.

As can be seen in the definition of LocalBusiness, schema.org uses multiple inheritance to also include properties from the "Place" type. So my question is: How would you model such a schema in Scala?

I have come up with two solutions so far. The first one use regular classes to model a single inheritance tree and uses traits to mixin those additional properties.

trait ThingA {
  var name: String = ""
  var url: String = ""
}

trait OrganizationA {
  var email: String = ""
}

trait PlaceA {
  var x: String = ""
  var y: String = ""
}

trait LocalBusinessA {
  var priceRange: String = ""
}

class OrganizationClassA extends ThingA with OrganizationA {}

class LocalBusinessClassA extends OrganizationClassA with PlaceA with LocalBusinessA {}

The second version tries to use case classes. However, since case class inheritance is deprecated, I cannot model the main hierarchy so easily.

trait ThingB {
  val name: String
}

trait OrganizationB {
  val email: String
}

trait PlaceB {
  val x: String
  val y: String
}

trait LocalBusinessB {
  val priceRange: String
}

case class OrganizationClassB(val name: String, val email: String) extends ThingB with OrganizationB

case class LocalBusinessClassB(val name: String, val email: String, val x: String, val y: String, val priceRange: String) extends ThingB with OrganizationB with PlaceB with LocalBusinessB

Is there a better way to model this? I could use composition similar to

case class LocalBusinessClassC(val thing:ThingClass, val place: PlaceClass, ...)

but then of course, LocalBusiness cannot be used when a "Place" is expected, for example when I try to render something on Google Maps.

Upvotes: 2

Views: 1065

Answers (1)

leedm777
leedm777

Reputation: 24052

What works best for you depends greatly on how you want to map your objects to the underlying datastore.

Given the need for multiple inheritance, and approach that might be worth considering would be to just use traits. This gives you multiple inheritance with the least amount of code duplication or boilerplating.

trait Thing {
  val name: String               // required
  val url: Option[String] = None // reasonable default
}

trait Organization extends Thing {
  val email: Option[String] = None
}

trait Place extends Thing {
  val x: String
  val y: String
}

trait LocalBusiness extends Organization with Place {
  val priceRange: String
}

Note that Organization extends Thing, as does Place, just as in schema.org.

To instantiate them, you create anonymous inner classes that specify the values of all attributes.

object UseIt extends App {
  val home = new Place {
    val name = "Home"
    val x = "-86.586104"
    val y = "34.730369"
  }

  val oz = new Place {
    val name = "Oz"
    val x = "151.206890"
    val y = "-33.873651"
  }

  val paulis = new LocalBusiness {
    val name = "Pauli's"
    override val url = "http://www.paulisbarandgrill.com/"
    val x = "-86.713660"
    val y = "34.755092"
    val priceRange = "$$$"
  }

}

If any fields have a reasonable default value, you can specify the default value in the trait.

I left fields without value as empty strings, but it probably makes more sense to make optional fields of type Option[String], to better indicate that their value is not set. You liked using Option, so I'm using Option.

The downside of this approach is that the compiler generates an anonymous inner class every place you instantiate one of the traits. This could give you an explosion of .class files. More importantly, though, it means that different instances of the same trait will have different types.

Edit:

In regards to how you would use this to load objects from the database, that depends greatly on how you access your database. If you use an object mapper, you'll want to structure your model objects in the way that the mapper expects them to be structured. If this sort of trick works with your object mapper, I'll be surprised.

If you're writing your own data access layer, then you can simply use a DAO or repository pattern for data access, putting the logic to build the anonymous inner classes in there.

This is just one way to structure these objects. It's not even the best way, but it demonstrates the point.

trait Database {
  // treats objects as simple key/value pairs
  def findObject(id: String): Option[Map[String, String]]
}

class ThingRepo(db: Database) {
  def findThing(id: String): Option[Thing] = {
    // Note that in this way, malformed objects (i.e. missing name) simply
    // return None. Logging or other responses for malformed objects is left
    // as an exercise :-)
    for {
      fields <- db.findObject(id) // load object from database
      name <- field.get("name")   // extract required field
    } yield {
      new Thing {
        val name = name
        val url = field.get("url")
      }
    }
  }
}

There's a bit more to it than that (how you identify objects, how you store them in the database, how you wire up repository, how you'll handle polymorphic queries, etc.). But this should be a good start.

Upvotes: 1

Related Questions