three-cups
three-cups

Reputation: 4385

Scala data model for read-only entities

I'm working on modeling entities that will be persisted in a DB. Using a User entity as an example, I'd like to work with them in this way:

val userBeforePersisting = new User("Joe", "[email protected]")

// DB access code (where rs is a ResultSet)
val foundUser = new User(rs.getLong("id"), rs.getString("name"), rs.getString("email"))

I'd like to use the same User code (i.e. minimize code duplication), while having two types of users:

  1. Pre-persisted users do not have an ID
  2. Persisted users retrieved from the DB have an ID

I'd like to enforce this as strictly as possible at compile-time.

I'd like to be able to treat all Users the same, except if I try and get an ID from an un-persisted User, an error will be raised or it would not compile.

I'd like to avoid having to make separate classes like this

class NewUser(val name: String, val email: String)
class PersistedUser(val id: Long, val name: String, val email: String)

I don't like this solution because of the code duplication (name and email fields).

Here's kind of what I'm thinking:

class User(val id: Long, val name: String, val email: String) {
  this(name: String, email: String) = this(0l, name, email)
  this(id: Long, name: String, email: String) = this(id, name, email)
}

But then my un-persisted users have an id of 0l.

Here's another approach:

trait User {
  val name: String
  val email: String
}

class NewUser(val name: String, val email: String) extends User

class PersistedUser(val id: Long, val name: String, val email: String) extends User

This gives me the compile-time checks that I'd like. I'm not sure if there are any draw-backs to this.

Maybe I could try something like this:

class User(val name: String, val email: String)
trait Persisted { val id: Long }
class PersistedUser(val id: Long, val name: String, val email: String)
  extends User(name, email)
  with Persisted

Any thoughts to these approaches? I've never done it this way, so I'm not sure if I understand all the consequences.

Upvotes: 1

Views: 303

Answers (1)

Faiz
Faiz

Reputation: 16255

Sounds like a possible use of Option.

class User(val id: Option[Long], val name: String, val email: String)

So persisted users have an id of Some(id) whereas non-persisted users have None.

As a convenience, you could grant id a default value of None:

class User(val id: Option[Long] = None, val name: String, val email: String)

// When you have an id...
val foundUser = new User(Some(rs.getLong("id")), 
    name = rs.getString("name"), email = rs.getString("email"))

// When you don't
val userBeforePersisting = new User(name = "Joe", email = "[email protected]")

// However this will throw a runtime error:
val idThatDoesntExist: Long = userBeforePersisting.id.get

This should also work with your multi-constructor example:

class User(val id: Option[Long], val name: String, val email: String) {
  def this(name: String, email: String) = this(None, name, email)
  def this(id: Long, name: String, email: String) = this(Some(id), name, email)
}

I thought Option might make sense because you'd like to express in the same class that a certain field can either have a value or not. The only other way seems to be to have two classes (possibly one inheriting from the other) with only one having an id field.

Upvotes: 3

Related Questions