Slick: Read nullable values as option when left join - sql

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)
}

Related

Linq2DB can't translate a mapped column in Where clause

I'm working with a legacy Oracle database that has a column on a table which stores boolean values as 'Y' or 'N' characters.
I have mapped/converted this column out like so:
MappingSchema.Default.SetConverter<char, bool>(ConvertToBoolean);
MappingSchema.Default.SetConverter<bool, char>(ConvertToChar);
ConvertToBoolean & ConvertToChar are simply functions that map between the types.
Here's the field:
private char hasDog;
[Column("HAS_DOG")]
public bool HasDog
{
get => ConvertToBoolean(hasDog);
set => hasDog = ConvertToChar(value);
}
This has worked well for simply retrieving data, however, it seems the translation of the following:
var humanQuery = (from human in database.Humans
join vetVisit in database.VetVisits on human.Identifier equals vetVisit.Identifier
select new HumanModel(
human.Identifier
human.Name,
human.HasDog,
vetVisit.Date,
vetVisit.Year,
vetVisit.PaymentDue
));
// humanQuery is filtered by year here
var query = from vetVisits in database.VetVisits
select new VetPaymentModel(
(humanQuery).First().Year,
(humanQuery).Where(q => q.HasDog).Sum(q => q.PaymentDue), -- These 2 lines aren't correctly translated to Y/N
(humanQuery).Where(q => !q.HasDog).Sum(q => q.PaymentDue)
);
As pointed out above, the .Where clause here doesn't translate the boolean comparison of HasDog being true/false to the relevant Y/N values, but instead a 0/1 and results in the error
ORA-01722: invalid number
Is there any way to handle this case? I'd like the generated SQL to check that HAS_DOG = 'Y' for instance with the specified Where clause :)
Notes
I'm not using EntityFramework here, the application module that this query exists in doesn't use EF/EFCore
You can define new mapping schema for your particular DataConnection:
var ms = new MappingSchema();
builder = ms.GetFluentMappingBuilder();
builder.Entity<Human>()
.Property(e => e.HasDog)
.HasConversion(v => v ? 'Y' : 'N', p => p == 'Y');
Create this schema ONCE and use when creating DataConnection

How to get result using better algorithm?

We have the list like below
List:
(("herry","0,1,2"),("herry","1,3"),("herry","3,6"),("herry","4"),("John","5"))
As the number in the string may be referred by different elements, the expected result is:
("herry","0,1,2,3,6"), ("herry","4"), ("John","5")
I worked out solution using scala, but it looks complicated, is there a more clean and easy way to work out the result? Thanks in advance!
Here is my solution in scala,
val foo=List(("herry","0,1,2"),("herry","1,3"),("herry","3,6"),("herry","4"),("John","5"))
println(GetValue)
def GetValue()={
foo.zipWithIndex.map((tuple: ((String, String), Int)) =>{
val tuples = getrelated(tuple._1, foo)
(tuple._2, tuples)
}).map((tuple: (Int, List[(String, String)])) => tuple._2)
.map((tuples: List[(String, String)]) => (tuples.head._1,tuples.map((tuple: (String, String)) => tuple._2)))
.map((tuple: (String, List[String])) => (tuple._1, tuple._2.mkString(",").split(",").distinct.sorted.mkString(",")))
.distinct
}
def getrelated(start:(String,String),fooList:List[(String,String)]):List[(String,String)]={
val fooListWithout = fooList.filter((tuple: (String, String)) => tuple != start)
val result=fooListWithout
.filter((tuple: (String, String)) => findmatching(tuple._2,start._2))
.flatMap((tuple: (String, String)) => start :: getrelated(tuple,fooListWithout))
if (result.isEmpty)
List(start)
else
result
}
def findmatching(key1:String,key2:String)={
(key1.split(",")++key2.split(","))
.groupBy(identity)
.mapValues((strings: Array[String]) => strings.size)
.exists((tuple: (String, Int)) => tuple._2>1)
}
Let me clarify the algorithm
if the number list has the overlapping number, then group these number as one element
if the number list has no overlapping number, then consider it as independent element
for example,
input: List(("herry","0,1,2"),("herry","1,3"),("herry","7,4"),("herry","4"),("John","5"))
expected output: List(("herry","0,1,2,3" ), ("herry","4,7"), ("John","5"))
input: List(("herry","0,1,2"),("herry","1,3"),("herry","3,6"),("herry","4"),("John","5"))
expected: List("herry","0,1,2,3,6"), ("herry","4"), ("John","5")
input: List(("herry","0,1"),("herry","2,3"),("herry","3,6"),("herry","4"),("John","5"))
expected: List("herry","0,1"),("herry","2,3,6"), ("herry","4"), ("John","5")
My assumption is that tuples for the same key that contain multiple values should be aggregated. It is unclear what should happen in the same value appears singly and as part of a list, e.g. ("herry", "4"), ("herry, "1,4") however it should be simple enough to remove any such cases
val data = List(("herry","0,1,2"),("herry","1,3"),("herry","3,6"),("herry","4"),("John","5"))// Start writing your ScalaFiddle code here
val (singles, multiples) = data.partition{case (name, list) => !list.contains(",")}
val multiplesAggregated = multiples
.groupBy{case (key, _) => key)
.map{
case (key, values) =>
key -> values.flatMap{case (_, numbers) => numbers.split(",")}.distinct.sorted.mkString(",")}.toList
println(multiplesAggregated ++ singles)
Output:
List((herry,0,1,2,3,6), (herry,4), (John,5))

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

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])]]

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

