0

I'm trying to use the lifted embedding approach of Slick on a real-life case (self-management of personal data for sports club members). I have already managed to retrieve the information from the database and to update records (implementing Member as a case class and using the case class copy method, as shown below) but I have some hard time finding the best way to implement member modification in a natural way.

I have contemplated 2 possibilities : 1) Maintaining immutability of class instances and implementing a general setter (see code) 2) Making the constructor parameters "var"'s (what would suppress immutability, and therefore not ideal)

Sticking with option 1, I came up with the following code (excerpt, not the whole source) :

    case class Member(id: Int, name: String, firstname: Option[String] = None,
      birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None,
      healthNotes: Option[String]) {

      // Just a try so far
      def set(n: String = name, fn: Option[String] = firstname, bd : Option[Date] = birthDate)
        (implicit session: Session) = { 
        val m = this.copy(id,n,fn,bd)
        Members.update(m)
        m
      }
    }

    object Members extends Table[Member]("clubmembers") 
        with CayenneAutoPKSupport {

      def id = column[Int]("Member_ID", O.PrimaryKey) // This is the primary key column
      def name = column[String]("Name")
      def firstname = column[Option[String]]("Firstname")
      def birthDate = column[Option[Date]]("BirthDate")
      def gender = column[Option[String]]("Gender")
      def country = column[Option[String]]("Country")
      def healthNotes = column[Option[String]]("HealthNotes")

      // Every table needs a * projection with the same type as the table's type parameter
      def * = id ~ name ~ firstname ~ birthDate ~ gender ~ country ~ healthNotes <> (Member.apply _, Member.unapply _)
}

This works as intended but I would like to have the same names for the named parameters of the set method (what would made the invocation more "natural"). I tried the following (to no avail)

def set( name: String = this.name, …

This does not compile and I can imagine why the compiler is not happy (OTOH the current implementation seems to work) but I could also imagine that it could work. Anyway: does someone see a way to achieve this?

Alternatively, what would one recommend as best practices to implement modifiable class instances for Slick-persisted objects?

Thanks in advance for any hint.

Regards

Taryn
  • 242,637
  • 56
  • 362
  • 405
koalabi
  • 109
  • 1
  • 8

2 Answers2

1

Well, actually, the original code works if one uses the same names for the parameters and the default values. It is only the syntax highlighter in Scala IDE that does not seem to understand: while the default gets (correctly) highlighted as class members if the names are different, they just get displayed as the parameter itself where the names are the same.

Here the current version (that works as intended but is not syntax-highlighted correctly):

 def set(name: String = name, firstname: String = firstname, birthDate : Option[Date] = birthDate,
        gender: String = gender, country: String = country, 
        addressBlockId: Option[Int] = addressBlockId, healthNotes: String = healthNotes)
        (implicit session: Session) = { 
    val m = this.copy(id,name,Option(firstname),birthDate,
        Option(gender),Option(country),addressBlockId,Option(healthNotes))
    m
  }

NOTE: String parameters would better get passed as Option[String] too.

Comments and suggestions welcome.

Regards

koalabi
  • 109
  • 1
  • 8
  • This is a valid way to do this and I consider this the correct answer. Having db access methods in case classes or not is a question of your design and architecture. If you don't want them like @krivachy.akos, you can just move the set method into your dao call is setMember and pass the member object as the first argument. – cvogt Nov 10 '13 at 17:04
  • This answer doesn't differ from Scala case classes built in copy method, it does the exact same thing. If you plan on doing something with that session variable then read my updated answer. – Akos Krivachy Nov 10 '13 at 20:39
  • OK. Thanks to both for the motivated answers and the pointers to further reference material – koalabi Nov 10 '13 at 21:04
  • @krivachy.akos: "does the exact same thing": true, but the intent is different (and clearer) from the caller's perspective. Personally, I find "set" more correct than "copy". – koalabi Nov 12 '13 at 11:54
  • @koalabi `set` implies mutability. A caller could misinterpret it as this: `val m = Member(...); m.set(name = "Name")`. This is clearly incorrect - the correct call woud be: `val updatedM = m.set(name = "Name")`. So this action is essentially a `copy` and not a `set`. – Akos Krivachy Nov 12 '13 at 13:01
0

This compiles for me, if this is what you were trying to achieve:

  def set(name: String = name, firstname: Option[String] = firstname, birthDate : Option[Date] = birthDate)(implicit session: Session) = {
    val m = Member(id, name, firstname, birthDate, gender, country, healthNotes)
    Members.update(m)
    m
  }
...
member.set(firstname = Some("name"))   

I wouldn't recommend doing Database functions inside your case classes. We generally use case classes as simple data storing objects, and only define functions that help manage that data.

The updating of a Member should happen in your Member DAO class, probably the simplest solution would be something like this:

object MemberDao {
  def updateMemberDetails(member: Member, name: String, firstname: Option[String], birthdate : Option[Date]) = {
    val updatedMember = member.copy(name = name, firstname = firstname, birthDate = birthdate)
    Members.update(updatedMember)
  }
}

Another solution to handling modifiable classes is using a pattern along the lines of this:

case class Member(id: Int, name: String, firstname: Option[String] = None, birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None, healthNotes: Option[String]) {
  def updateName(n: String) = this.copy(name = n)
  def updateFirstName(fn: Option[String]) = this.copy(firstname = fn)
  def updateBirthDate(bd: Option[Date]) = this.copy(birthDate = bd)
}

And finally if you want to have a save function you could do a fluent style syntax injected through an implicit syntax, which always returns a new Member with a save function defined.

case class Member(id: Int, name: String, firstname: Option[String] = None, birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None, healthNotes: Option[String])

object Updatable {
  implicit class UpdatableMember(member: Member) {
    def updateName(n: String) = member.copy(name = n)
    def updateFirstName(fn: Option[String]) = member.copy(firstname = fn)
    def updateBirthDate(bd: Option[Date]) = member.copy(birthDate = bd)
    def save(implicit session: Session) = {
      ???
    }
  }
}

object MemberDao {
  def updateMember(member: Member) = {
    import Updatable._
    DB withSession { implicit session =>
      member.updateFirstName(Some("First Name")).updateBirthDate(Some(new Date(123456789))).save
    }
  }
}

Hope you find some of these options helpful. If I misunderstood what you're requirements are, then please comment!

Update

You don't necessarily have to have one update method for each member, but you could potentially do other logic there. If you want a simple solution then use Scala's built in copy function as you already did.

val member = MemberDao.getMember(123)
val updatedMember = member.copy(birthDate = Some(new Date(123456789)), name = "Updated name")
MemberDao.persist(updatedMember)

If this doesn't work for you, then please explain why.

My problem with having a save method in a class that essentially stores data is that it doesn't take into account the Seperation of Concerns. Also your set function has side-effects, that could confuse other developers (and isn't too functional). What if you would like to set only the firstname and then pass it onto another class to set the birthdate? Would you want to do two database transaction? I would guess not.

