I'm using the play-silhouette-slick seed for a project and I need to add users to different tables depending on their type. However, if I change the TableQuery used to insertUpdate the user in UserDAOImpl.save, a SecuredAction further down the line (for ApplicationController.index) does not execute, suggesting that the user hasn't been authenticated. So by swapping this line of code;
_ <- slickUsers.insertOrUpdate(dbUser)
To this;
_ <-slickAdministrators.insertOrUpdate(dbUser)
My SecuredAction doesn't execute. Can anyone point me in the right direction please? More of my code is below;
class UserDAOImpl extends UserDAO with DAOSlick {
import driver.api._
def find(loginInfo: LoginInfo) = {
getUserTypeAndID(loginInfo) match {
case Some((id, userType)) if userType.equals("administrator") => val userQuery = for {
dbUser <- slickUsers.filter(_.userID === id)
} yield dbUser
db.run(userQuery.result.headOption).map { dbUserOption =>
dbUserOption.map { user =>
Administrator(UUID.fromString(user.userID), loginInfo, user.title, user.firstName, user.lastName, user.email)
}
}
case None => val userQuery = for {
dbLoginInfo <- loginInfoQuery(loginInfo)
dbUser <- slickUsers.filter(_.userID === dbLoginInfo.userID)
} yield dbUser
db.run(userQuery.result.headOption).map { dbUserOption =>
dbUserOption.map { user =>
Administrator(UUID.fromString(user.userID), loginInfo, user.title, user.firstName, user.lastName, user.email)
}
}
}
def getUserTypeAndID(loginInfo: LoginInfo) = {
val userQuery = for {
dbLoginInfo <- loginInfoQuery(loginInfo)
} yield dbLoginInfo
val dbresult = db.run(userQuery.result.headOption)
val userOption = Await.result(dbresult, 5 second)
userOption map {info => (info.userID, info.userType)}
}
def save(user: User) = {
val dbUser = DBUser(user.userID.toString, user.title, user.firstName, user.lastName, user.email)
val userType = user.getClass.getTypeName match {
case "models.Administrator" => "administrator"
}
val loginInfoAction = {
val retrieveLoginInfo = slickLoginInfos.filter(
info => info.providerID === user.loginInfo.providerID &&
info.providerKey === user.loginInfo.providerKey).result.headOption
val insertLoginInfo = slickLoginInfos += DBLoginInfo(dbUser.userID, user.loginInfo.providerID, user.loginInfo.providerKey, userType)
for {
loginInfoOption <- retrieveLoginInfo
loginInfo <- loginInfoOption.map(DBIO.successful(_)).getOrElse(insertLoginInfo)
} yield loginInfo
}
val actions = (for {
_ <- slickAdministrators.insertOrUpdate(dbUser)
loginInfo <- loginInfoAction
} yield ()).transactionally
// run actions and return user afterwards
db.run(actions).map(_ => user)
}
}
DBTableDefinitions
trait DBTableDefinitions {
protected val driver: JdbcProfile
import driver.api._
case class DBUser (
userID: String,
title: Option[String],
firstName: Option[String],
lastName: Option[String],
email: Option[String]
)
class Users(tag: Tag) extends Table[DBUser](tag, "user") {
def userID = column[String]("userid", O.PrimaryKey)
def title = column[Option[String]]("title")
def firstName = column[Option[String]]("firstname")
def lastName = column[Option[String]]("lastname")
def email = column[Option[String]]("email")
def * = (userID, title, firstName, lastName, email) <> (DBUser.tupled, DBUser.unapply)
}
class Administrators(tag: Tag) extends Table[DBUser](tag, "administrators") {
def userID = column[String]("userid", O.PrimaryKey)
def title = column[Option[String]]("title")
def firstName = column[Option[String]]("firstname")
def lastName = column[Option[String]]("lastname")
def email = column[Option[String]]("email")
def * = (userID, title, firstName, lastName, email) <> (DBUser.tupled, DBUser.unapply)
}
case class DBLoginInfo (
userID: String,
providerID: String,
providerKey: String,
userType: String
)
class LoginInfos(tag: Tag) extends Table[DBLoginInfo](tag, "logininfo") {
def userID = column[String]("userid", O.PrimaryKey)
def providerID = column[String]("providerid")
def providerKey = column[String]("providerkey")
def userType = column[String]("usertype")
def * = (userID, providerID, providerKey, userType) <> (DBLoginInfo.tupled, DBLoginInfo.unapply)
}
case class DBPasswordInfo (
hasher: String,
password: String,
userID: String
)
class PasswordInfos(tag: Tag) extends Table[DBPasswordInfo](tag, "passwordinfo") {
def hasher = column[String]("hasher")
def password = column[String]("password")
def userID = column[String]("userid")
def * = (hasher, password, userID) <> (DBPasswordInfo.tupled, DBPasswordInfo.unapply)
}
// table query definitions
val slickUsers = TableQuery[Users]
val slickAdministrators = TableQuery[Administrators]
val slickLoginInfos = TableQuery[LoginInfos]
val slickPasswordInfos = TableQuery[PasswordInfos]
// queries used in multiple places
def loginInfoQuery(loginInfo: LoginInfo) =
slickLoginInfos.filter(dbLoginInfo => dbLoginInfo.providerID === loginInfo.providerID && dbLoginInfo.providerKey === loginInfo.providerKey)
}
SignUpController
class SignUpController #Inject() (
val messagesApi: MessagesApi,
val env: Environment[User, CookieAuthenticator],
userService: UserService,
authInfoRepository: AuthInfoRepository,
passwordHasher: PasswordHasher)
extends Silhouette[User, CookieAuthenticator] {
/**
* Registers a new user.
*
* #return The result to display.
*/
def signUp(userType: String) = Action.async { implicit request =>
SignUpForm.form.bindFromRequest.fold(
form => Future.successful(BadRequest(views.html.signUp(form))),
data => {
val loginInfo = LoginInfo(CredentialsProvider.ID, data.email)
userService.retrieve(loginInfo).flatMap {
case Some(user) =>
Future.successful(Redirect(routes.ApplicationController.signUp()).flashing("error" -> Messages("user.exists")))
case None =>
val authInfo = passwordHasher.hash(data.password)
val user = getUser(userType, data, loginInfo)
for {
user <- userService.save(user)
authInfo <- authInfoRepository.add(loginInfo, authInfo)
//shouldn't need below data -> it creates cookie info to continue as the user added
authenticator <- env.authenticatorService.create(loginInfo)
value <- env.authenticatorService.init(authenticator)
result <- env.authenticatorService.embed(value, Redirect(routes.ApplicationController.index()))
} yield {
env.eventBus.publish(SignUpEvent(user, request, request2Messages))
env.eventBus.publish(LoginEvent(user, request, request2Messages))
result
}
}
}
)
}
/**
* Creates a new user according to the specified userType.
*
* #param userType the type of user required
* #param data the data from the SignUpForm
* #param loginInfo the users loginInfo
* #return an instance of a User.
*/
def getUser(userType: String, data: SignUpForm.Data, loginInfo: LoginInfo): User = userType match{
case "administrator" => Administrator(
userID = UUID.randomUUID(),
loginInfo = loginInfo,
title = Some(data.title),
firstName = Some(data.firstName),
lastName = Some(data.lastName),
email = Some(data.email)
)
}
}
ApplicationController
class ApplicationController #Inject() (
val messagesApi: MessagesApi,
val env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* Handles the index action.
*
* #return The result to display.
*/
def index = SecuredAction.async { implicit request =>
Future.successful(Ok(views.html.home(request.identity)))
}
/**
* Handles the Sign In action.
*
* #return The result to display.
*/
def signIn = UserAwareAction.async { implicit request =>
request.identity match {
case Some(user) => Future.successful(Redirect(routes.ApplicationController.index()))
case None => Future.successful(Ok(views.html.signIn(SignInForm.form)))
}
}
/**
* Handles the Sign Up action.
*
* #return The result to display.
*/
def signUp = UserAwareAction.async { implicit request =>
request.identity match {
case Some(user) => Future.successful(Redirect(routes.ApplicationController.index()))
case None => Future.successful(Ok(views.html.signUp(SignUpForm.form)))
}
}
/**
* Handles the Sign Out action.
*
* #return The result to display.
*/
def signOut = SecuredAction.async { implicit request =>
val result = Redirect(routes.ApplicationController.index())
env.eventBus.publish(LogoutEvent(request.identity, request, request2Messages))
env.authenticatorService.discard(request.authenticator, result)
}
}
The code above works if I change that single line in UserDAOImpl.save.
The problem was that I had not implemented a method in UserDAOImpl to find a user in the administrators table according to their LoginInfo. So by adding this;
def find(loginInfo: LoginInfo) = {
getUserTypeAndID(loginInfo) match {
case Some((id, userType)) if userType.equals("administrator") => val userQuery = for {
dbUser <- slickAdministrators.filter(_.userID === id)
} yield dbUser
db.run(userQuery.result.headOption).map { dbUserOption =>
dbUserOption.map { user =>
Administrator(UUID.fromString(user.userID), loginInfo, user.title, user.firstName, user.lastName, user.email)
}
}
case None => val userQuery = for {
dbLoginInfo <- loginInfoQuery(loginInfo)
dbUser <- slickUsers.filter(_.userID === dbLoginInfo.userID)
} yield dbUser
db.run(userQuery.result.headOption).map { dbUserOption =>
dbUserOption.map { user =>
Administrator(UUID.fromString(user.userID), loginInfo, user.title, user.firstName, user.lastName, user.email)
}
}
}
ie, the line
dbUser <- slickAdministrators.filter(_.userID === id)
The SecuredAction now executes as expected for a valid user. I didn't realise that this method was called for authentication for a SecuredAction (or maybe I'm wrong on this and it's being called from somewhere else), so if anyone can explain further it would be really helpful - this has taken me all day to sort!
Related
I am working on a Library data model where each book can have multiple authors and vice versa (many to many).
I want to pass a list of books to a html view page that each book includes a list of its author(s).
To do that I have defined the following tables for book and authors:
private class BookTable(tag: Tag) extends Table[Book](tag, "book") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def publishDate = column[Date]("publish_date")
def memberId = column[Option[Long]]("member_id")
def member = foreignKey("member_fk",memberId,members)(_.id)
type Data = (Long, String, Date, Option[Long])
def constructBook: Data => Book = {
case (id, name, publishDate, memberId) =>
Book(id, name, publishDate, memberId)
}
def extractBook: PartialFunction[Book, Data] = {
case Book(id, name, publishDate, memberId, _) =>
(id, name, publishDate, memberId)
}
def * = (id, name, publishDate, memberId) <> (constructBook, extractBook.lift)
}
private class AuthorBookTable (tag: Tag) extends Table[AuthorBook](tag, "author_book") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def authorId = column[Long]("author_id")
def bookId = column[Long]("book_id")
def memberId = column[Option[Long]]("member_id")
def author = foreignKey("author_fk",authorId,authors)(_.id)
def book = foreignKey("book_fk",bookId,books)(_.id)
def * = (id, authorId, bookId) <> ((AuthorBook.apply _).tupled, AuthorBook.unapply)
}
private class AuthorTable (tag: Tag) extends Table[Author](tag, "author") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = (id, name) <> ((Author.apply _).tupled, Author.unapply)
}
The book case class is as below:
case class Book(id: Long, name: String, publishDate: Date, memberId: Option[Long] = None, authors: Seq[Author]= Seq.empty)
{
def updateAuthors(authorss: Seq[Author]) = {
this.copy(authors=authorss)
}
}
In controller I use the below:
def getBooks = Action.async { implicit request =>
repo.getBooks.map { books =>
val booksWithAuthors=books.map( b=> {val updateB=b.updateAuthors( repo.getBookAuthors(b.id))
updateB})
Ok(Json.toJson(booksWithAuthors))
}
}
My question is about the getBookAuthors implementation shown below:
implicit def waitForFuture[A](f:Future[A]) = {
def res: A = Await.result(f, Duration.Inf)
res
}
def getBookAuthors(id: Long): Seq[Author] = {
val result=db.run {
val innerJoin = for {
(ab, a) <- authorBooks join authors on (_.authorId === _.id)
} yield (a, ab.bookId)
innerJoin.filter(_._2 === id).sortBy(_._1.name).map(_._1).result
}
waitForFuture(result)
}
My concern is that the getBookAuthors function is blocking and I am not sure if it's the best practice. Please advise if there is a better way to do this.
As you are saying, blocking methods are pretty bad in this context and you will lost the advantages of using a non-blocking library as Slick.
the getBookAuthors would be written as follows, returning a Future[Seq[Author]] thats needs to be managed in the caller
def getBookAuthors(id: Long): Future[Seq[Author]] =
db.run {
val innerJoin = for {
(ab, a) <- authorBooks join authors on (_.authorId === _.id)
} yield (a, ab.bookId)
innerJoin.filter(_._2 === id).sortBy(_._1.name).map(_._1).result
}
So the caller should be rewritten as:
def getBooks = Action.async { implicit request =>
repo.getBooks.flatMap { books =>
Future.sequence(
books.map { b =>
repo.getBookAuthors(b.id).map(authors => b.updateAuthors(authors))
}
).map { booksWithAuthors =>
Ok(Json.toJson(booksWithAuthors))
}
}
}
This means that, once you will have the books: Seq[Book] you will map over it to integrate the authors and this will end with a Seq[Future[Book]].
Then it can be transformed into a Future[Seq[Book]] (with authors) with the Future.sequence method.
Finally you need to flatMap on the outer Future to move from Future[Future[Seq[Book]]] to a simpler Future[Seq[Book]]
This second snippet can be refactored in a more clean way taking advantage of the for-comprehension that is a syntactic sugar for the flatMap
private def addAuthorsToBooks(books: Seq[Book]): Future[Seq[Book]] =
Future.sequence(
books.map { b =>
repo.getBookAuthors(b.id).map(authors => b.updateAuthors(authors))
}
)
def getBooks = Action.async { implicit request =>
for {
books <- repo.getBooks
booksWithAuthors <- addAuthorsToBooks(books)
} yield Ok(Json.toJson(booksWithAuthors))
}
I am developing a restful web service with play in scala and slick. I am trying to implement filtering. The only thing i cannot get working is to set the operator for the comparing operation dynamically. I want to support ===, =!=, <=, >= and LIKE.
I have a problem in the filteringOperator function with the generics but i cannot find it. The exception is as follows:
value === is not a member of HardwareRepo.this.dbConfig.driver.api.Rep[A]
Anybody an idea?
def filtering(exp: FilterExpression): (HardwareTable) => Rep[Option[Boolean]] = {
exp.field match {
case "serialNumber" => filteringOperator(exp, _.serialNumber, exp.value)
case "online" => filteringOperator(exp, _.online, Try(exp.value.toBoolean).getOrElse(false))
case _ => throw new FilterFieldNotSupportedException(s"Filter field '${exp.field}' not supported")
}
}
def filteringOperator[A](exp: FilterExpression, x: Rep[A], y: A): Rep[Option[Boolean]] = {
exp.operator match {
case FilterExpressionOperator.Equals => x === y
...
}
}
def all(offset: Int, size: Int, filter: List[FilterExpression]): Future[List[Hardware]] = {
var q = Hardwares.to[List]
//filtering
for (exp <- filter) {
try {
q = q.filter(filtering(exp))
} catch {
case ex: FilterFieldNotSupportedException => return Future.failed(ex)
}
}
//pagination
db.run(q.drop(offset).take(size).result)
}
whole sourcecode:
package models
import java.sql.Timestamp
import java.util.UUID
import javax.inject.Inject
import helper.{FilterExpression, FilterExpressionOperator, SortExpression, SortExpressionOrder}
import play.api.db.slick.DatabaseConfigProvider
import helper.exceptions.{EntityAlreadyExistsException, FilterFieldNotSupportedException, SortFieldNotSupportedException}
import slick.driver.JdbcProfile
import slick.lifted.ColumnOrdered
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Try
case class Hardware(id: UUID,
createdAt: Timestamp,
serialNumber: String,
color: Int,
online: Boolean,
renterCount: Int)
class HardwareRepo #Inject()()(protected val dbConfigProvider: DatabaseConfigProvider) {
val dbConfig = dbConfigProvider.get[JdbcProfile]
val db = dbConfig.db
import dbConfig.driver.api._
private val Hardwares = TableQuery[HardwareTable]
val allowedSorting = Map( "id" -> { (hw: Hardware) => hw.id } )
private def _findById(id: UUID): DBIO[Option[Hardware]] =
Hardwares.filter(_.id === id).result.headOption
private def _findBySerialNumber(serialNumber: String): Query[HardwareTable, Hardware, List] =
Hardwares.filter(_.serialNumber === serialNumber).to[List]
def findById(id: UUID): Future[Option[Hardware]] =
db.run(_findById(id))
def findBySerialNumber(serialNumber: String): Future[List[Hardware]] =
db.run(_findBySerialNumber(serialNumber).result)
def sorting(exp: SortExpression): (HardwareTable) => ColumnOrdered[_] = {
exp.field match {
case "id" => if (exp.order == SortExpressionOrder.Asc) _.id.asc else _.id.desc
case "serialNumber" => if (exp.order == SortExpressionOrder.Asc) _.serialNumber.asc else _.serialNumber.desc
case "createdAt" => if (exp.order == SortExpressionOrder.Asc) _.createdAt.asc else _.createdAt.desc
case "color" => if (exp.order == SortExpressionOrder.Asc) _.color.asc else _.color.desc
case "online" => if (exp.order == SortExpressionOrder.Asc) _.online.asc else _.online.desc
case _ => throw new SortFieldNotSupportedException(s"Sort field '${exp.field}' not supported")
}
}
def filtering(exp: FilterExpression): (HardwareTable) => Rep[Option[Boolean]] = {
exp.field match {
case "serialNumber" => _.serialNumber === exp.value
case "online" => _.online === Try(exp.value.toBoolean).getOrElse(false)
case _ => throw new FilterFieldNotSupportedException(s"Filter field '${exp.field}' not supported")
}
}
def filteringOperator[A](exp: FilterExpression, x: Rep[A], y: A): Rep[Option[Boolean]] = {
exp.operator match {
case FilterExpressionOperator.Equals => x === y
}
}
def all(offset: Int, size: Int, sort: List[SortExpression], filter: List[FilterExpression]): Future[List[Hardware]] = {
var q = Hardwares.to[List]
//sorting
for (exp <- sort) {
try {
q = q.sortBy(sorting(exp))
} catch {
case ex: SortFieldNotSupportedException => return Future.failed(ex)
}
}
//filtering
for (exp <- filter) {
try {
q = q.filter(filtering(exp))
} catch {
case ex: FilterFieldNotSupportedException => return Future.failed(ex)
}
}
//pagination
db.run(q.drop(offset).take(size).result)
}
def exists(serialNumber: String): Future[Boolean] = {
db.run(Hardwares.filter(hw => hw.serialNumber === serialNumber).exists.result)
}
def create(serialNumber: Option[String]): Future[UUID] = {
if (serialNumber.isEmpty) {
db.run(Hardwares.map(hw => (hw.color)) returning Hardwares.map(_.id) += (0))
} else {
//check serial number
val action = (Hardwares.filter(hw => hw.serialNumber === serialNumber).exists.result.flatMap {
case true => DBIO.failed(new EntityAlreadyExistsException("Serialnumber already exists"))
case false => Hardwares.map(hw => (hw.color, hw.serialNumber)) returning Hardwares.map(_.id) += (0, serialNumber.get)
}).transactionally
db.run(action)
}
}
class HardwareTable(tag: Tag) extends Table[Hardware](tag, "hardware") {
def id = column[UUID]("id", O.PrimaryKey)
def createdAt = column[Timestamp]("created_at")
def serialNumber = column[String]("serial_number")
def color = column[Int]("color")
def online = column[Boolean]("online")
def renterCount = column[Int]("renter_count")
def * = (id, createdAt, serialNumber, color, online, renterCount) <> (Hardware.tupled, Hardware.unapply)
def ? = (id.?, createdAt.?, serialNumber.?, color.?, online.?, renterCount.?).shaped.<>({ r => import r._; _1.map(_ => Hardware.tupled((_1.get, _2.get, _3.get, _4.get, _5.get, _6.get))) }, (_: Any) => throw new Exception("Inserting into ? projection not supported."))
}
}
to resolve the error
value === is not a member of
HardwareRepo.this.dbConfig.driver.api.Rep[A]
for the === operator to work the profile api must be imported and available in scope. I would refactor the class HardwareRepo as below
class HardwareRepo #Inject() (protected val dbConfigProvider: DatabaseConfigProvider) {
with HasDatabaseConfigProvider[JdbcProfile] {
import driver.api._
/*
other repo methods
*/
}
I'm scala beginner and for now I'm trying to build a basic play/slick app (sort of user database).
It seems I've been able to build up all the stuff but data transferring to the front-end.
Here is what I have:
UserDAO.scala
class UserDao #Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends BaseDao {
import driver.api._
def entities = TableQuery[UsersTable]
def all(): Future[Seq[User]] = {
db.run(entities.result)
}
class UsersTable(tag: Tag) extends BaseTable(tag, "USER") {
def email = column[String]("email")
def password = column[String]("password")
def * = (id, email, password) <> (User.tupled, User.unapply)
}
}
Application.scala
Application #Inject()(userDAO: UserDao) extends Controller {
def users = Action.async {
val userList = userDAO.all()
userList
.map { list => Ok(list.map(elem => Json.toJson(elem : UserDto))) }
.recover { case _ => InternalServerError }
}
}
UserDTO.scala
case class UserDto(id: Long, login: String)
object UserDto {
implicit val userWriter = Json.writes[UserDto]
implicit def from(user: User): UserDto = UserDto(user.id, user.login)
}
What I don't understand is why compiler complains about .map { list => Ok(list.map(elem => Json.toJson(elem : UserDto))) } in Application.scala. It seems that I provided everything required for conversion to json. Could please, anybody, show what I'm doing wrong?
Replace Ok(list.map(elem => Json.toJson(elem : UserDto))) with Json.toJson(list: Seq[UserDto])
Application #Inject()(userDAO: UserDao) extends Controller {
def users = Action.async {
val userList = userDAO.all()
userList
.map { list => Ok(Json.toJson(list: Seq[UserDto])) }
.recover { case _ => InternalServerError }
}
}
I've used the Slick 3.1 code generator to create the default object and trait Tables.scala
The below method works however I would like to implicitly or explicitly convert UserRow and PasswordsRow to User and UserPassword.
Working method:
override def getUser(email: String): Future[Option[(Tables.UsersRow, Tables.PasswordsRow)]] = db.run {
(for {
user <- users if user.email === email
password <- passwords if password.id === user.id
} yield (user, password)).result.headOption
}
Desired method:
override def getUser(email: String): Future[Option[(User, UserPassword)]] = db.run {
(for {
user <- users if user.email === email
password <- passwords if password.id === user.id
} yield (user, password)).result.headOption
}
User.scala
package model
import com.wordnik.swagger.annotations.{ ApiModel, ApiModelProperty }
import slick.jdbc.GetResult
import spray.json.DefaultJsonProtocol
import scala.annotation.meta.field
case class User(
id: Int,
email: String,
name: Option[String] = None,
surname: Option[String] = None,
passwordId: Option[Int] = None
)
object User extends DefaultJsonProtocol{
implicit val getUserResult = GetResult(r => User(r.<<, r.<<, r.<<, r.<<, r.<<))
implicit val userFormat = jsonFormat5(User.apply)
}
UserPassword.scala
package model
import com.github.t3hnar.bcrypt.{Password, generateSalt}
import slick.jdbc.GetResult
case class UserPassword(id: Int, hashedPassword: Option[String], salt: String = generateSalt) {
def passwordMatches(password: String): Boolean = hashedPassword.contains(password.bcrypt(salt))
}
object UserPassword {
implicit val getUserPasswordResult = GetResult(r => UserPassword(r.<<, r.<<, r.<<))
def newWithPassword(password: String) = {
val salt = generateSalt
new UserPassword(0, Some(password.bcrypt(salt)), salt)
}
}
Someting like this maybe?
val futureUserRowAndPwdRow = getUser(email)
val futureUser: Future[Option[(User, UserPassword)]] = futureUserRowAndPwdRow map {
maybeUserRow => maybeUserRow map {
case (userRow, pwdRow) => (User(userRow.whatever....), UserPassword(..))
}
}
I have a Many to Many relationship setup like this:
Person <-> PersonField <-> Field
Now I want to query not only all the fields of a Person (I can do that), but a joined version of PersonField with Field of a Person. (I want to query/retrieve the Information in the Pivot/Intermediate Table "PersonField" as well!)
Person:
case class Person(id: Long, name: String)
{
def fields =
{
person <- Persons.all.filter(_.id === this.id)
field <- person.fields
} yield field
}
class Persons(tag: Tag) extends Table[Person](tag, "persons")
{
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = (id, name) <> (Person.tupled, Person.unapply)
def fields = PersonFields.all.filter(_.personID === id).flatMap(_.fieldFK)
}
object Persons
{
lazy val all = TableQuery[Persons]
}
Field:
case class Field(id: Long, name: String, description: Option[String])
class Fields(tag: Tag) extends Table[Field](tag, "fields")
{
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def description = column[Option[String]]("description")
def * = (id, name, description) <> (Field.tupled, Field.unapply)
}
object Fields
{
lazy val all = TableQuery[Fields]
}
PersonField:
case class PersonField(id: Long, personID: Long, fieldID: Long, value: String)
// TODO add constraint to make (personID, fieldID) unique
class PersonFields(tag: Tag) extends Table[PersonField](tag, "person_field")
{
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def personID = column[Long]("person_id")
def fieldID = column[Long]("field_id")
def value = column[String]("value")
def * = (id, personID, fieldID, value) <> (PersonField.tupled, PersonField.unapply)
def personFK = foreignKey("person_fk", personID, Persons.all)(_.id)
def fieldFK = foreignKey("field_fk", fieldID, Fields.all)(_.id)
}
object PersonFields
{
lazy val all = TableQuery[PersonFields]
}
Now to query all the fields of a Person I have a little helper-class:
def getFields(p: Person): Future[Seq[Field]] =
{
val query = p.fields
db.run(query.result)
}
So I can do
val personX ...
personX.onSuccess
{
case p: Person =>
{
val fields = helper.getFields(p)
fields.onSuccess
{
case f: Seq[Field] => f foreach println
}
}
}
Now each field of personX gets printed to the console. Works like a charm.
The thing is, I want to get the PersonField as well (with the Field)!
So I tried the following changes (among others that didn't work, which I can't remember)
In Person:
def fields =
{
for
{
person <- Persons.all.filter(_.id === this.id)
field <- person.fields join Fields.all on (_.fieldID === _.id)
} yield field
}
In PersonS
def fields = PersonFields.all.filter(_.personID === id) // No flatMap here!
then getFields(p: Person) looks like this:
def getFields(p: Person): Future[Seq[(PersonField, Field)]]
but
personX.onSuccess
{
case p: Person =>
{
val fields = helper.getFields(p)
fields.onSuccess
{
case f: Seq[(PersonField, Field)] => f map(f => println(f._1)}
}
}
}
gives me nothing, so I guess my join must be wrong. But what exactly am I doing wrong?
You can join all three, then yield the result
for {
((personField, person), field) <- PersonFields.all join Persons.all on (_.personId === _.id) join Fields.all on (_._1.fieldId === _.id)
if person.id === this.id
} yield (personField, person, field)
(I am not sure I got exactly what you were trying to get out of the query, so you can just edit the yield part )