FunDA (1) - Query Result Row: Strongly typed Query result row

Keywords: Scala Database SQL

One of the characteristics of FunDA is to provide row-by-row data operation support in the form of data streams. This function solves the problem that FRM such as Slick data operation is mainly based on SQL batch mode. In order to achieve safe and efficient data row operation, we must transform the Query result set generated by FRM into a strongly typed result set, that is, a data row type result set that can be operated on by field names. In a previous discussion, we introduced Shape to change the type of Slick Query result row. However, such a transformation requires programmers to have a better understanding of Slick. More importantly, this approach relies too much on Slick's internal functionality. We hope that FunDA can support multiple FRMs, so we should try to avoid close coupling with any FRM. It seems that it is a realistic choice to convert the data row type format from the result of FRM. Generally speaking, we can assume that any FRM user can understand the Query result set type of FRM, because their main purpose is to use the result set. So it's not too demanding for FunDA users to provide a Query result data row with another type conversion function. FunDA's design idea is to provide a target type and FRM Query result data line to the type conversion function of the forceful type, and then the forceful type result set is provided by FunDA. Let's start with a typical Slick Query example:

 1 import slick.driver.H2Driver.api._
 2 import scala.concurrent.duration._
 3 import scala.concurrent.Await
 4 
 5 object TypedRow extends App {
 6 
 7   class AlbumsTable(tag: Tag) extends Table[
 8     (Long,String,String,Option[Int],Int)](tag,"ALBUMS") {
 9     def id = column[Long]("ID",O.PrimaryKey)
10     def title = column[String]("TITLE")
11     def artist = column[String]("ARTIST")
12     def year = column[Option[Int]]("YEAR")
13     def company = column[Int]("COMPANY")
14     def * = (id,title,artist,year,company)
15   }
16   val albums = TableQuery[AlbumsTable]
17   class CompanyTable(tag: Tag) extends Table[(Int,String)](tag,"COMPANY") {
18     def id = column[Int]("ID",O.PrimaryKey)
19     def name = column[String]("NAME")
20     def * = (id, name)
21   }
22   val companies = TableQuery[CompanyTable]
23 
24   val albumInfo = for {
25     a <- albums
26     c <- companies
27     if (a.company === c.id)
28   } yield(a.title,a.artist,a.year,c.name)
29 
30   val db = Database.forConfig("h2db")
31 
32   Await.result(db.run(albumInfo.result),Duration.Inf).foreach {r =>
33     println(s"${r._1} by ${r._2}, ${r._3.getOrElse(2000)} ${r._4}")
34   }
35   
36 }

The albumInfo returned result row type in the example above is a Tuple type: (String,String,Option[Int],Int), and there is no field name, so you can only select fields by location annotation such as r._1,r._2.... Using returned results in this form can easily lead to confusion and errors in choosing fields.

As mentioned earlier, if the user can provide a return row type and a conversion function as follows:

1   case class AlbumRow(title: String,artist: String,year: Int,studio: String)
2   def toTypedRow(raw: (String,String,Option[Int],String)):AlbumRow =
3     AlbumRow(raw._1,raw._2,raw._3.getOrElse(2000),raw._4)

We can use this function to convert row types after reading data:

1   Await.result(db.run(albumInfo.result),Duration.Inf).map{raw =>
2     toTypedRow(raw)}.foreach {r =>
3     println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
4   }

The return row type AlbumRow is a strong type. Now I can use the field name to select the value of the data field. However, there are still some things wrong: after the user has provided the target row type and conversion function, a direct call to a function can get the desired result set. Yes, we're just going to design a back-end tool library to provide this function.

Next we will design FunDA's data row type class FDADataRow. This type is now basically designed for Slick, and loosely coupled issues will be considered later on when the functionality is successfully implemented. This type requires a target row type definition and a type conversion function, plus some information such as Slick profile, database, etc. Then a target row type result set function getTypedRows is provided:

package com.bayakala.funda.rowtypes

import scala.concurrent.duration._
import scala.concurrent.Await
import slick.driver.JdbcProfile

object DataRowType {
  class FDADataRow[SOURCE, TARGET](slickProfile: JdbcProfile,convert: SOURCE  => TARGET){
    import slickProfile.api._

    def getTypedRows(slickAction: DBIO[Iterable[SOURCE]])(slickDB: Database): Iterable[TARGET] =
      Await.result(slickDB.run(slickAction), Duration.Inf).map(raw => convert(raw))
  }

