├── .gitignore ├── README.md ├── Vagrantfile ├── pg-setup.sh ├── project ├── Build.scala └── plugins.sbt ├── schema.sql └── src ├── main └── scala │ └── noisycode │ └── akkajdbc │ ├── .DS_Store │ ├── AkkaJdbcKernel.scala │ ├── Gists.scala │ └── model │ └── Person.scala └── test ├── resources └── reference.conf └── scala └── noisycode └── akkajdbc └── PersonClientTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | project/project 2 | project/target 3 | target 4 | *~ 5 | .vagrant 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | akka-jdbc-post 2 | ==== 3 | This is the supporting example project for my [blog post on Akka and JDBC](http://noisycode.com/blog/2014/07/27/akka-and-jdbc-to-services/) that follows the promise + circuit breaker approach. 4 | 5 | The [Vagrantfile](http://vagrantup.com) provided will boot the PostgreSQL instance required by the tests, e.g. 6 | 7 | $ vagrant up 8 | $ sbt test 9 | 10 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | 6 | end 7 | 8 | Vagrant::Config.run do |config| 9 | config.vm.box = "precise64" 10 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 11 | config.vm.provision :shell, :path => "pg-setup.sh" 12 | config.vm.forward_port 5432, 15432 13 | end -------------------------------------------------------------------------------- /pg-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This was heavily based on https://github.com/jackdb/pg-app-dev-vm/blob/master/Vagrant-setup/bootstrap.sh 4 | # Use/reuse as you see fit at your own risk as this is intended solely for integration tests and basic demo. 5 | 6 | DB_USER=akkajdbc 7 | DB_PASS=akkajdbc 8 | 9 | PG_VERSION=9.3 10 | PG_REPO_APT_SOURCE=/etc/apt/sources.list.d/pgdg.list 11 | 12 | echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > "$PG_REPO_APT_SOURCE" 13 | wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | apt-key add - 14 | 15 | apt-get update 16 | 17 | apt-get -y install "postgresql-$PG_VERSION" "postgresql-contrib-$PG_VERSION" 18 | 19 | PG_CONF="/etc/postgresql/$PG_VERSION/main/postgresql.conf" 20 | PG_HBA="/etc/postgresql/$PG_VERSION/main/pg_hba.conf" 21 | PG_DIR="/var/lib/postgresql/$PG_VERSION/main" 22 | 23 | # Edit postgresql.conf to change listen address to '*': 24 | sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" "$PG_CONF" 25 | 26 | # Append to pg_hba.conf to add password auth: 27 | echo "host all all all md5" >> "$PG_HBA" 28 | 29 | service postgresql restart 30 | 31 | sudo -u postgres createdb akkajdbc 32 | sudo -u postgres psql -f /vagrant/schema.sql akkajdbc 33 | 34 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import akka.sbt.AkkaKernelPlugin 4 | import akka.sbt.AkkaKernelPlugin.{ Dist, outputDirectory, distJvmOptions} 5 | 6 | object ProtoTruthBuild extends Build { 7 | val Organization = "noisycode" 8 | val Version = "1.0" 9 | val ScalaVersion = "2.10.3" 10 | 11 | lazy val ProtoTruthKernel = Project( 12 | id = "akka-jdbc", 13 | base = file("."), 14 | settings = defaultSettings ++ AkkaKernelPlugin.distSettings ++ Seq( 15 | libraryDependencies ++= Dependencies.akkaJdbc, 16 | distJvmOptions in Dist := "-Xms1G -Xmx2G", 17 | outputDirectory in Dist := file("target/akka-jdbc") 18 | ) 19 | ) 20 | 21 | lazy val buildSettings = Defaults.defaultSettings ++ Seq( 22 | organization := Organization, 23 | version := Version, 24 | scalaVersion := ScalaVersion, 25 | crossPaths := false, 26 | organizationName := "noisycode", 27 | organizationHomepage := Some(url("http://noisycode.com")) 28 | ) 29 | 30 | lazy val defaultSettings = buildSettings ++ Seq( 31 | // compile options 32 | scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked"), 33 | javacOptions ++= Seq("-Xlint:unchecked", "-Xlint:deprecation") 34 | ) 35 | } 36 | 37 | object Dependencies { 38 | import Dependency._ 39 | 40 | val akkaJdbc = Seq(akkaActor, akkaKernel, postgres, scalaTest, akkaTestkit) 41 | } 42 | 43 | object Dependency { 44 | val akkaVersion = "2.3.4" 45 | 46 | val akkaActor = "com.typesafe.akka" %% "akka-actor" % akkaVersion 47 | val akkaKernel = "com.typesafe.akka" %% "akka-kernel" % akkaVersion 48 | val postgres = "postgresql" % "postgresql" % "9.1-901-1.jdbc4" 49 | 50 | val scalaTest = "org.scalatest" %% "scalatest" % "2.0" 51 | val akkaTestkit = "com.typesafe.akka" %% "akka-testkit" % akkaVersion 52 | } 53 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.akka" % "akka-sbt-plugin" % "2.2.3") 2 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | -- clearly don't do this in a real project, this is just for convenience here: 2 | CREATE USER akkajdbc WITH PASSWORD 'akkajdbc'; 3 | 4 | -- our basic aggregate root: 5 | CREATE TABLE person ( 6 | id serial primary key, 7 | name text, 8 | email text unique 9 | ); 10 | 11 | -- obviously minimal: 12 | CREATE TABLE address ( 13 | owner integer references person (id), 14 | street text, 15 | city text 16 | ); 17 | 18 | GRANT SELECT,INSERT,UPDATE,DELETE ON person TO akkajdbc; 19 | GRANT SELECT,INSERT,UPDATE,DELETE ON address TO akkajdbc; 20 | GRANT SELECT,USAGE ON person_id_seq TO akkajdbc; 21 | 22 | -------------------------------------------------------------------------------- /src/main/scala/noisycode/akkajdbc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j14159/akka-jdbc-post/83c4a0fc7a062ff4c58fbdfbaa044e2a98987715/src/main/scala/noisycode/akkajdbc/.DS_Store -------------------------------------------------------------------------------- /src/main/scala/noisycode/akkajdbc/AkkaJdbcKernel.scala: -------------------------------------------------------------------------------- 1 | package noisycode.akkajdbc 2 | 3 | import akka.actor.{ ActorRef, ActorSystem } 4 | import akka.kernel.Bootable 5 | import akka.pattern.CircuitBreaker 6 | 7 | import com.typesafe.config.Config 8 | 9 | import noisycode.akkajdbc.model.{ PersonClient, PersonDaoFactory } 10 | 11 | import scala.concurrent.duration._ 12 | 13 | /** 14 | * A simple example microkernel boot class to show how one might wire everything 15 | * together. 16 | */ 17 | class AkkaJdbcKernel extends Bootable { 18 | val system = ActorSystem("akkajdbc-example") 19 | 20 | /** 21 | * A simple example of a [[PersonClient]] you can easily use wherever necessary. 22 | * I would tend to avoid this in favour of actors leveraging the [[PersonClient]] 23 | * trait directly but that's not a hard and fast rule. 24 | */ 25 | class StandaloneClient(val breaker: CircuitBreaker, sys: ActorSystem) extends PersonClient { 26 | val personActor = PersonDaoFactory.newPool(sys) 27 | } 28 | 29 | /** 30 | * Get a [[CircuitBreaker]] using configuration. 31 | */ 32 | def breaker(config: Config): CircuitBreaker = 33 | breaker(config.getInt("breaker.max-failures"), 34 | config.getLong("breaker.call-timeout"), 35 | config.getLong("breaker.reset-timeout")) 36 | 37 | /** 38 | * Get a configured [[CircuitBreaker]], times in MS. 39 | */ 40 | def breaker(maxFailures: Int, callTimeout: Long, resetTimeout: Long): CircuitBreaker = 41 | new CircuitBreaker( 42 | system.dispatcher, 43 | system.scheduler, 44 | maxFailures, 45 | callTimeout millis, 46 | resetTimeout millis) 47 | 48 | def startup = { 49 | val client = new StandaloneClient(breaker(system.settings.config), system) 50 | 51 | /* 52 | * here you'd start any actors handling web requests, etc. Generally speaking, 53 | * I would favour my actors extending the [[PersonClient]] trait directly rather than 54 | * sharing the above standalone client. 55 | */ 56 | } 57 | 58 | def shutdown = { 59 | system.shutdown() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/noisycode/akkajdbc/Gists.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Source for the gists used in the blog post. 3 | */ 4 | 5 | package noisycode.akkajdbc 6 | 7 | import akka.actor._ 8 | import akka.actor.SupervisorStrategy.Restart 9 | import akka.routing.RoundRobinPool 10 | import akka.pattern.ask 11 | import akka.util.Timeout 12 | 13 | import java.sql._ 14 | 15 | import scala.concurrent.Future 16 | import scala.concurrent.duration._ 17 | 18 | object Gist1 { 19 | val databaseUrl = "postgresql://some-hostname:5432/db-name" 20 | 21 | Class.forName("my.sql.database.driver.classname") 22 | 23 | class BasicJdbcActor(connFac: () => Connection) extends Actor { 24 | lazy val conn = connFac() 25 | 26 | override def preRestart(why: Throwable, msg: Option[Any]): Unit = 27 | try { conn.close() } 28 | 29 | def receive = { 30 | case anything => throw new Exception("Where's my implementation?") 31 | } 32 | } 33 | 34 | def connFac = () => DriverManager.getConnection(databaseUrl) 35 | 36 | def makeMeAnActor(sys: ActorSystem): ActorRef = 37 | sys.actorOf(Props(new BasicJdbcActor(connFac))) 38 | } 39 | 40 | object Gist2 { 41 | import Gist1._ 42 | 43 | // very naive, be more specific based on your problem: 44 | val restartStrategy = OneForOneStrategy( 45 | maxNrOfRetries = 10, 46 | withinTimeRange = 1 minute) { 47 | case _ => Restart 48 | } 49 | 50 | def newPool(sys: ActorSystem): ActorRef = { 51 | val props = Props(new BasicJdbcActor(connFac)) 52 | val pool = RoundRobinPool(4, supervisorStrategy = restartStrategy) 53 | sys.actorOf(pool.props(props)) 54 | } 55 | } 56 | 57 | object Gist4 { 58 | import Gist1._ 59 | import Gist2._ 60 | 61 | def newBulkheadingPool(sys: ActorSystem): ActorRef = { 62 | val props = Props(new BasicJdbcActor(connFac)) 63 | .withDispatcher("my-dispatcher") 64 | val pool = RoundRobinPool(4, supervisorStrategy = restartStrategy) 65 | sys.actorOf(pool.props(props)) 66 | } 67 | } 68 | 69 | object Gist5 { 70 | case class Person(name: String, email: String) 71 | case class PersonById(id: Int) 72 | 73 | class PersonDao(cf: () => Connection) extends Actor { 74 | lazy val conn = cf() 75 | 76 | override def preRestart(why: Throwable, msg: Option[Any]): Unit = 77 | try { conn.close() } 78 | 79 | def receive = { 80 | case Person(n, e) => 81 | //call insert function with above connection 82 | sender ! 1 // mock person ID 83 | case PersonById(id) => 84 | //get person from connection above 85 | sender ! Person("name", "email") // mock 86 | } 87 | } 88 | } 89 | 90 | object Gist6 { 91 | import Gist5._ 92 | 93 | trait PersonClient { 94 | // supply a router with a pool of PersonDao: 95 | val personPool: ActorRef 96 | 97 | // how long should we wait for a response from PersonDao: 98 | val timeoutInMillis: Long 99 | 100 | implicit val timeout = Timeout(timeoutInMillis millis) 101 | 102 | def addPerson(p: Person): Future[Int] = 103 | (personPool ? p).mapTo[Int] 104 | 105 | def personById(id: Long): Future[Person] = 106 | (personPool ? PersonById).mapTo[Person] 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/scala/noisycode/akkajdbc/model/Person.scala: -------------------------------------------------------------------------------- 1 | package noisycode.akkajdbc.model 2 | 3 | import akka.actor._ 4 | import akka.actor.SupervisorStrategy.Restart 5 | import akka.pattern.{ ask, CircuitBreaker } 6 | import akka.routing.RoundRobinPool 7 | import akka.util.Timeout 8 | 9 | import java.sql._ 10 | 11 | import scala.concurrent.duration._ 12 | import scala.concurrent.{ ExecutionContext, Future, Promise } 13 | import scala.util.{ Failure, Success, Try } 14 | 15 | /** 16 | * Our example aggregate root. 17 | */ 18 | case class Person(id: Option[Int], name: String, email: String, addresses: Seq[Address]) 19 | case class Address(street: String, city: String) 20 | 21 | /** 22 | * Provides a simple way to get a pool of Person DAO actors behind a router. 23 | */ 24 | object PersonDaoFactory { 25 | /** 26 | * This is the most naive possible strategy in that it always restarts. 27 | */ 28 | val restartStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { 29 | case _ => Restart 30 | } 31 | 32 | /** 33 | * Use the given [[ActorSystem]]'s config to make a new pool of 34 | * DAOs behind a router. 35 | */ 36 | def newPool(sys: ActorSystem): ActorRef = { 37 | val count = sys.settings.config.getInt("person-worker-count") 38 | val connUrl = sys.settings.config.getString("db.url") 39 | 40 | Class.forName(sys.settings.config.getString("db.driver")) 41 | 42 | val connFac = () => DriverManager.getConnection(connUrl) 43 | 44 | sys.actorOf(RoundRobinPool(count, supervisorStrategy = restartStrategy) 45 | .props(Props(new PersonDaoActor(connFac)).withDispatcher("person-dispatcher"))) 46 | } 47 | } 48 | 49 | /** 50 | * Here is the implementation of our [[Future]]-based client/interface to the DAO. Within this 51 | * trait is where you would swap out calls to an internal actor for calls to an external service 52 | * when/if it is necessary/desirable to do so. 53 | */ 54 | trait PersonClient { 55 | /** 56 | * A reference to an instance of [[PersonDaoActor]] or a router in front of a pool of them. 57 | */ 58 | val personActor: ActorRef 59 | /** 60 | * All calls will be protected with this circuit breaker. 61 | */ 62 | val breaker: CircuitBreaker 63 | 64 | def addPerson(person: Person): Future[Int] = withBreaker(PersonApi.AddPerson(Promise[Int](), person)) 65 | 66 | def personById(id: Int): Future[Option[Person]] = 67 | withBreaker(PersonApi.PersonById(Promise[Option[Person]](), id)) 68 | 69 | def personByEmail(email: String): Future[Option[Person]] = 70 | withBreaker(PersonApi.PersonByEmail(Promise[Option[Person]](), email)) 71 | 72 | def withBreaker[T](msg: PersonApi.PersonDaoMsg[T]): Future[T] = { 73 | personActor ! msg 74 | breaker.withCircuitBreaker(msg.res.future) 75 | } 76 | } 77 | 78 | /** 79 | * All messages that a [[PersonDaoActor]] understands are given here. 80 | */ 81 | private [model] object PersonApi { 82 | /** 83 | * We have this trait so that a [[PersonDaoActor]] can send back a failure as part of restart 84 | * behaviour by getting a handle on the original [[Promise]]. 85 | */ 86 | trait PersonDaoMsg[T] { 87 | val res: Promise[T] 88 | } 89 | 90 | case class AddPerson(res: Promise[Int], p: Person) extends PersonDaoMsg[Int] 91 | case class PersonById(res: Promise[Option[Person]], id: Int) extends PersonDaoMsg[Option[Person]] 92 | case class PersonByEmail(res: Promise[Option[Person]], email: String) extends PersonDaoMsg[Option[Person]] 93 | } 94 | 95 | class PersonDaoActor(connFactory: () => Connection) extends Actor with ActorLogging with PersonJdbc { 96 | lazy val conn = connFactory() 97 | 98 | /** 99 | * The behaviour here assumes that the supervision policy has made the right 100 | * decision to actually replace this actor. 101 | */ 102 | override def preRestart(reason: Throwable, msg: Option[Any]): Unit = { 103 | msg match { 104 | // send the failure to the caller for this one, they might want a bit more info: 105 | case Some(daoMsg) if daoMsg.isInstanceOf[PersonApi.PersonDaoMsg[_]] => 106 | daoMsg.asInstanceOf[PersonApi.PersonDaoMsg[_]].res.failure(reason) 107 | case Some(other) => {} //somebody sent a bad message, ignore 108 | case None => {} 109 | } 110 | 111 | try { conn.close } 112 | } 113 | 114 | override def postStop(): Unit = { 115 | try { conn.close() } 116 | super.postStop() 117 | } 118 | 119 | //we leave all fault handling to the supervisor: 120 | def receive = { 121 | case PersonApi.AddPerson(res, p) => 122 | log.debug(s"Making a person: ${p}") 123 | res success addPerson(conn, p) 124 | case PersonApi.PersonById(res, id) => res success getPersonById(conn, id) 125 | case PersonApi.PersonByEmail(res, email) => res success getPersonByEmail(conn, email) 126 | } 127 | } 128 | 129 | /** 130 | * Keeping the actual DB function separate from the actor keeps the actor code 131 | * clean and simple while also letting us write simple integration tests against 132 | * the functions without needing the actor should one want to. Note the complete 133 | * lack of try/catch and Try since decisions about error handling are all up to the 134 | * actor and/or its supervision strategy. 135 | * 136 | * Using an ORM or something like slick/jooq/etc is completely acceptable here, 137 | * I just have no particular objection to the slight overhead involved in writing JDBC 138 | * for something simple like this. 139 | */ 140 | private [model] trait PersonJdbc { 141 | val clause = "AND address.owner=person.id" 142 | val byIdQ = s"SELECT person.id,name,email,street,city FROM person,address WHERE person.id=? ${clause}" 143 | val byEmailQ = s"SELECT person.id,name,email,street,city FROM person,address WHERE email=? ${clause}" 144 | 145 | val insertPerson = "INSERT INTO person (name,email) VALUES (?,?) RETURNING id" 146 | val insertAddress = "INSERT INTO address (owner,street,city) VALUES (?,?,?)" 147 | 148 | /** 149 | * Add a [[Person]] to the database and return their new ID. 150 | */ 151 | def addPerson(c: Connection, p: Person): Int = { 152 | val s = c.prepareStatement(insertPerson) 153 | s.setString(1, p.name) 154 | s.setString(2, p.email) 155 | val id = idFromResultSet(s.executeQuery()) 156 | s.close() 157 | 158 | addAddresses(c, id, p.addresses) 159 | id 160 | } 161 | 162 | def addAddresses(c: Connection, personId: Int, a: Seq[Address]): Unit = { 163 | val s = c.prepareStatement(insertAddress) 164 | a.foreach { case Address(street, city) => 165 | s.clearParameters() 166 | s.setInt(1, personId) 167 | s.setString(2, street) 168 | s.setString(3, city) 169 | s.execute() 170 | } 171 | 172 | s.close() 173 | } 174 | 175 | private def idFromResultSet(rs: ResultSet): Int = { 176 | rs.next() 177 | val res = rs.getInt(1) 178 | rs.close() 179 | res 180 | } 181 | 182 | def getPersonById(c: Connection, id: Int): Option[Person] = 183 | runPersonQuery(c, byIdQ, _.setInt(1, id)) 184 | 185 | def getPersonByEmail(c: Connection, email: String): Option[Person] = 186 | runPersonQuery(c, byEmailQ, _.setString(1, email)) 187 | 188 | def runPersonQuery(c: Connection, q: String, f: PreparedStatement => Unit): Option[Person] = { 189 | val s = c.prepareStatement(q) 190 | f(s) 191 | val res = personFromResultSet(s.executeQuery()) 192 | s.close() 193 | res 194 | } 195 | 196 | /** 197 | * We expect single Person instances so this will both position and close 198 | * the ResultSet. It should be immediately obvious that I'm making some 199 | * large assumptions in the interest of brevity. 200 | */ 201 | def personFromResultSet(rs: ResultSet): Option[Person] = { 202 | val res = 203 | if(rs.next() && !rs.isAfterLast) 204 | Some(Person(Some(rs.getInt(1)), rs.getString(2), rs.getString(3), 205 | addressFrom(rs) :: addressesFromPositionedResultSet(rs).toList)) 206 | else 207 | None 208 | 209 | rs.close 210 | res 211 | } 212 | 213 | /** 214 | * Closing over the chunk of side-effects and mutability in the ResultSet is 215 | * hardly ideal but I'm making the assumption that one would not be foolish 216 | * enough to share this or put it in a potentially concurrent situation outside 217 | * of the actor leveraging this trait's functionality. Knives come with sharp 218 | * edges. 219 | */ 220 | def addressesFromPositionedResultSet(rs: ResultSet): Iterator[Address] = 221 | new Iterator[Address] { 222 | def hasNext() = rs.next() 223 | def next() = addressFrom(rs) 224 | } 225 | 226 | def addressFrom(rs: ResultSet) = Address(rs.getString(4), rs.getString(5)) 227 | } 228 | -------------------------------------------------------------------------------- /src/test/resources/reference.conf: -------------------------------------------------------------------------------- 1 | person-worker-count = 4 2 | 3 | db { 4 | driver: "org.postgresql.Driver" 5 | //don't keep your user/pass like this in production: 6 | url: "jdbc:postgresql://localhost:15432/akkajdbc?user=akkajdbc&password=akkajdbc" 7 | } 8 | 9 | breaker { 10 | max-failures: 3 11 | call-timeout: 500 12 | reset-timeout: 60000 13 | } 14 | 15 | person-dispatcher { 16 | type = Dispatcher 17 | executor = "fork-join-executor" 18 | fork-join-executor { 19 | parallelism-min = 2 20 | //2 threads per core 21 | parallelism-factor = 2.0 22 | 23 | // We won't actually need this many since our worker count above is 4. 24 | // Just remember that FJE can create more up to a given max when necessary: 25 | parallelism-max = 8 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/scala/noisycode/akkajdbc/PersonClientTest.scala: -------------------------------------------------------------------------------- 1 | package noisycode.akkajdbc 2 | 3 | import akka.util.Timeout 4 | 5 | import noisycode.akkajdbc.model.{ Person, Address } 6 | 7 | import org.scalatest._ 8 | 9 | import scala.concurrent.Await 10 | import scala.concurrent.duration._ 11 | 12 | /** 13 | * Very basic integration test using the provided Vagrant VM. The cleanup in afterAll() 14 | * is obviously a bit brittle. 15 | */ 16 | class PersonClientTest extends FlatSpec with Matchers with BeforeAndAfterAll { 17 | // Using this for convenience and to test whole chain of methods: 18 | val kernel = new AkkaJdbcKernel() 19 | import kernel._ 20 | val client = new StandaloneClient(breaker(kernel.system.settings.config), system) 21 | 22 | val person1 = Person(None, "Some Person", "who@where.com", 23 | List(Address("123 Nowhere St", "Nowheresville"))) 24 | 25 | val person2 = Person(None, "Some person with 2 houses", "property@where.com", 26 | List(Address("321 Nowhere St", "Nowheresville"), 27 | Address("456 Other Ave", "Nowheresville"))) 28 | 29 | "A StandaloneClient" should "add and retrieve a Person correctly" in { 30 | 31 | val id1 = Await.result(client.addPerson(person1), 1 second) 32 | val id2 = Await.result(client.addPerson(person2), 1 second) 33 | 34 | val expected1 = Some(person1.copy(id = Some(id1))) 35 | val expected2 = Some(person2.copy(id = Some(id2))) 36 | 37 | Await.result(client.personById(id1), 1 second) should be (expected1) 38 | Await.result(client.personByEmail(expected1.get.email), 1 second) should be (expected1) 39 | Await.result(client.personById(id2), 1 second) should be (expected2) 40 | } 41 | 42 | override def afterAll() { 43 | val c = java.sql.DriverManager.getConnection(kernel.system.settings.config.getString("db.url")) 44 | val personDelete = c.prepareStatement("delete from person where email=? OR email=?") 45 | val addressDelete = c.prepareStatement("delete from address where city=?") 46 | 47 | personDelete.setString(1, person1.email) 48 | personDelete.setString(2, person2.email) 49 | addressDelete.setString(1, person1.addresses.head.city) 50 | 51 | addressDelete.execute() 52 | personDelete.execute() 53 | 54 | personDelete.close() 55 | addressDelete.close() 56 | c.close() 57 | 58 | kernel.shutdown 59 | } 60 | } 61 | --------------------------------------------------------------------------------