diff --git a/build.sbt b/build.sbt index 569e59f..3a24297 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,7 @@ tutSourceDirectory := sourceDirectory.value / "pages" tutTargetDirectory := target.value / "pages" -scalaVersion := "2.11.7" +scalaVersion := "2.12.1" scalacOptions ++= Seq( "-deprecation", @@ -18,8 +18,8 @@ scalacOptions ++= Seq( ) libraryDependencies ++= Seq( - "com.typesafe.slick" %% "slick" % "3.1.1", - "com.typesafe.slick" %% "slick-hikaricp" % "3.1.1", + "com.typesafe.slick" %% "slick" % "3.2.0", + "com.typesafe.slick" %% "slick-hikaricp" % "3.2.0", "com.h2database" % "h2" % "1.4.185", "ch.qos.logback" % "logback-classic" % "1.1.2", "joda-time" % "joda-time" % "2.6", diff --git a/project/plugins.sbt b/project/plugins.sbt index 20e5762..b468567 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.7") +addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.8") diff --git a/sbt.sh b/sbt.sh new file mode 100755 index 0000000..4ba1635 --- /dev/null +++ b/sbt.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +export JAVA_OPTS="-Xmx3g -XX:+TieredCompilation -XX:ReservedCodeCacheSize=256m -XX:+UseNUMA -XX:+UseParallelGC -XX:+CMSClassUnloadingEnabled" + +sbt "$@" diff --git a/src/pages/0-preface.md b/src/pages/0-preface.md index 71984c1..772de51 100644 --- a/src/pages/0-preface.md +++ b/src/pages/0-preface.md @@ -28,7 +28,7 @@ This material is aimed at beginner-to-intermediate Scala developers. You need: * an installed JDK 8 or later, along with a programmer's text editor or IDE. -The material presented focuses on Slick version 3.1. Examples use [H2][link-h2-home] as the relational database. +The material presented focuses on Slick version 3.2. Examples use [H2][link-h2-home] as the relational database. ## How to Contact Us {-} diff --git a/src/pages/1-basics.md b/src/pages/1-basics.md index 0daf2e8..3ad3dbb 100644 --- a/src/pages/1-basics.md +++ b/src/pages/1-basics.md @@ -105,21 +105,19 @@ Which will give output similar to: ~~~ scala > console -[info] Compiling 1 Scala source to /Users/jonoabroad/developer/books/essential-slick-code/chapter-01/target/scala-2.11/classes... [info] Starting scala interpreter... [info] -import slick.driver.H2Driver.api._ +Welcome to Scala 2.12.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_112). +Type in expressions for evaluation. Or try :help. + +scala> import slick.jdbc.H2Profile.api._ import Example._ import scala.concurrent.duration._ import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global -db: slick.driver.H2Driver.backend.Database = slick.jdbc.JdbcBackend$DatabaseDef@75028b56 -exec: [T](program: slick.driver.H2Driver.api.DBIO[T])T +db: slick.jdbc.H2Profile.backend.Database = slick.jdbc.JdbcBackend$DatabaseDef@ac9a820 +exec: [T](program: slick.jdbc.H2Profile.api.DBIO[T])T res0: Option[Int] = Some(4) -Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_25). -Type in expressions to have them evaluated. -Type :help for more information. - scala> ~~~ @@ -162,10 +160,10 @@ name := "essential-slick-chapter-01" version := "1.0.0" -scalaVersion := "2.11.7" +scalaVersion := "2.12.1" libraryDependencies ++= Seq( - "com.typesafe.slick" %% "slick" % "3.1.1", + "com.typesafe.slick" %% "slick" % "3.2.0", "com.h2database" % "h2" % "1.4.185", "ch.qos.logback" % "logback-classic" % "1.1.2" ) @@ -183,13 +181,13 @@ If we were using a separate database like MySQL or PostgreSQL, we would substitu ### Importing Library Code -Database management systems are not created equal. Different systems support different data types, different dialects of SQL, and different querying capabilities. To model these capabilities in a way that can be checked at compile time, Slick provides most of its API via a database-specific *driver*. For example, we access most of the Slick API for H2 via the following `import`: +Database management systems are not created equal. Different systems support different data types, different dialects of SQL, and different querying capabilities. To model these capabilities in a way that can be checked at compile time, Slick provides most of its API via a database-specific *profile*. For example, we access most of the Slick API for H2 via the following `import`: ```tut:silent -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ ``` -Slick makes heavy use of implicit conversions and extension methods, so we generally need to include this import anywhere where we're working with queries or the database. [Chapter 5](#Modelling) looks how you can keep a specific database driver out of your code until necessary. +Slick makes heavy use of implicit conversions and extension methods, so we generally need to include this import anywhere where we're working with queries or the database. [Chapter 5](#Modelling) looks how you can keep a specific database profile out of your code until necessary. ### Defining our Schema @@ -600,17 +598,21 @@ messages += Message("Dave","What if I say 'Pretty please'?") If you don't wait for the future to complete, you'll see just the future itself: ```tut:book -db.run(messages += Message("Dave","What if I say 'Pretty please'?")) +val f = db.run(messages += Message("Dave","What if I say 'Pretty please'?")) ``` ```tut:invisible -{ + // Post-exercise clean up // We inserted a new message for Dave twice in the last solution. // We need to fix this so the next exercise doesn't contain confusing duplicates + + // NB: this block is not inside {}s because doing that triggered: + // Could not initialize class $line41.$read$$iw$$iw$$iw$$iw$$iw$$iw$ + import scala.concurrent.ExecutionContext.Implicits.global - val ex1cleanup = for { + val ex1cleanup: DBIO[Int] = for { _ <- messages.filter(_.content === "What if I say 'Pretty please'?").delete m = Message("Dave","What if I say 'Pretty please'?", 5L) _ <- messages.forceInsert(m) @@ -618,7 +620,6 @@ db.run(messages += Message("Dave","What if I say 'Pretty please'?")) } yield count val rowCount = exec(ex1cleanup) assert(rowCount == 1, s"Wrong number of rows after cleaning up ex1: $rowCount") -} ``` diff --git a/src/pages/2-selecting.md b/src/pages/2-selecting.md index 4a9cf46..d7ccb6a 100644 --- a/src/pages/2-selecting.md +++ b/src/pages/2-selecting.md @@ -13,7 +13,7 @@ In [Chapter 6](#joins) we'll look at more complex queries involving joins, aggre The simplest select query is the `TableQuery` generated from a `Table`. In the following example, `messages` is a `TableQuery` for `MessageTable`: ```tut:silent -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ ``` ```tut:book final case class Message( @@ -48,10 +48,10 @@ Our `TableQuery` is the equivalent of the SQL `select * from message`. **Query Extension Methods** Like many of the methods discussed below, the `result` method is actually an extension method applied to `Query` via an implicit conversion. -You'll need to have everything from `H2Driver.api` in scope for this to work: +You'll need to have everything from `H2Profile.api` in scope for this to work: ```tut:silent -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ ``` @@ -681,6 +681,7 @@ We'll see the Slick representation of `GROUP` and `JOIN` in [Chapter 6](#joins). We introduced some new terminology: * _unpacked_ type, which is the regular Scala types we work with, such as `String`; and + * _mixed_ type, which is Slick's column representation, such as `Rep[String]`. We run queries by converting them to actions using the `result` method. diff --git a/src/pages/3-modifying.md b/src/pages/3-modifying.md index 9caf2e7..3aff5b2 100644 --- a/src/pages/3-modifying.md +++ b/src/pages/3-modifying.md @@ -1,5 +1,5 @@ ```tut:invisible -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ final case class Message( sender: String, @@ -178,11 +178,11 @@ exec(messages returning messages += This is a shame, but getting the primary key is often all we need.
-**Driver Capabilities** +**Profile Capabilities** -The Slick manual contains a comprehensive table of the [capabilities for each database driver][link-ref-dbs]. The ability to return complete records from an insert query is referenced as the `jdbc.returnInsertOther` capability. +The Slick manual contains a comprehensive table of the [capabilities for each database profile][link-ref-dbs]. The ability to return complete records from an insert query is referenced as the `jdbc.returnInsertOther` capability. -The API documentation for each driver also lists the capabilities that the driver *doesn't* have. For an example, the top of the [H2 Driver Scaladoc][link-ref-h2driver] page points out several of its shortcomings. +The API documentation for each profile also lists the capabilities that the profile *doesn't* have. For an example, the top of the [H2 Profile Scaladoc][link-ref-h2driver] page points out several of its shortcomings.
If we want to get a complete populated `Message` back from a database without `jdbc.returnInsertOther` support, we retrieve the primary key and manually add it to the inserted record. Slick simplifies this with another method, `into`: @@ -520,7 +520,7 @@ For modifying the rows in the database we have seen that: Auto-incrementing values are inserted by Slick, unless forced. The auto-incremented values can be returned from the insert by using `returning`. -Databases have different capabilities. The limitations of each driver is listed in the driver's Scala Doc page. +Databases have different capabilities. The limitations of each profile is listed in the profile's Scala Doc page. ## Exercises diff --git a/src/pages/4-combining-actions.md b/src/pages/4-combining-actions.md index ca1dc7b..2610a2b 100644 --- a/src/pages/4-combining-actions.md +++ b/src/pages/4-combining-actions.md @@ -1,5 +1,5 @@ ```tut:invisible -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ final case class Message( sender: String, diff --git a/src/pages/5-data_modelling.md b/src/pages/5-data_modelling.md index 9ad644d..366a775 100644 --- a/src/pages/5-data_modelling.md +++ b/src/pages/5-data_modelling.md @@ -21,9 +21,9 @@ So far, all of our examples have been written in a single Scala file. This approach doesn't scale to larger application codebases. In this section we'll explain how to split up application code into modules. -Until now we've also been exclusively using Slick's H2 driver. +Until now we've also been exclusively using Slick's H2 profile. When writing real applications we often need to be able to -switch drivers in different circumstances. +switch profiles in different circumstances. For example, we may use PostgreSQL in production and H2 in our unit tests. An example of this pattern can be found in the [example project][link-example], @@ -31,30 +31,21 @@ folder _chapter-05_, file _structure.scala_. ### Abstracting over Databases -Let's look at how we can write code that works with multiple different database drivers. +Let's look at how we can write code that works with multiple different database profiles. When we previously wrote... ```tut:book -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ ``` ...we were locking ourselves into H2. -We want to write an import that works with a variety of drivers. -Fortunately, Slick provides a common supertype for the drivers -for the most popular databases---a trait called `JdbcProfile`: +We want to write an import that works with a variety of profiles. +Fortunately, Slick provides a common supertype for profiles---a trait called `JdbcProfile`: ```tut:book -import slick.driver.JdbcProfile +import slick.jdbc.JdbcProfile ``` -
-*Drivers and Profiles* - -Slick uses the words "driver" and "profile" interchangeably. -We'll start referring to Slick drivers as "profiles" here -to distinguish them from the JDBC drivers that sit lower in the code. -
- We can't import directly from `JdbcProfile` because it isn't a concrete object. Instead, we have to *inject a dependency* of type `JdbcProfile` into our application and import from that. The basic pattern we'll use is as follows: @@ -81,7 +72,7 @@ trait DatabaseModule { object Main extends App { // Instantiate the database module, assigning a concrete profile: val databaseLayer = new DatabaseModule { - val profile = slick.driver.H2Driver + val profile = slick.jdbc.H2Profile } } ``` @@ -123,7 +114,7 @@ class DatabaseLayer(val profile: JdbcProfile) extends // Instantiate the modules and inject a profile: object Main extends App { - val databaseLayer = new DatabaseLayer(slick.driver.H2Driver) + val databaseLayer = new DatabaseLayer(slick.jdbc.H2Profile) } ``` @@ -136,7 +127,7 @@ To work with a different database, we inject a different profile when we instantiate the database code: ```tut:book -val anotherDatabaseLayer = new DatabaseLayer(slick.driver.PostgresDriver) +val anotherDatabaseLayer = new DatabaseLayer(slick.jdbc.PostgresProfile) ``` This basic pattern is reasonable way of structuring your application. @@ -159,7 +150,7 @@ object messages extends TableQuery(new MessageTable(_)) { def messagesFrom(name: String) = this.filter(_.sender === name) - val numSenders = this.map(_.sender).countDistinct + val numSenders = this.map(_.sender).distinct.length } ~~~ @@ -533,7 +524,7 @@ so we've introduced a type alias for `AttrHList` to avoid as much typing as we can. ```tut:invisible -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ import scala.concurrent.{Await,Future} import scala.concurrent.duration._ @@ -865,18 +856,6 @@ meaning the schema will include: ALTER TABLE "user" ADD CONSTRAINT "pk_id" PRIMARY KEY("id") ~~~ -
-**H2 Issue** - -As it happens, this specific example [doesn't currently work with H2 and Slick](https://github.com/slick/slick/issues/763). - -The `O.AutoInc` marks the column as an H2 "IDENTIY" -column which is, implicitly, a primary key as far as H2 is concerned. - -It's fixed in Slick 3.2. -
- - The `primaryKey` method is more useful for defining *compound* primary keys that involve two or more columns. diff --git a/src/pages/6-joins.md b/src/pages/6-joins.md index 96ba055..1f550e2 100644 --- a/src/pages/6-joins.md +++ b/src/pages/6-joins.md @@ -29,7 +29,7 @@ To demonstrate joins we will need at least two tables. We'll start this chapter with `User`... ```tut:book -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ import scala.concurrent.ExecutionContext.Implicits.global case class User(name: String, id: Long = 0L) @@ -632,19 +632,19 @@ of your chosen database profile: ```tut:book // H2 supports zip -slick.driver.H2Driver.capabilities. +slick.jdbc.H2Profile.capabilities. map(_.toString). contains("relational.zip") // SQLite does not support zip -slick.driver.SQLiteDriver.capabilities. +slick.jdbc.SQLiteProfile.capabilities. map(_.toString). contains("relational.zip") ``` ```tut:invisible -assert(false == slick.driver.SQLiteDriver.capabilities.map(_.toString).contains("relational.zip"), "SQLLite now supports ZIP!") -assert(true == slick.driver.H2Driver.capabilities.map(_.toString).contains("relational.zip"), "H2 no longer supports ZIP?!") +assert(false == slick.jdbc.SQLiteProfile.capabilities.map(_.toString).contains("relational.zip"), "SQLLite now supports ZIP!") +assert(true == slick.jdbc.H2Profile.capabilities.map(_.toString).contains("relational.zip"), "H2 no longer supports ZIP?!") ``` @@ -712,8 +712,6 @@ Method SQL --------------- --------------------------------------------------- `length` `COUNT(1)` - `countDistinct` `COUNT(DISTINCT column)` - `min` `MIN(column)` `max` `MAX(column)` @@ -732,13 +730,13 @@ Using them causes no great surprises, as shown in the following examples: val numRows: DBIO[Int] = messages.length.result val numDifferentSenders: DBIO[Int] = - messages.map(_.senderId).countDistinct.result + messages.map(_.senderId).distinct.length.result val firstSent: DBIO[Option[Long]] = messages.map(_.id).min.result ``` -While `length` and `countDistinct` return an `Int`, the other functions return an `Option`. +While `length` returns an `Int`, the other functions return an `Option`. This is because there may be no rows returned by the query, meaning the is no minimum, maximum and so on. diff --git a/src/pages/7-plain_sql.md b/src/pages/7-plain_sql.md index bce148d..dc2ce3f 100644 --- a/src/pages/7-plain_sql.md +++ b/src/pages/7-plain_sql.md @@ -1,5 +1,5 @@ ```tut:invisible -import slick.driver.H2Driver.api._ +import slick.jdbc.H2Profile.api._ import scala.concurrent.{Await,Future} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -358,7 +358,7 @@ The `@StaticDatabaseConfig` syntax is called an _annotation_. This particular `S ```scala tsql { - driver = "slick.driver.H2Driver$" + profile = "slick.jdbc.H2Profile$" db { connectionPool = disabled url = "jdbc:h2:mem:chapter06; INIT= @@ -369,9 +369,9 @@ tsql { } ``` -Note the `$` in the driver class name is not a typo. The class name is being passed to Java's `Class.forName`, but of course Java doesn't have a singleton as such. The Slick configuration does the right thing to load `$MODULE` when it sees `$`. This interoperability with Java is described in [Chapter 29 of _Programming in Scala_][link-pins-interop]. +Note the `$` in the profile class name is not a typo. The class name is being passed to Java's `Class.forName`, but of course Java doesn't have a singleton as such. The Slick configuration does the right thing to load `$MODULE` when it sees `$`. This interoperability with Java is described in [Chapter 29 of _Programming in Scala_][link-pins-interop]. -You won't have seen this when we introduced the database configuration in Chapter 1. That's because this `tsql` configuration has a different format, and combines the Slick driver (`slicker.driver.H2Driver$`) and the JDBC driver (`org.h2.Drvier`) in one entry. +You won't have seen this when we introduced the database configuration in Chapter 1. That's because this `tsql` configuration has a different format, and combines the Slick profile (`slick.jdbcr.H2Profile`) and the JDBC driver (`org.h2.Drvier`) in one entry. A consequence of supplying a `@StaticDatabaseConfig` is that you can define one databases configuration for your application and a different one for the compiler to use. That is, perhaps you are running an application, or test suite, against an in-memory database, but validating the queries at compile time against a full-populated production-like integration database. diff --git a/src/pages/8-databases.md b/src/pages/8-databases.md index 10eb94f..e27c7d5 100644 --- a/src/pages/8-databases.md +++ b/src/pages/8-databases.md @@ -5,8 +5,8 @@ However Slick also supports PostgreSQL, MySQL, Derby, SQLite, Oracle, and Micros There was a time when you needed a commercial license from Lightbend to use Slick in production with Oracle, SQL Server, or DB2. This restriction was removed in early 2016[^slick-blog-open]. -However, there was an effort to build free and open drivers, resulting in the FreeSlick project. -These drivers continue to be available, and you can find out more about this from the [FreeSlick GitHub page](https://github.com/smootoo/freeslick). +However, there was an effort to build free and open profiles, resulting in the FreeSlick project. +These profiles continue to be available, and you can find out more about this from the [FreeSlick GitHub page](https://github.com/smootoo/freeslick). [^slick-blog-open]: [http://slick.lightbend.com/news/2016/02/01/slick-extensions-licensing-change.html](http://slick.lightbend.com/news/2016/02/01/slick-extensions-licensing-change.html). @@ -21,7 +21,7 @@ In summary you will need to ensure that: * a database is available with the correct name; * the `build.sbt` file has the correct dependency; * the correct JDBC driver is referenced in the code; and - * the correct Slick driver is used. + * the correct Slick profile is used. Each chapter uses its own database---so these steps will need to be applied for each chapter. @@ -79,18 +79,18 @@ chapter01 = { } ~~~ -### Update Slick Driver +### Update Slick Profile Change the import from ```scala -slick.driver.H2Driver.api._ +slick.jdbc.H2Profile.api._ ``` to ```scala -slick.driver.PostgresDriver.api._ +slick.jdbc.PostgresProfile.api._ ``` @@ -154,16 +154,16 @@ Note that we've formatted the `connectionPool` line to make it legible. In reality all those `&` parameters will be on the same line. -### Update Slick Driver +### Update Slick DriverProfile Change the import from ```scala -slick.driver.H2Driver.api._ +slick.jdbc.H2Profile.api._ ``` to ```scala -slick.driver.MySQLDriver.api._ +slick.jdbc.MySQLProfile.api._ ``` diff --git a/src/pages/LEFT OVER BITS.md b/src/pages/LEFT OVER BITS.md deleted file mode 100644 index ba0d009..0000000 --- a/src/pages/LEFT OVER BITS.md +++ /dev/null @@ -1,328 +0,0 @@ - - - - - - - - - -Sometimes it's necessary to convert from a non-nullable column to a nullable one in a query. For example, if we're performing an outer join on two tables, it is always possible that columns in one table will contain `null` values. We will see examples of this in [Chapter 5](#joins). In these circumstances, we can convert a non-`Optional` column into an `Optional` one using the `?` operator: - -~~~ scala -messages.map(_.sender.?) -// res19: scala.slick.lifted.Query[ -// scala.slick.lifted.Column[Option[String]], -// Option[String], -// Seq -// ] = scala.slick.lifted.WrappingQuery@38e47d45 -~~~ - - - - - - - - -Another possible surprise is that matching on a specific non-null value works with a `String` or an `Option[String]`: - -~~~ scala -val oe: Option[String] = Some("dave@example.org") -val e: String = "dave@example.org" - -// True: -users.filter(_.email === oe).list == users.filter(_.email === e).list -~~~ - -To understand what's going on here, we need to review the types involved in the query: - -![Column Types in the Query](src/img/query-types.png) - -Although the values being tested for are `String` and `Option[String]`, -Slick implicitly lifts these values into `Column[T]` types for comparison. -From there, Slick will compare the `Column[Option[String]]` we defined in the table -with either the `Column[String]` or `Column[Option[String]]` in the query. -This is not specific to `String` types---it is a pattern for all the optional columns. - - - - - - - -## Counting Results: The *length* Method and Column Queries - -We can create a `Column` expression representing the length of any `Query` by calling its `length` method: - -~~~ scala -messages.length -// res8: scala.slick.lifted.Column[Int] = -// Column(Apply Function count(*)) -~~~ - -We can either use this in larger expressions or, interestingly, invoke it directly: - -~~~ scala -messages.length.run -// res9: Int = 4 -~~~ - -But how does this work? A `Column` isn't a `Query` so how can we invoke it? - -Slick provides limited support for running column expressions directly against the database. We can't use all of the invoker methods described in the last section, but we can use `run` and `selectStatement`. Here's a simple example: - -~~~ scala -((10 : Column[Int]) + 20).run -// res10: Int = 30 -~~~ - -Here we create a constant `Column[Int]` of value `10`, and use the `+` method described in the [Numeric Column Methods](#NumericColumnMethods) section to create a simple SQL expression `10 + 20`. We use the `run` method to execute this against the database and retrieve the value `30`. If we use the `selectStatement` method we'll see that the database is actually doing all of the math: - -~~~ scala -((10 : Column[Int]) + 20).selectStatement -// res11: String = select 10 + 20 -~~~ - -It's this same process that allows us to call `messages.length.run`. Let's look at the SQL: - -~~~ scala -messages.length.selectStatement -// res9: String = -// select x2.x3 from ( -// select count(1) as x3 from ( -// select x4."sender" as x5, x4."content" as x6, x4."id" as x7 -// from "message" x4 -// ) x8 -// ) x2 -~~~ - -Slick generates an overly complicated query here. There are two issues: - - 1. the query contains three nested `SELECT` statements instead of one; - 2. the query counts distinct values of the tuple `(sender, content, id)`, instead of simply counting `ids`. - -These issues cause varying amounts of trouble depending on the quality of the query planner in our database. PostgreSQL, for example, has an excellent query planner that will optimise the nested selects away, although your mileage may vary with other database engines. - -We can help our database out by being slightly cleverer in our choice of query. For example, counting values of a single indexed primary key is considerably faster than counting entire rows: - -~~~ scala -messages.map(_.id).length.selectStatement -// res10: String = -// select x2.x3 from ( -// select count(1) as x3 from ( -// select x4."id" as x5 from "message" x4 -// ) x6 -// ) x2 -~~~ - -When using Slick, or indeed any database library that generates SQL from a DSL, it's important to keep one eye on the performance characteristics of the queries we're generating. Knowing SQL and the query planning capabilities of the database server is as important as knowing the query DSL itself. - - - - - - - - -#### Boilerplate Free Primary Keys - -Modify the definition of `Occupant` to use type parameter definition of table primary keys, assume `User` and `Room` are already implement this way. - -~~~ scala -case class Occupant(roomId: Long, userId: Long) - -class OccupantTable(tag: Tag) extends Table[Occupant](tag, "occupant") { - def roomId = column[Long]("room") - def userId = column[Long]("user") - - def pk = primaryKey("room_user_pk", (roomId, userId)) - - def * = (roomId, userId) <> (Occupant.tupled, Occupant.unapply) -} - -lazy val occupants = TableQuery[OccupantTable] -~~~ - -
-We need to update the existing definition of `roomId` and `userId` from `Long` to `PK[TableName]`: - -~~~ scala -case class Occupant(roomId: PK[RoomTable], userId: PK[UserTable]) - -class OccupantTable(tag: Tag) extends Table[Occupant](tag, "occupant") { - def roomId = column[PK[RoomTable]]("room") - def userId = column[PK[UserTable]]("user") - - def pk = primaryKey("room_user_pk", (roomId, userId)) - - def * = (roomId, userId) <> (Occupant.tupled, Occupant.unapply) -} - -lazy val occupants = TableQuery[OccupantTable] -~~~ - -
- - - - -Let's look at one more table to see what a more involved query looks like. You'll probably want to refer to the schema for this chapter shown in figure 5.1. - - -We can retrieve all messages by Dave in the Air Lock, again as an implicit join: - -~~~ scala -val daveId: PK[UserTable] = ??? -val roomId: PK[RoomTable] = ??? - -val davesMessages = for { - message <- messages - user <- users - room <- rooms - if message.senderId === user.id && - message.roomId === room.id && - user.id === daveId && - room.id === airLockId -} yield (message.content, user.name, room.title) -~~~ - -As we have foreign keys (`sender`, `room`) defined on our `message` table, we can use them in the query. Here we have reworked the same example to use the foreign keys: - -~~~ scala -val daveId: PK[UserTable] = ??? -val roomId: PK[RoomTable] = ??? - -val davesMessages = for { - message <- messages - user <- message.sender - room <- message.room - if user.id === daveId && - room.id === airLockId -} yield (message.content, user.name, room.title) -~~~ - -Both cases will produce SQL something like this: - -~~~ sql -select - m."content", u."name", r."title" -from - "message" m, "user" u, "room" r -where ( - (u."id" = m."sender") and - (r."id" = m."room") -) and ( - (u."id" = 1) and - (r."id" = 1) -) -~~~ - ------------------------- - -_This applies to Slick 2, but in Slick 3 the static query stuff has the comment of "TODO port to 3.0". So it may come back. _ - -### Constructing Queries - -In addition to using `$` to reference Scala values in queries, you can build up queries incrementally. - -The queries produced by both and `sql` and `sqlu` (which we see later) are `StaticQuery`s. As the word "static" suggests, -these kinds of queries do not compose, other than via a form of string concatenation. - -The operations available to you are: - -* `+` to append a string to the query, giving a new query; and -* `+?` to add a value, and correctly escape the value for use in SQL. - -As an example, we can find all IDs for messages... - -~~~ scala -val query = sql"""SELECT "id" from "message"""".as[Long] -~~~ - -...and then create a new query based on this to filter by message content: - -``` scala -val pattern = "%Dave%" -val sensitive = query + """ WHERE "content" NOT LIKE """ +? pattern -``` - -The result of this is a new `StaticQuery` which we can execute. - -------------------------- - -_The same applies for sqlu_ - -### Composing Updates - -All the techniques described for selects applies for composing plain SQL updates. - -As an example, we can start with an unconditional update... - -~~~ scala -val query = sqlu"""UPDATE "message" SET "content" = CONCAT("content", $char)""" -~~~ - -...and then create an alternative query using the `+` method defined on `StaticQuery`: - -~~~ scala -val pattern = "%!" -val sensitive = query + """ WHERE "content" NOT LIKE """ +? pattern -~~~ - -The resulting query would append an `!` only to rows that don't already end with that character. - - ------------------------ - -_StaticQuery exercises... may come back one day...._ - -### String Interpolation Mistake - -When we constructed our `sensitive` query, we used `+?` to include a `String` in our query. - -It looks as if we could have used regular string interpolation instead: - -``` scala -val sensitive = query + s""" WHERE "content" NOT LIKE $pattern""" -``` - -Why didn't we do that? - -
-The standard Scala string interpolator doesn't have any knowledge of SQL. It doesn't know that `Strings` need to be quoted in single quotes, for example. - -In contrast, Slick's `sql` and `sqlu` interpolators do understand SQL and do the correct embedding of values. When working with regular `String`s, as we were, you must use `+?` to ensure values are correctly encoded for SQL. -
- - -### Unsafe Composition - -Here's a utility method that takes any string, and return a query to append the string to all messages. - -~~~ scala -def append(s: String) = - sqlu"""UPDATE "message" SET "content" = CONCAT("content", $s)""" -~~~ - -Using, but not modifying, the method, restrict the update to messages from "HAL". - -Would it be possible to construct invalid SQL? - -
-~~~ scala -def append(s: String) = - sqlu"""UPDATE "message" SET "content" = CONCAT("content", $s)""" - -val halOnly = append("!") + """ WHERE "sender" = 'HAL' """ -~~~ - -It is very easy to get this query wrong and only find out at run-time. Notice, for example, we had to include a space before "WHERE" and use the correct single quoting around "HAL". -
- - - - - - diff --git a/src/pages/links.md b/src/pages/links.md index 9242a02..a84803b 100644 --- a/src/pages/links.md +++ b/src/pages/links.md @@ -1,16 +1,16 @@ [link-example]: https://github.com/underscoreio/essential-slick-code/tree/3.0 [link-slick]: http://slick.lightbend.com/ -[link-ref-dbs]: http://slick.lightbend.com/doc/3.1.1/supported-databases.html -[link-ref-gen]: http://slick.lightbend.com/doc/3.1.1/code-generation.html -[link-ref-h2driver]: http://slick.lightbend.com/doc/3.1.1/api/#slick.driver.H2Driver -[link-ref-orm]: http://slick.lightbend.com/doc/3.1.1/orm-to-slick.html -[link-ref-actions]: http://slick.lightbend.com/doc/3.1.1/dbio.html -[link-slick-column-options]: http://slick.lightbend.com/doc/3.1.1/api/index.html#slick.ast.ColumnOption -[link-slick-hlist]: http://slick.lightbend.com/doc/3.1.1/api/#slick.collection.heterogeneous.HList -[link-source-dbPublisher]: http://slick.lightbend.com/doc/3.1.1/api/index.html#slick.backend.DatabasePublisher -[link-source-extmeth]: https://github.com/slick/slick/blob/3.1/slick/src/main/scala/slick/lifted/ExtensionMethods.scala -[link-slick-streaming]: http://slick.lightbend.com/doc/3.1.1/database.html#streaming +[link-ref-dbs]: http://slick.lightbend.com/doc/3.2.0/supported-databases.html +[link-ref-gen]: http://slick.lightbend.com/doc/3.2.0/code-generation.html +[link-ref-h2driver]: http://slick.lightbend.com/doc/3.2.0/api/#slick.jdbc.H2Profile +[link-ref-orm]: http://slick.lightbend.com/doc/3.2.0/orm-to-slick.html +[link-ref-actions]: http://slick.lightbend.com/doc/3.2.0/dbio.html +[link-slick-column-options]: http://slick.lightbend.com/doc/3.2.0/api/index.html#slick.ast.ColumnOption +[link-slick-hlist]: http://slick.lightbend.com/doc/3.2.0/api/#slick.collection.heterogeneous.HList +[link-source-dbPublisher]: http://slick.lightbend.com/doc/3.2.0/api/index.html#slick.basic.DatabasePublisher +[link-source-extmeth]: https://github.com/slick/slick/blob/3.2.0/slick/src/main/scala/slick/lifted/ExtensionMethods.scala +[link-slick-streaming]: http://slick.lightbend.com/doc/3.2.0/database.html#streaming [link-hikari]: http://brettwooldridge.github.io/HikariCP/ [link-config]: https://github.com/typesafehub/config