  object FDADataRow {
    def apply[SOURCE, TARGET](slickProfile: JdbcProfile, converter: SOURCE => TARGET): FDADataRow[SOURCE, TARGET] =
      new FDADataRow[SOURCE, TARGET](slickProfile, converter)
  }

}

Following is a demonstration of the use of this library:

1   import com.bayakala.funda.rowtypes.DataRowType
2 
3   val loader = FDADataRow(slick.driver.H2Driver, toTypedRow _)
4 
5   loader.getTypedRows(albumInfo.result)(db).foreach {r =>
6     println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
7   }

So, as a data row, how to update the data field? We should treat it as an immutable object and update it in a functional way:

1   def updateYear(from: AlbumRow): AlbumRow =
2     AlbumRow(from.title,from.artist,from.year+1,from.studio)
3 
4   loader.getTypedRows(albumInfo.result)(db).map(updateYear).foreach {r =>
5     println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
6   }

updateYear is a typical functional method: pass in AlbumRow and return the new AlbumRow.

Here is the source code for this discussion:

FunDA library:

 1 package com.bayakala.funda.rowtypes
 2 
 3 import scala.concurrent.duration._
 4 import scala.concurrent.Await
 5 import slick.driver.JdbcProfile
 6 
 7 object DataRowType {
 8   class FDADataRow[SOURCE, TARGET](slickProfile: JdbcProfile,convert: SOURCE  => TARGET){
 9     import slickProfile.api._
10 
11     def getTypedRows(slickAction: DBIO[Iterable[SOURCE]])(slickDB: Database): Iterable[TARGET] =
12       Await.result(slickDB.run(slickAction), Duration.Inf).map(raw => convert(raw))
13   }
14 
15   object FDADataRow {
16     def apply[SOURCE, TARGET](slickProfile: JdbcProfile, converter: SOURCE => TARGET): FDADataRow[SOURCE, TARGET] =
17       new FDADataRow[SOURCE, TARGET](slickProfile, converter)
18   }
19 
20 }

Functional test source code:

 1 import slick.driver.H2Driver.api._
 2 
 3 import scala.concurrent.duration._
 4 import scala.concurrent.Await
 5 
 6 object TypedRow extends App {
 7 
 8   class AlbumsTable(tag: Tag) extends Table[
 9     (Long,String,String,Option[Int],Int)](tag,"ALBUMS") {
10     def id = column[Long]("ID",O.PrimaryKey)
11     def title = column[String]("TITLE")
12     def artist = column[String]("ARTIST")
13     def year = column[Option[Int]]("YEAR")
14     def company = column[Int]("COMPANY")
15     def * = (id,title,artist,year,company)
16   }
17   val albums = TableQuery[AlbumsTable]
18   class CompanyTable(tag: Tag) extends Table[(Int,String)](tag,"COMPANY") {
19     def id = column[Int]("ID",O.PrimaryKey)
20     def name = column[String]("NAME")
21     def * = (id, name)
22   }
23   val companies = TableQuery[CompanyTable]
24 
25   val albumInfo =
26     for {
27       a <- albums
28       c <- companies
29       if (a.company === c.id)
30     } yield(a.title,a.artist,a.year,c.name)
31 
32   val db = Database.forConfig("h2db")
33 
34   Await.result(db.run(albumInfo.result),Duration.Inf).foreach {r =>
35     println(s"${r._1} by ${r._2}, ${r._3.getOrElse(2000)} ${r._4}")
36   }
37 
38   case class AlbumRow(title: String,artist: String,year: Int,studio: String)
39   def toTypedRow(raw: (String,String,Option[Int],String)):AlbumRow =
40     AlbumRow(raw._1,raw._2,raw._3.getOrElse(2000),raw._4)
41 
42   Await.result(db.run(albumInfo.result),Duration.Inf).map{raw =>
43     toTypedRow(raw)}.foreach {r =>
44     println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
45   }
46 
47   import com.bayakala.funda.rowtypes.DataRowType.FDADataRow
48 
49   val loader = FDADataRow(slick.driver.H2Driver, toTypedRow _)
50 
51   loader.getTypedRows(albumInfo.result)(db).foreach {r =>
52     println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
53   }
54 
55   def updateYear(from: AlbumRow): AlbumRow =
56     AlbumRow(from.title,from.artist,from.year+1,from.studio)
57 
58   loader.getTypedRows(albumInfo.result)(db).map(updateYear).foreach {r =>
59     println(s"${r.title} by ${r.artist}, ${r.year} ${r.studio}")
60   }
61 
62 }

Posted by solus on Thu, 27 Dec 2018 19:27:07 -0800