[2.0-scala] Migrating from Anorm in 1.x to 2.x

1,047 views
Skip to first unread message

Matt Raible

unread,
May 30, 2012, 12:00:50 PM5/30/12
to play-fr...@googlegroups.com
Hello all,

I'm trying to migrate my Play 1.2.4 (with Scala) app to Play 2 Scala. The first issue I've run into is pagination. The only good example I've found is in the computer-database project. Is this the recommended solution or does someone have a simpler example (like the prevNext method in 1.x)?

My second issue is how to create the same method signature I had in one of my models from 1.x. In my Workout.scala class, I had the following method:

object Workout extends Magic[Workout] {

  def byIdWithAthleteAndComments(id: Long): Option[(Workout, Athlete, List[Comment])] =
    SQL(
      """
          select * from Workout w
          join Athlete a on w.athleteId = a.id
          left join Comment c on c.workoutId = w.id
          where w.id = {id}
      """
    ).on("id" -> id).as(Workout ~< Athlete ~< Workout.spanM(Comment) ^^ flatten ?)
}

In 2.x, I'm having issues getting this same method signature. I've gotten close, but the Comment is singular instead of a List. 

  val withAthleteAndComments = withAthlete ~ Comment.simple map {
    case workout~comments => (workout, comments)
  }


  def byIdWithAthleteAndComments(id: Long): List[((Workout, Athlete), Comment)] = DB.withConnection { implicit connection =>
    SQL(
      """
          select * from Workout w
          join Athlete a on w.athleteId = a.id
          left join Comment c on c.workoutId = w.id
          where w.id = {id}
      """
    ).on('id -> id).as(Workout.withAthleteAndComments *)
  }

You can find the full classes in the following gist. 

https://gist.github.com/2837139

Thanks in advance for any advice.

Cheers,

Matt

Julien Tournay

unread,
May 30, 2012, 12:06:50 PM5/30/12
to play-fr...@googlegroups.com
Hi

spanM was removed, you need to group results using groupBy (standard scala List api).
Here's an example extracted from one of my models (a Project has a List[Author]):

  val simple = {
      val rs =
        (get[Pk[Long]]("project.id") ~
        get[String]("project.description") ~
        get[String]("project.name") ~
        get[Int]("project.score") ~
        get[Boolean]("project.validated") ~
        get[String]("project.image") ~
        get[Option[String]]("project.url") ~
        get[String]("project.repo") ~
        Author.simple).map {
          case id ~ description ~ name ~ score ~ validated ~ image ~ url ~ repo ~ author
            => (id, description, name, score, validated, image, url, repo, author)
        } *;

    rs.map(r =>
        r.groupBy(_._1)
        .flatMap{ case (k, ps) =>
            ps.headOption.map{ p =>
                val (id, description, name, score, validated, image, url, repo, author) = p
                Project(id, name, description, repo, score, validated, image, ps.map(_._9), url)
            }
        }.toList
    )
    
  }


jto.

Julien Tournay

unread,
May 30, 2012, 12:09:29 PM5/30/12
to play-fr...@googlegroups.com
It could be slightly easier using flatten:

 val rs =
        (get[Pk[Long]]("project.id") ~
        get[String]("project.description") ~
        get[String]("project.name") ~
        get[Int]("project.score") ~
        get[Boolean]("project.validated") ~
        get[String]("project.image") ~
        get[Option[String]]("project.url") ~
        get[String]("project.repo") ~
        Author.simple).map {
          flatten
        } *;

Guillaume Bort

unread,
May 30, 2012, 12:26:24 PM5/30/12
to play-fr...@googlegroups.com
It should be something like:

val withAthleteAndComments = {
(Workout.withAthlete ~ Comment.simple *) map {
case all => all.groupBy(_._1).headOption.map {
case (workoutWithAthlete, comments) => (workout, athlete,
comment.map(_._2))
}
}
}

That will get you a Option[((Workout,Athlete),List[Comment])]

Btw the type is pretty complicated, it should be simpler by using a
case class instead, something like:

XXXX(workout: Workout, athlete: Athlete, comments: List[Comment])
> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To view this discussion on the web visit
> https://groups.google.com/d/msg/play-framework/-/yAzj7lEgyywJ.
> To post to this group, send email to play-fr...@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framewor...@googlegroups.com.
> For more options, visit this group at
> http://groups.google.com/group/play-framework?hl=en.



--
Guillaume Bort, http://guillaume.bort.fr

Matt Raible

unread,
May 31, 2012, 8:48:16 AM5/31/12
to play-fr...@googlegroups.com


On Wednesday, May 30, 2012 10:26:24 AM UTC-6, Guillaume Bort wrote:
It should be something like:

val withAthleteAndComments = {
  (Workout.withAthlete ~ Comment.simple *) map {
    case all => all.groupBy(_._1).headOption.map {
      case (workoutWithAthlete, comments) => (workout, athlete,
comment.map(_._2))
    }
  }
}

That will get you a Option[((Workout,Athlete),List[Comment])]

 
If I try that, I get errors about the lowercase variables:

error: not found: value workout
case (workoutWithAthlete, comments) => (workout, athlete, comment.map(_._2))

Changing them to uppercase seems to help, but not much:

  val withAthleteAndComments = {
    (withAthlete ~ Comment.simple *) map {
      case all => all.groupBy(_._1).headOption.map {
        case (workoutWithAthlete, comments) => (Workout, Athlete, Comment.map(_._2))
      }
    }
  }

error: value map is not a member of object models.Comment
case (workoutWithAthlete, comments) => (Workout, Athlete, Comment.map(_._2))

Thanks,

Matt
 
> To post to this group, send email to play-framework@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framework+unsubscribe@googlegroups.com.

Guillaume Bort

unread,
May 31, 2012, 10:38:15 AM5/31/12
to play-fr...@googlegroups.com
Oh yes, of course this is wrong. It should be something like:

case (workoutWithAthlete, comments) => (workoutWithAthlete, comment.map(_._2))

Btw, to explain what I'm trying to do here:

1) You already have a RowParser[(Workout,Athlete)] called
Workout.withAthlete that is able to extract a tuple (Workout,Athlete)
from a ResultSet row.