How can I do this Scala SQL (Slick 1.0.1) query in a functional rather than mutable manner?

I have a Scala Slick table of School objects and I want to filter based upon 0-5 parameters contained within a filter object SchoolFilter:
case class SchoolFilter(name: Option[String],
city: Option[String],
state: Option[String],
zip: Option[String],
district: Option[String])
A None value means "don't filter on this key at all" because I have a html page with a table of School objects and an AJAX call that does a filter based upon user input. I implemented this behaviour by creating a mutable list of all schools and then running a filter on each member of SchoolFilter that is defined. But the method doesn't seem horribly efficient, sucking in all of the records from the database and then making (potentially) 5 passes over the list before returning the results.
Is there a more functional (or perhaps more efficient) way of accomplishing this goal?
def findSchoolsByFilter(f: SchoolFilter = SchoolFilter(None, None, None, None, None),
n: Int = 5)
(implicit session: Session) = Try {
var s = collection.mutable.LinkedList(Query(Schools).list().toSeq: _*)
if (f.name.isDefined)
s = s.filter(_.name.toLowerCase.startsWith(f.name.get.toLowerCase))
if (f.city.isDefined)
s = s.filter(_.city.toLowerCase.startsWith(f.city.get.toLowerCase))
if (f.state.isDefined)
s = s.filter(_.state.toLowerCase.startsWith(f.state.get.toLowerCase))
if (f.zip.isDefined)
s = s.filter(_.zip.toLowerCase.startsWith(f.zip.get.toLowerCase))
if (f.district.isDefined)
s = s.filter(_.district.toLowerCase.startsWith(f.district.get.toLowerCase))
List(s.toSeq: _*).take(n).sorted
}
Local mutability (contained within a function, no leaked) is usually not a problem. Mutability becomes hard, when you have to reason about several places in your code modifying the same thing. But you can improve readability and efficiency of your code using a functional style. We can do the filter as a completely lazy query builder. No database round-trip, just building of an appropriate query, which means also no session needed anymore for your function.
def findSchoolsByFilter(f: SchoolFilter = SchoolFilter(None, None, None, None, None),
n: Int = 5) = {
/** Case insensitive option startsWith */
def iStartsWith( a:Column[String], bOption:Option[String] ) = bOption.map( b => a.toLowerCase startsWith b.toLowerCase )
Query(Schools).filter( s =>
Seq(
iStartsWith( s.name, f.name ),
iStartsWith( s.city, f.city ),
iStartsWith( s.state, f.state ),
iStartsWith( s.zip, f.zip ),
iStartsWith( s.district, f.district )
).flatten.reduce(_ && _) // flatten removes Nones and unwrap the Somes XMAS-Style
).take(n)
}
You could even go one step further and make schools an argument to findSchoolsByFilter, so you can lazily combine this filter with other filters.