That's where the Data Access Object and its advantages come into play. You would place all your database operations for a case class into one class, thereby seperating concerns: your case class holds the data and has functions that only do operations on the data, and your DAO has methods that query/persist/update/delete your case classes. This also makes testing your application easier, as it's simpler to mock a DAO than a case class' save function.

Many people have differing opinions on DAO's, the statements are just my opinion.

Not using a DAO

I would recommend defining a trait for case classes that are updatable:

trait DatabaseAccess[T] {
  def create(implicit session: Session): T
  def update(implicit session: Session): T
  def delete(implicit session: Session): Boolean
}

case class Member(id: Int, name: String, firstname: Option[String] = None, birthDate: Option[Date] = None, gender: Option[String] = None, country: Option[String] = None, healthNotes: Option[String]) extends DatabaseAccess[Member] {
  def create(implicit session: Session): Member = ???
  def delete(implicit session: Session): Boolean = ???
  def update(implicit session: Session): Member = ???
}

These are just ideas, I don't think there's a right or wrong solution. Choose one that makes sense to you and gives you the necessary usability/flexibility ratio.

Best practices

You asked about Scala best practices. Your question is more of a software design issue, so reading up on general OO software design can help you. Scala's case classes are just a much much easier way of writing POJOs. So before adding any new function into a case class, ask yourself the question: "Would I put this in a POJO?". If yes, then go ahead :)

Community
  • 1
  • 1
Akos Krivachy
  • 4,915
  • 21
  • 28
  • Hello Krivachy.Achos, – koalabi Nov 10 '13 at 09:17
  • First of all, thanks for this detailed answer and the time taken to document it. These are for sure relevant and interesting comments and I will examine them more in detail. Regarding the code, yes it compiles but I'm afraid - based only on syntax highlighting :-(, I confess - that "name" is the same as the named parameter. That's why I was trying to qualify it. If my perception is true, the default value has no effect. – koalabi Nov 10 '13 at 09:33
  • Now, regarding "fluent" methods and DAO's, this is just what I wanted to avoid: what's the point in using case classes (which a.o. avoid the need for setters and getters "à la JavaBean") if one then has to implement 1 update method per member? Secondly - and this is a true question - where can one find best practices about case classes and database access? I mean: I own and re-read regularly "Programming in Scala, 2nd ed." (Odersky, Spoon and Venners) and I see nowhere that methods in case classes are discouraged. Their examples seem to show the contrary. – koalabi Nov 10 '13 at 09:33
  • Could you provide some pointers or elaborate on this? Thank in advance. – koalabi Nov 10 '13 at 09:34