2) You already have a RowParser[Comment] called Comment.simple that is
able to extract a Commet from a ResultSet row.

3) Let's compose both to have a RowParser[(Workout,Athlete)~Comment]:

----
Workout.withAthlete ~ Comment.simple
----

4) Now, let's use this to construct a
ResultSetParser[List[(Workout,Athlete)~Comment]]. This is a parser
able to give me a List[(Workout,Athlete)~Comment] from a ResultSet:

----
(Workout.withAthlete ~ Comment.simple) *
----

This parser will give you something like:

----
List(
(myWorkout1,myAthlete1)~comment1,
(myWorkout1,myAthlete1)~comment2,
(myWorkout1,myAthlete1)~comment3
)
----

That is a simple representation of your JDBC ResultSet

5) Let's transform this
ResultSetParser[List[(Workout,Athlete)~Comment]] to a
ResultSetParser[Option[((Workout,Athlete),List[Comment]]] because this
is what you want.

We use the `map` operation to do that (it transforms a
ResultSetParser[A] to a ResultSetParser[B] given a A => B function).

We want to transform:

----
List(
(myWorkout1,myAthlete1)~comment1,
(myWorkout1,myAthlete1)~comment2,
(myWorkout1,myAthlete1)~comment3
)
----

to

----
Some( ( (myWorkout1,myAthlete1), List(comment1,comment2comment3) ) )
----

And

----
List()
----

to

----
None
----

Let's start by grouping by the first item:

----
items.groupBy(_._1)
----

So we get:

----
Map(
(myWorkout1,myAthlete1),
List(
(myWorkout1,myAthlete1)~comment1,
(myWorkout1,myAthlete1)~comment2,
(myWorkout1,myAthlete1)~comment3
)
)
----

Basically we get a Map with a single key if the query returned any
result, or an empty Map if the query returned no results.

Let's transform it to an Option:

----
items.groupBy(_._1).headOption
----

So we get:

----
Some(
(
(myWorkout1,myAthlete1),
List(
(myWorkout1,myAthlete1)~comment1,
(myWorkout1,myAthlete1)~comment2,
(myWorkout1,myAthlete1)~comment3
)
)
)
----

(or None if the Map was empty)

Now, we just want to filter out the repeated (myWorkout1,myAthlete1)
in the List to keep the comment only:

----
items.groupBy(_._1).headOption.map {
case (workoutWithAthlete, list) => (workoutWithAthlete, list.map(_._2))
}
----

And we finally get:

----
Some(
(
(myWorkout1,myAthlete1),
List(
comment1,
comment2,
comment3
)
)
)
----

Or None if the initial list was empty.

Finally, here is the whole stuff altogether that you can assign to a
value and reuse as a generic parser to parse the result of several
different queries:

----
val withAthleteAndComments = {
(Workout.withAthlete ~ Comment.simple *) map { items =>
items.groupBy(_._1).headOption.map {
case (workoutWithAthlete, list) => (workoutWithAthlete, list.map(_._2))
}
}
}
----
>> > To post to this group, send email to play-fr...@googlegroups.com.
>> > To unsubscribe from this group, send email to
>> > play-framewor...@googlegroups.com.
>> > For more options, visit this group at
>> > http://groups.google.com/group/play-framework?hl=en.
>>
>>
>>
>> --
>> Guillaume Bort, http://guillaume.bort.fr
>
> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To view this discussion on the web visit
> https://groups.google.com/d/msg/play-framework/-/EjkwtUA27nsJ.
>
> To post to this group, send email to play-fr...@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framewor...@googlegroups.com.

Matt Raible

unread,
Jun 1, 2012, 10:02:59 AM6/1/12
to play-fr...@googlegroups.com
Thanks Guillaume, I appreciate the detailed response.

Now I'm trying to get the following method to work with this parser:

  def allWithAthleteAndComments: List[((Workout, Athlete), List[Comment])] = DB.withConnection {
    implicit connection =>
      SQL(
        """
          select * from Workout w
          join Athlete a on w.athleteId = a.id
          left join Comment c on c.workoutId = w.id
          order by w.postedAt desc
        """
      ).as(Workout.withAthleteAndComments).toList
  }

This compiles, but I'm not sure it's correct. I have the following test:

  "Global" should {
    "load seed data into DB" in {
      running(FakeApplication(additionalConfiguration = inMemoryDatabase())) {

        InitialData.insert()

        Athlete.count() must be_==(2)
        Workout.count() must be_==(3)
        Comment.count() must be_==(3)

        Athlete.connect("mra...@gmail.com", "beer") should not be (None)
        Athlete.connect("trishm...@gmail.com", "whiskey") should not be (None)
        Athlete.connect("trishm...@gmail.com", "badpassword") must be_==(None)
        Athlete.connect("fr...@gmail.com", "secret") must be_==(None)

        val allWorkoutsWithAthleteAndComments = Workout.allWithAthleteAndComments

        allWorkoutsWithAthleteAndComments must be_==(3)
      }
    }
  }

Running it results in the following error:

[info] Global should
[error] ! load seed data into DB
[error]     RuntimeException: UnexpectedNullableFound(COMMENT.ID) (Predef.scala:66)
[error] anorm.Sql$.as(Anorm.scala:513)
[error] anorm.Sql$class.as(Anorm.scala:433)
[error] anorm.SqlQuery.as(Anorm.scala:459)
[error] models.Workout$$anonfun$allWithAthleteAndComments$1.apply(Workout.scala:83)
[error] models.Workout$$anonfun$allWithAthleteAndComments$1.apply(Workout.scala:75)
[error] play.api.db.DBApi$class.withConnection(DB.scala:77)
[error] play.api.db.BoneCPApi.withConnection(DB.scala:264)
[error] play.api.db.DB$$anonfun$withConnection$3.apply(DB.scala:156)
[error] play.api.db.DB$$anonfun$withConnection$3.apply(DB.scala:156)
[error] play.api.db.DB$.withConnection(DB.scala:156)
[error] models.Workout$.allWithAthleteAndComments(Workout.scala:74)
[error] UnitTests$$anonfun$3$$anonfun$apply$82$$anonfun$apply$83.apply(UnitTests.scala:110)
[error] UnitTests$$anonfun$3$$anonfun$apply$82$$anonfun$apply$83.apply(UnitTests.scala:97)
[error] play.api.test.Helpers$.running(Helpers.scala:33)
[error] UnitTests$$anonfun$3$$anonfun$apply$82.apply(UnitTests.scala:97)
[error] UnitTests$$anonfun$3$$anonfun$apply$82.apply(UnitTests.scala:97)

Any advice is appreciated.

Thanks,

Matt
>> > To post to this group, send email to play-framework@googlegroups.com.
>> > To unsubscribe from this group, send email to
>> > For more options, visit this group at
>> > http://groups.google.com/group/play-framework?hl=en.
>>
>>
>>
>> --
>> Guillaume Bort, http://guillaume.bort.fr
>
> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To view this discussion on the web visit
> https://groups.google.com/d/msg/play-framework/-/EjkwtUA27nsJ.
>
> To post to this group, send email to play-framework@googlegroups.com.
> To unsubscribe from this group, send email to

Guillaume Bort

unread,
Jun 1, 2012, 12:28:37 PM6/1/12
to play-fr...@googlegroups.com
Well, by using a "left join" for the comments, you should expect null
values there (because it will select Workout and Athlete without any
comments). So you need to handle Comment parsing as optional:

Workout.withAthlete ~ (Comment.simple?)

Then you will have to manage Option[Comment] values instead. It is
really easy to transform a List[Option[Comment]] into a List[Comment]
by filtering undefined value using `flatten`.

You should end with something like this:

val withAthleteAndComments = {
(Workout.withAthlete ~ (Comment.simple?) *) map { items =>
items.groupBy(_._1).headOption.map {
case (workoutWithAthlete, list) => (workoutWithAthlete,
list.map(_._2).flatten)
>> >> > To post to this group, send email to play-fr...@googlegroups.com.
>> >> > To unsubscribe from this group, send email to
>> >> > play-framewor...@googlegroups.com.
>> >> > For more options, visit this group at
>> >> > http://groups.google.com/group/play-framework?hl=en.
>> >>
>> >>
>> >>
>> >> --
>> >> Guillaume Bort, http://guillaume.bort.fr
>> >
>> > --
>> > You received this message because you are subscribed to the Google
>> > Groups
>> > "play-framework" group.
>> > To view this discussion on the web visit
>> > https://groups.google.com/d/msg/play-framework/-/EjkwtUA27nsJ.
>> >
>> > To post to this group, send email to play-fr...@googlegroups.com.
>> > To unsubscribe from this group, send email to
>> > play-framewor...@googlegroups.com.
>> > For more options, visit this group at
>> > http://groups.google.com/group/play-framework?hl=en.
>>
>>
>>
>> --
>> Guillaume Bort, http://guillaume.bort.fr
>
> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To view this discussion on the web visit
> https://groups.google.com/d/msg/play-framework/-/y3tooIpyIk4J.
>
> To post to this group, send email to play-fr...@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framewor...@googlegroups.com.

Matt Raible

unread,
Jun 4, 2012, 9:33:36 AM6/4/12
to play-fr...@googlegroups.com
I still get the same error after this change:

  lazy val withAthleteAndComments = {
    (Workout.withAthlete ~ (Comment.simple?) *) map {
      items =>
        items.groupBy(_._1).headOption.map {
          case (workoutWithAthlete, list) => (workoutWithAthlete, list.map(_._2).flatten)
        }
    }
  }

  def allWithAthleteAndComments: List[((Workout, Athlete), List[Comment])] = DB.withConnection {
    implicit connection =>
      SQL(
        """
          select * from Workout w
          join Athlete a on w.athleteId = a.id
          left join Comment c on c.workoutId = w.id
          order by w.postedAt desc
        """
      ).as(Workout.withAthleteAndComments).toList
  }

[info] Global should
[error] ! load seed data into DB
[error]     RuntimeException: UnexpectedNullableFound(comment.id) (Predef.scala:66)
[error] anorm.Sql$.as(Anorm.scala:513)
[error] anorm.Sql$class.as(Anorm.scala:433)
[error] anorm.SqlQuery.as(Anorm.scala:459)
[error] models.Workout$$anonfun$allWithAthleteAndComments$1.apply(Workout.scala:83)
[error] models.Workout$$anonfun$allWithAthleteAndComments$1.apply(Workout.scala:75)
[error] play.api.db.DBApi$class.withConnection(DB.scala:77)
[error] play.api.db.BoneCPApi.withConnection(DB.scala:264)
[error] play.api.db.DB$$anonfun$withConnection$3.apply(DB.scala:156)
[error] play.api.db.DB$$anonfun$withConnection$3.apply(DB.scala:156)
[error] play.api.db.DB$.withConnection(DB.scala:156)

>         Athlete.connect("mraible@gmail.com", "beer") should not be (None)
>         Athlete.connect("trishmcginity@gmail.com", "whiskey") should not be
> (None)
>         Athlete.connect("trishmcginity@gmail.com", "badpassword") must
> be_==(None)
>         Athlete.connect("fred@gmail.com", "secret") must be_==(None)
>> >> > To post to this group, send email to play-framework@googlegroups.com.
>> >> > To unsubscribe from this group, send email to
>> >> > play-framework+unsubscribe@googlegroups.com.
>> >> > For more options, visit this group at
>> >> > http://groups.google.com/group/play-framework?hl=en.
>> >>
>> >>
>> >>
>> >> --
>> >> Guillaume Bort, http://guillaume.bort.fr
>> >
>> > --
>> > You received this message because you are subscribed to the Google
>> > Groups
>> > "play-framework" group.
>> > To view this discussion on the web visit
>> > https://groups.google.com/d/msg/play-framework/-/EjkwtUA27nsJ.
>> >
>> > To post to this group, send email to play-framework@googlegroups.com.
>> > To unsubscribe from this group, send email to
>> > play-framework+unsubscribe@googlegroups.com.
>> > For more options, visit this group at
>> > http://groups.google.com/group/play-framework?hl=en.
>>
>>
>>
>> --
>> Guillaume Bort, http://guillaume.bort.fr
>
> --
> You received this message because you are subscribed to the Google Groups
> "play-framework" group.
> To view this discussion on the web visit
> https://groups.google.com/d/msg/play-framework/-/y3tooIpyIk4J.
>
> To post to this group, send email to play-framework@googlegroups.com.
> To unsubscribe from this group, send email to
> play-framework+unsubscribe@googlegroups.com.

Guillaume Bort

unread,
Jun 4, 2012, 1:38:25 PM6/4/12
to play-fr...@googlegroups.com
Can you share your database creation script? I don't see why it would fail this way. 
To view this discussion on the web visit https://groups.google.com/d/msg/play-framework/-/a1u-8CvLuDoJ.
To post to this group, send email to play-fr...@googlegroups.com.
To unsubscribe from this group, send email to play-framewor...@googlegroups.com.

Matt Raible

unread,
Jun 4, 2012, 3:07:40 PM6/4/12
to play-fr...@googlegroups.com
Here's my evolution to create the table:

# --- !Ups

CREATE TABLE Comment (
    id SERIAL PRIMARY KEY,
    author varchar(255) NOT NULL,
    content text NOT NULL,
    postedAt timestamp NOT NULL,
    workoutId bigint NOT NULL REFERENCES Workout ON DELETE CASCADE
);

# --- !Downs

DROP TABLE Comment;

And my Comment class:

package models

import anorm._
import anorm.SqlParser._

import java.util.Date

import play.api.db.DB
import play.api.Play.current

case class Comment(
  id: Pk[Long],
  author: String, content: String, postedAt: Date, workoutId: Long
)

object Comment {
  def apply(workoutId: Long, author: String, content: String) = {
    new Comment(NotAssigned, author, content, new Date(), workoutId)
  }

  val simple = {
    get[Pk[Long]]("comment.id") ~
    get[String]("comment.author") ~
    get[String]("comment.content") ~
    get[Long]("comment.postedAt") ~
    get[Long]("comment.workoutId") map {
      case id~author~content~postedAt~workoutId =>
        Comment(id, author, content, new Date(postedAt), workoutId)
    }
  }

  def count(): Long = {
    DB.withConnection {
      implicit connection =>
        SQL("select count(*) from comment").as(scalar[Long].single)
    }
  }

  def create(comment: Comment) = {
    DB.withConnection {
      implicit connection =>
        SQL("insert into comment (author, content, postedAt, workoutId) values " +
          "({author}, {content}, {postedAt}, {workoutId})").on(
          'author -> comment.author,
          'content -> comment.content,
          'postedAt -> new Date(),
          'workoutId -> comment.workoutId
        ).executeInsert()
    }
  }
}

Matt Raible

unread,
Jun 5, 2012, 1:58:19 AM6/5/12
to play-fr...@googlegroups.com
I was able to solve this using the Yabe project that's been updated for Play 2:


Since I used Play 1's Yabe example to learn, this was a great help in seeing how to do the same thing in Play 2.
Reply all
Reply to author
Forward
0 new messages