How correctly built an object graph based on multi level join in Slick? - sql

I have a model structure as following:
Group -> Many Parties -> Many Participants
In on of the API calls I need to get single groups with parties and it's participants attached.
This whole structure is built on 4 tables:
group
party
party_participant
participant
Naturally, with SQL it's a pretty straight forward join that combines all of them. And this is exactly what I am trying to do with slick.
Mu method is dao class looks something like this:
def findOneByKeyAndAccountIdWithPartiesAndParticipants(key: UUID, accountId: Int): Future[Option[JourneyGroup]] = {
val joins = JourneyGroups.groups join
Parties.parties on (_.id === _.journeyGroupId) joinLeft
PartiesParticipants.relations on (_._2.id === _.partyId) joinLeft
Participants.participants on (_._2.map(_.participantId) === _.id)
val query = joins.filter(_._1._1._1.accountId === accountId).filter(_._1._1._1.key === key)
val q = for {
(((journeyGroup, party), partyParticipant), participant) <- query
} yield (journeyGroup, party, participant)
val result = db.run(q.result)
result ????
}
The problem here, is that the result is type of Future[Seq[(JourneyGroup, Party, Participant)]]
However, what I really need is Future[Option[JourneyGroup]]
Note: case classes of JourneyGroup and Party have sequences for there children defined:
case class Party(id: Option[Int] = None,
partyType: Parties.Type.Value,
journeyGroupId: Int,
accountId: Int,
participants: Seq[Participant] = Seq.empty[Participant])
and
case class JourneyGroup(id: Option[Int] = None,
key: UUID,
name: String,
data: Option[JsValue],
accountId: Int,
parties: Seq[Party] = Seq.empty[Party])
So they both can hold the descendants.
What is the correct way to convert to the result I need? Or am I completely in a wrong direction?
Also, is this statement is correct:
Participants.participants on (_._2.map(_.participantId) === _.id) ?

I ended up doing something like this:
journeyGroupDao.findOneByKeyAndAccountIdWithPartiesAndParticipants(key, account.id.get) map { data =>
val groupedByJourneyGroup = data.groupBy(_._1)
groupedByJourneyGroup.map { case (group, rows) =>
val parties = rows.map(_._2).distinct map { party =>
val participants = rows.filter(r => r._2.id == party.id).flatMap(_._3)
party.copy(participants = participants)
}
group.copy(parties = parties)
}.headOption
}
where DAO method's signature is:
def findOneByKeyAndAccountIdWithPartiesAndParticipants(key: UUID, accountId: Int): Future[Seq[(JourneyGroup, Party, Option[Participant])]]

Related

Select records where id does not equal to x

I have these tables
object BooksAuthors : Table(name = "books_authors") {
val book = reference("book_id", Books, onDelete = ReferenceOption.CASCADE)
val author = reference("author_id", Authors, onDelete = ReferenceOption.CASCADE)
}
object Books : IntIdTable() {
val title = varchar("title", 250)
val isbn = varchar("isbn", 13)
}
object Authors : IntIdTable() {
val email = varchar("email", 100).uniqueIndex()
}
i would like to write a query that returns all books that dont have a specific author so i wrote this
suspend fun getBooksWithoutAuthorId(authorId: Int): List<BookDTO> = DbFactory.dbQuery {
val query = BooksAuthors.innerJoin(Books).select { BooksAuthors.author neq authorId }
Book.wrapRows(query).map { it.toDTO() }
}
But the query returns books that have the author. What am i doing wrong?
As Sebastian Redl mentioned, there could be books with multiple authors and your query doesn't cover that case.
Correct Exposed query should be:
val query = Books.select {
Books.id notInSubQuery
BooksAuthors.slice(BooksAuthors.book).select { BooksAuthors.author eq authorId }
}
It looks like you have an n:m mapping where a book can have multiple authors.
Your query, as written, finds any book that an author other than your selected one has authored.
This means if authors Alice and Bob wrote a book together, and you want to find books "not by Bob", you would still find the book because Alice took part.
Your desired query cannot be expressed as a simple join; you need nested queries instead.
Something like equivalent to this SQL:
SELECT * from books b WHERE ? NOT IN (
SELECT ab.author_id FROM authors_books ab WHERE ab.book_id = b.id);
Though I'm afraid I don't know how to express this in Exposed.
https://www.db-fiddle.com/f/7BsVUW95g6L4rXDBCoaXK3/0

Slick query for one to optional one (zero or one) relationship

Given tables of:
case class Person(id: Int, name: String)
case class Dead(personId: Int)
and populated with:
Person(1, "George")
Person(2, "Barack")
Dead(1)
is it possible to have a single query that would produce a list of (Person, Option[Dead]) like so?
(Person(1, "George"), Some(Dead(1)))
(Person(2, "Barack"), None)
For slick 3.0 it should be something like this:
val query = for {
(p, d) <- persons joinLeft deads on (_.id === _.personId)
} yield (p, d)
val results: Future[Seq[(Person, Option[Dead])]] = db.run(query.result)
In slick, outer joins are automatically wrapped in an Option type. You can read more about joining here: http://slick.typesafe.com/doc/3.0.0/queries.html#joining-and-zipping

Slick: Read nullable values as option when left join

Problem when using Slick to join: I have 2 tables User and UserInfo and I want to leftJoin them to get user's info. I've tried this:
val q = for{
(user,info) <- User leftJoin UserInfo on (_.id === _.userid)
} yield(user, info)
But the UserInfo table has some nullable field, so when I try to execute the query:
q.map(user_info => (user_info._1,user_info._2)).list
It makes error because user_info._2 has some null values. I know a solution that yield each field in UserInfo and add getOrElse(None) for nullable fields. However, UserInfo has many field so I don't want to use this.
Can anyone help me?
What you CAN do, is this define a function that does the conversion, and then use it in your map:
def nullToOption[A](input: A): Option[A] = input match {
case null => None
case x => Some(x)
}
And then you just use it in your map.
I made a simple example using a simple list:
val lst = List("Hello", null, "hi", null)
val newlst = map lst nullToOption
newList is now the following: List(Some("Hello"), None, Some("hi"), None)
Of course you can modify nullToOption to fit your needs; here's a version that takes tuples:
def nullToOption[A, B](input: (A,B)): (Option[A], Option[B]) = input match {
case (x, y) => (Some(x), Some(y))
case (x, null) => (Some(x), None)
case (null, y) => (None, Some(y))
case (null, null) => (None, None)
}

Grails query to filter on association and only return matching entities

I have the following 1 - M (one way) relationship:
Customer (1) -> (M) Address
I am trying to filter the addresses for a specific customer that contain certain text e.g.
def results = Customer.withCriteria {
eq "id", 995L
addresses {
ilike 'description', '%text%'
}
}
The problem is that this returns the Customer and when I in turn access the "addresses" it gives me the full list of addresses rather than the filtered list of addresses.
It's not possible for me to use Address.withCriteria as I can't access the association table from the criteria query.
I'm hoping to avoid reverting to a raw SQL query as this would mean not being able to use a lot functionality that's in place to build up criteria queries in a flexible and reusable manner.
Would love to hear any thoughts ...
I believe the reason for the different behavior in 2.1 is documented here
Specifically this point:
The previous default of LEFT JOIN for criteria queries across associations is now INNER JOIN.
IIRC, Hibernate doesn't eagerly load associations when you use an inner join.
Looks like you can use createAlias to specify an outer join example here:
My experience with this particular issue is from experience with NHibernate, so I can't really shed more light on getting it working correctly than that. I'll happily delete this answer if it turns out to be incorrect.
Try this:
def results = Customer.createCriteria().listDistinct() {
eq('id', 995L)
addresses {
ilike('description', '%Z%')
}
}
This gives you the Customer object that has the correct id and any matching addresses, and only those addresses than match.
You could also use this query (slightly modified) to get all customers that have a matching address:
def results = Customer.createCriteria().listDistinct() {
addresses {
ilike('description', '%Z%')
}
}
results.each {c->
println "Customer " + c.name
c.addresses.each {address->
println "Address " + address.description
}
}
EDIT
Here are the domain classes and the way I added the addresses:
class Customer {
String name
static hasMany = [addresses: PostalAddress]
static constraints = {
}
}
class PostalAddress {
String description
static belongsTo = [customer: Customer]
static constraints = {
}
}
//added via Bootstrap for testing
def init = { servletContext ->
def custA = new Customer(name: 'A').save(failOnError: true)
def custB = new Customer(name: 'B').save(failOnError: true)
def custC = new Customer(name: 'C').save(failOnError: true)
def add1 = new PostalAddress(description: 'Z1', customer: custA).save(failOnError: true)
def add2 = new PostalAddress(description: 'Z2', customer: custA).save(failOnError: true)
def add3 = new PostalAddress(description: 'Z3', customer: custA).save(failOnError: true)
def add4 = new PostalAddress(description: 'W4', customer: custA).save(failOnError: true)
def add5 = new PostalAddress(description: 'W5', customer: custA).save(failOnError: true)
def add6 = new PostalAddress(description: 'W6', customer: custA).save(failOnError: true)
}
When I run this I get the following output:
Customer A
Address Z3
Address Z1
Address Z2

scala: how to model a basic parent-child relation

I have a Brand class that has several products
And in the product class I want to have a reference to the brand, like this:
case class Brand(val name:String, val products: List[Product])
case class Product(val name: String, val brand: Brand)
How can I poulate these classes???
I mean, I can't create a product unless I have a brand
And I can't create the brand unless I have a list of Products (because Brand.products is a val)
What would be the best way to model this kind of relation?
I would question why you are repeating the information, by saying which products relate to which brand in both the List and in each Product.
Still, you can do it:
class Brand(val name: String, ps: => List[Product]) {
lazy val products = ps
override def toString = "Brand("+name+", "+products+")"
}
class Product(val name: String, b: => Brand) {
lazy val brand = b
override def toString = "Product("+name+", "+brand.name+")"
}
lazy val p1: Product = new Product("fish", birdseye)
lazy val p2: Product = new Product("peas", birdseye)
lazy val birdseye = new Brand("BirdsEye", List(p1, p2))
println(birdseye)
//Brand(BirdsEye, List(Product(fish, BirdsEye), Product(peas, BirdsEye)))
By-name params don't seem to be allowed for case classes unfortunately.
See also this similar question: Instantiating immutable paired objects
Since your question is about model to this relationship, I will say why not just model them like what we do in database? Separate the entity and the relationship.
val productsOfBrand: Map[Brand, List[Product]] = {
// Initial your brand to products mapping here, using var
// or mutable map to construct the relation is fine, since
// it is limit to this scope, and transparent to the outside
// world
}
case class Brand(val name:String){
def products = productsOfBrand.get(this).getOrElse(Nil)
}
case class Product(val name: String, val brand: Brand) // If you really need that brand reference