├── .gitignore ├── README.md ├── build.sbt ├── domain └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ └── domain │ ├── entity │ ├── Message.scala │ └── User.scala │ ├── repository │ ├── MessageRepository.scala │ ├── UserRepository.scala │ └── scalikejdbc │ │ ├── DomainDB.scala │ │ ├── MessageRepositoryImpl.scala │ │ └── UserRepositoryImpl.scala │ └── service │ ├── MessageService.scala │ └── UserService.scala ├── fujitask-scalikejdbc └── src │ └── main │ └── scala │ └── fujitask │ └── scalikejdbc │ ├── Transaction.scala │ └── package.scala ├── fujitask └── src │ ├── main │ └── scala │ │ └── fujitask │ │ ├── Task.scala │ │ └── Transaction.scala │ └── test │ └── scala │ ├── Helper.scala │ └── TaskTest.scala └── project └── build.properties /.gitignore: -------------------------------------------------------------------------------- 1 | project/project 2 | project/target 3 | target 4 | .idea 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fujitask-simple 2 | 3 | ## License 4 | [MIT](https://opensource.org/licenses/MIT) 5 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val commonSettings = Seq( 2 | scalaVersion := "2.11.7", 3 | scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlint", "-language:_") 4 | ) 5 | 6 | lazy val root = (project in file(".")).aggregate(fujitask, fujitaskScalikeJDBC, domain) 7 | 8 | lazy val fujitask = project.settings(commonSettings:_*).settings( 9 | commonSettings ++ Seq( 10 | libraryDependencies ++= Seq( 11 | "com.github.scalaprops" %% "scalaprops" % "0.1.15" % "test", 12 | "com.github.scalaprops" %% "scalaprops-scalazlaws" % "0.1.15" % "test" 13 | ), 14 | testFrameworks += new TestFramework("scalaprops.ScalapropsFramework") 15 | ):_* 16 | ) 17 | 18 | lazy val fujitaskScalikeJDBC = (project in file("fujitask-scalikejdbc")).settings( 19 | commonSettings ++ Seq( 20 | libraryDependencies ++= Seq( 21 | "org.scalikejdbc" %% "scalikejdbc" % "2.2.9" 22 | ) 23 | ):_* 24 | ).dependsOn(fujitask) 25 | 26 | lazy val domain = project.settings( 27 | commonSettings ++ Seq( 28 | libraryDependencies ++= Seq( 29 | "org.scalikejdbc" %% "scalikejdbc-config" % "2.2.9", 30 | "com.h2database" % "h2" % "1.4.190" 31 | ), 32 | initialCommands in console := """ 33 | import scala.concurrent._ 34 | import scala.concurrent.duration.Duration 35 | import domain.service._ 36 | import domain.repository.scalikejdbc._ 37 | def getValue[A](f: Future[A]): A = Await.result(f, Duration(300, "seconds")) 38 | DomainDB.setup() 39 | DomainDB.createTables() 40 | """ 41 | ):_* 42 | ).dependsOn(fujitaskScalikeJDBC) 43 | -------------------------------------------------------------------------------- /domain/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # JDBC settings 2 | db.default.driver="org.h2.Driver" 3 | db.default.url="jdbc:h2:mem:default" 4 | db.default.user="sa" 5 | db.default.password="" 6 | # Connection Pool settings 7 | db.default.poolInitialSize=10 8 | db.default.poolMaxSize=20 9 | db.default.connectionTimeoutMillis=1000 10 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/entity/Message.scala: -------------------------------------------------------------------------------- 1 | package domain.entity 2 | 3 | case class Message(id: Long, message: String, userName: String) 4 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/entity/User.scala: -------------------------------------------------------------------------------- 1 | package domain.entity 2 | 3 | case class User(id: Long, name: String) 4 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/repository/MessageRepository.scala: -------------------------------------------------------------------------------- 1 | package domain.repository 2 | 3 | import domain.entity.Message 4 | import fujitask.Task 5 | import fujitask.ReadTransaction 6 | import fujitask.ReadWriteTransaction 7 | 8 | trait MessageRepository { 9 | 10 | def create(message: String, userName: String): Task[ReadWriteTransaction, Message] 11 | 12 | def read(id: Long): Task[ReadTransaction, Option[Message]] 13 | 14 | def readAll: Task[ReadTransaction, List[Message]] 15 | 16 | def update(user: Message): Task[ReadWriteTransaction, Unit] 17 | 18 | def delete(id: Long): Task[ReadWriteTransaction, Unit] 19 | 20 | } 21 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/repository/UserRepository.scala: -------------------------------------------------------------------------------- 1 | package domain.repository 2 | 3 | import fujitask.Task 4 | import fujitask.ReadTransaction 5 | import fujitask.ReadWriteTransaction 6 | import domain.entity.User 7 | 8 | trait UserRepository { 9 | 10 | def create(name: String): Task[ReadWriteTransaction, User] 11 | 12 | def read(id: Long): Task[ReadTransaction, Option[User]] 13 | 14 | def readAll: Task[ReadTransaction, List[User]] 15 | 16 | def update(user: User): Task[ReadWriteTransaction, Unit] 17 | 18 | def delete(id: Long): Task[ReadWriteTransaction, Unit] 19 | 20 | } 21 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/repository/scalikejdbc/DomainDB.scala: -------------------------------------------------------------------------------- 1 | package domain.repository.scalikejdbc 2 | 3 | import scalikejdbc._ 4 | import scalikejdbc.config._ 5 | 6 | object DomainDB { 7 | 8 | def setup() = DBs.setupAll() 9 | 10 | def close() = DBs.closeAll() 11 | 12 | val userDdl = sql""" 13 | create table if not exists `users` ( 14 | `id` bigint not null auto_increment, 15 | `name` varchar(64) not null 16 | ) 17 | """ 18 | 19 | val messageDdl = sql""" 20 | create table if not exists `messages` ( 21 | `id` bigint not null auto_increment, 22 | `message` varchar(256) not null, 23 | `user_name` varchar(64) not null 24 | ) 25 | """ 26 | 27 | def createTables() = DB localTx { implicit s => 28 | userDdl.execute.apply() 29 | messageDdl.execute.apply() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/repository/scalikejdbc/MessageRepositoryImpl.scala: -------------------------------------------------------------------------------- 1 | package domain.repository.scalikejdbc 2 | 3 | import domain.entity.Message 4 | import domain.repository.MessageRepository 5 | import fujitask.Task 6 | import fujitask.ReadTransaction 7 | import fujitask.ReadWriteTransaction 8 | import fujitask.scalikejdbc._ 9 | import scalikejdbc._ 10 | 11 | object MessageRepositoryImpl extends MessageRepository { 12 | 13 | def create(message: String, userName: String): Task[ReadWriteTransaction, Message] = 14 | ask.map { implicit session => 15 | val sql = sql"""insert into messages (user_name, message) values ($userName, $message)""" 16 | val id = sql.updateAndReturnGeneratedKey.apply() 17 | Message(id, userName, message) 18 | } 19 | 20 | def read(id: Long): Task[ReadTransaction, Option[Message]] = 21 | ask.map { implicit session => 22 | val sql = sql"""select * from messages where id = $id""" 23 | sql.map(rs => Message(rs.long("id"), rs.string("user_name"), rs.string("message"))).single.apply() 24 | } 25 | 26 | def readAll: Task[ReadTransaction, List[Message]] = 27 | ask.map { implicit session => 28 | val sql = sql"""select * from messages""" 29 | sql.map(rs => Message(rs.long("id"), rs.string("user_name"), rs.string("message"))).list.apply() 30 | } 31 | 32 | def update(message: Message): Task[ReadWriteTransaction, Unit] = 33 | ask.map { implicit session => 34 | val sql = sql"""update messages set user_name = ${message.userName}, message = ${message.message} where id = ${message.id}""" 35 | sql.update.apply() 36 | } 37 | 38 | def delete(id: Long): Task[ReadWriteTransaction, Unit] = 39 | ask.map { implicit session => 40 | val sql = sql"""delete messages where id = $id""" 41 | sql.update.apply() 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/repository/scalikejdbc/UserRepositoryImpl.scala: -------------------------------------------------------------------------------- 1 | package domain.repository.scalikejdbc 2 | 3 | import fujitask.Task 4 | import fujitask.ReadTransaction 5 | import fujitask.ReadWriteTransaction 6 | import fujitask.scalikejdbc._ 7 | import domain.entity.User 8 | import domain.repository.UserRepository 9 | import scalikejdbc._ 10 | 11 | object UserRepositoryImpl extends UserRepository { 12 | 13 | def create(name: String): Task[ReadWriteTransaction, User] = 14 | ask.map { implicit session => 15 | val sql = sql"""insert into users (name) values ($name)""" 16 | val id = sql.updateAndReturnGeneratedKey.apply() 17 | User(id, name) 18 | } 19 | 20 | def read(id: Long): Task[ReadTransaction, Option[User]] = 21 | ask.map { implicit session => 22 | val sql = sql"""select * from users where id = $id""" 23 | sql.map(rs => User(rs.long("id"), rs.string("name"))).single.apply() 24 | } 25 | 26 | def readAll: Task[ReadTransaction, List[User]] = 27 | ask.map { implicit session => 28 | val sql = sql"""select * from users""" 29 | sql.map(rs => User(rs.long("id"), rs.string("name"))).list.apply() 30 | } 31 | 32 | def update(user: User): Task[ReadWriteTransaction, Unit] = 33 | ask.map { implicit session => 34 | val sql = sql"""update users set name = ${user.name} where id = ${user.id}""" 35 | sql.update.apply() 36 | } 37 | 38 | def delete(id: Long): Task[ReadWriteTransaction, Unit] = 39 | ask.map { implicit session => 40 | val sql = sql"""delete users where id = $id""" 41 | sql.update.apply() 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/service/MessageService.scala: -------------------------------------------------------------------------------- 1 | package domain.service 2 | 3 | import domain.entity.Message 4 | import domain.repository.UserRepository 5 | import domain.repository.MessageRepository 6 | import domain.repository.scalikejdbc.UserRepositoryImpl 7 | import domain.repository.scalikejdbc.MessageRepositoryImpl 8 | import fujitask.scalikejdbc._ 9 | import scala.concurrent.Future 10 | 11 | object MessageService { 12 | 13 | val userRepository: UserRepository = UserRepositoryImpl 14 | 15 | val messageRepository: MessageRepository = MessageRepositoryImpl 16 | 17 | def create(message: String, userName: String): Future[Message] = 18 | messageRepository.create(message, userName).run() 19 | 20 | def read(id: Long): Future[Option[Message]] = 21 | messageRepository.read(id).run() 22 | 23 | def readAll: Future[List[Message]] = 24 | messageRepository.readAll.run() 25 | 26 | def update(message: Message): Future[Unit] = 27 | messageRepository.update(message).run() 28 | 29 | def delete(id: Long): Future[Unit] = 30 | messageRepository.delete(id).run() 31 | 32 | def createByUserId(message: String, userId: Long): Future[Message] = { 33 | val task = 34 | for { 35 | userOpt <- userRepository.read(userId) 36 | user = userOpt.getOrElse(throw new IllegalArgumentException("User Not Found")) 37 | message <- messageRepository.create(message, user.name) 38 | } yield message 39 | 40 | task.run() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /domain/src/main/scala/domain/service/UserService.scala: -------------------------------------------------------------------------------- 1 | package domain.service 2 | 3 | import domain.entity.User 4 | import domain.repository.UserRepository 5 | import domain.repository.scalikejdbc.UserRepositoryImpl 6 | import fujitask.scalikejdbc._ 7 | import scala.concurrent.Future 8 | 9 | object UserService { 10 | 11 | val userRepository: UserRepository = UserRepositoryImpl 12 | 13 | def create(name: String): Future[User] = 14 | userRepository.create(name).run() 15 | 16 | def read(id: Long): Future[Option[User]] = 17 | userRepository.read(id).run() 18 | 19 | def readAll: Future[List[User]] = 20 | userRepository.readAll.run() 21 | 22 | def update(user: User): Future[Unit] = 23 | userRepository.update(user).run() 24 | 25 | def delete(id: Long): Future[Unit] = 26 | userRepository.delete(id).run() 27 | 28 | def create3(name1: String, name2: String, name3: String): Future[(User, User, User)] = 29 | (for { 30 | user1 <- userRepository.create(name1) 31 | user2 <- userRepository.create(name2) 32 | user3 <- userRepository.create(name3) 33 | } yield (user1, user2, user3)).run() 34 | 35 | import scala.concurrent.ExecutionContext.Implicits.global 36 | 37 | def create3CommitEach(name1: String, name2: String, name3: String): Future[(User, User, User)] = 38 | for { 39 | user1 <- userRepository.create(name1).run() 40 | user2 <- userRepository.create(name2).run() 41 | user3 <- userRepository.create(name3).run() 42 | } yield (user1, user2, user3) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /fujitask-scalikejdbc/src/main/scala/fujitask/scalikejdbc/Transaction.scala: -------------------------------------------------------------------------------- 1 | package fujitask.scalikejdbc 2 | 3 | import fujitask.ReadTransaction 4 | import fujitask.ReadWriteTransaction 5 | import scalikejdbc._ 6 | 7 | abstract class ScalikeJDBCTransaction(val session: DBSession) 8 | 9 | class ScalikeJDBCReadTransaction(session: DBSession) extends ScalikeJDBCTransaction(session) with ReadTransaction 10 | 11 | class ScalikeJDBCReadWriteTransaction(session: DBSession) extends ScalikeJDBCTransaction(session) with ReadWriteTransaction 12 | -------------------------------------------------------------------------------- /fujitask-scalikejdbc/src/main/scala/fujitask/scalikejdbc/package.scala: -------------------------------------------------------------------------------- 1 | package fujitask 2 | 3 | import scala.concurrent.ExecutionContext 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import scala.concurrent.Future 6 | import _root_.scalikejdbc._ 7 | 8 | package object scalikejdbc { 9 | 10 | def ask: Task[Transaction, DBSession] = 11 | new Task[Transaction, DBSession] { 12 | def execute(transaction: Transaction)(implicit ec: ExecutionContext): Future[DBSession] = 13 | Future.successful(transaction.asInstanceOf[ScalikeJDBCTransaction].session) 14 | } 15 | 16 | implicit def readRunner[R >: ReadTransaction] : TaskRunner[R] = 17 | new TaskRunner[R] { 18 | def run[A](task: Task[R, A]): Future[A] = { 19 | println("ReadRunner") 20 | val session = DB.readOnlySession() 21 | val future = task.execute(new ScalikeJDBCReadTransaction(session)) 22 | future.onComplete(_ => session.close()) 23 | future 24 | } 25 | } 26 | 27 | implicit def readWriteRunner[R >: ReadWriteTransaction]: TaskRunner[R] = 28 | new TaskRunner[R] { 29 | def run[A](task: Task[R, A]): Future[A] = { 30 | println("ReadWriteRunner") 31 | DB.futureLocalTx(session => task.execute(new ScalikeJDBCReadWriteTransaction(session))) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /fujitask/src/main/scala/fujitask/Task.scala: -------------------------------------------------------------------------------- 1 | package fujitask 2 | 3 | import scala.concurrent.ExecutionContext 4 | import scala.concurrent.Future 5 | 6 | /** 7 | * 『PofEAA』の「Unit of Work」パターンの実装 8 | * 9 | * トランザクションとはストレージに対するまとまった処理である 10 | * トランザクションオブジェクトとはトランザクションを表現するオブジェクトで、 11 | * 具体的にはデータベースライブラリのセッションオブジェクトなどが該当する 12 | * 13 | * @tparam Resource トランザクションオブジェクトの型 14 | * @tparam A トランザクションを実行して得られる値の型 15 | */ 16 | trait Task[-Resource, +A] { lhs => 17 | /** 18 | * トランザクションの内部で実行される個々の処理の実装 19 | * このメソッドを実装することでTaskが作られる 20 | * 21 | * @param resource トランザクションオブジェクト 22 | * @param ec ExecutionContext 23 | * @return トランザクションの内部で実行される個々の処理で得られる値 24 | */ 25 | def execute(resource: Resource)(implicit ec: ExecutionContext): Future[A] 26 | 27 | /** 28 | * Taskモナドを合成する 29 | * その際、変位指定によりResourceの型は両方のTaskのResourceの共通のサブクラスの型になる 30 | * 31 | * @param f モナド関数 32 | * @tparam ExtendedResource トランザクションオブジェクトの型 33 | * @tparam B 合成されたTaskを実行すると得られる値の型 34 | * @return 合成されたTask 35 | */ 36 | def flatMap[ExtendedResource <: Resource, B](f: A => Task[ExtendedResource, B]): Task[ExtendedResource, B] = 37 | new Task[ExtendedResource, B] { 38 | def execute(resource: ExtendedResource)(implicit ec: ExecutionContext): Future[B] = 39 | lhs.execute(resource).map(f).flatMap(_.execute(resource)) 40 | } 41 | 42 | /** 43 | * 関数をTaskの結果に適用する 44 | * 45 | * @param f 適用したい関数 46 | * @tparam B 関数を適用して得られた値の型 47 | * @return 関数が適用されたTask 48 | */ 49 | def map[B](f: A => B): Task[Resource, B] = flatMap(a => Task(f(a))) 50 | 51 | /** 52 | * TaskRunnerを使ってTaskを実行する 53 | * implicitによりResourceに合ったTaskRunnerが選ばれる 54 | * 55 | * @param runner Taskを実行するためのTaskRunner 56 | * @tparam ExtendedResource トランザクションオブジェクトの型 57 | * @return 個々のTaskの処理の結果得られる値 58 | */ 59 | def run[ExtendedResource <: Resource]()(implicit runner: TaskRunner[ExtendedResource]): Future[A] = runner.run(this) 60 | } 61 | 62 | object Task { 63 | /** 64 | * Taskのデータコンストラクタ 65 | * 66 | * @param a Taskの値 67 | * @tparam Resource トランザクションオブジェクトの型 68 | * @tparam A Taskの値の型 69 | * @return 実行するとaの値を返すTask 70 | */ 71 | def apply[Resource, A](a: => A): Task[Resource, A] = 72 | new Task[Resource, A] { 73 | def execute(resource: Resource)(implicit executor: ExecutionContext): Future[A] = 74 | Future(a) 75 | } 76 | } 77 | 78 | /** 79 | * Taskを実行する 80 | * トランザクションオブジェクトの型ごとにインスタンスを作成すること 81 | * 82 | * @tparam Resource トランザクションオブジェクトの型 83 | */ 84 | trait TaskRunner[Resource] { 85 | /** 86 | * Taskを実行する 87 | * 88 | * @param task 実行するTask 89 | * @tparam A Task実行すると得られる値の型 90 | * @return Task実行して得られた値 91 | */ 92 | def run[A](task: Task[Resource, A]): Future[A] 93 | } 94 | -------------------------------------------------------------------------------- /fujitask/src/main/scala/fujitask/Transaction.scala: -------------------------------------------------------------------------------- 1 | package fujitask 2 | 3 | trait Transaction 4 | 5 | trait ReadTransaction extends Transaction 6 | 7 | trait ReadWriteTransaction extends ReadTransaction 8 | -------------------------------------------------------------------------------- /fujitask/src/test/scala/Helper.scala: -------------------------------------------------------------------------------- 1 | package fujitask 2 | 3 | import java.util.concurrent.{TimeUnit, Executor} 4 | 5 | import scala.concurrent.duration.Duration 6 | import scala.concurrent.{Await, ExecutionContext, Future} 7 | import scala.util.{Failure, Success, Try} 8 | import scalaprops._ 9 | import scalaz.Equal 10 | 11 | object Helper { 12 | type ATask[A] = Task[Int, A] 13 | 14 | private implicit val dummyExecutor = ExecutionContext.fromExecutor(new BlockingExecutor) 15 | 16 | class BlockingExecutor extends Executor { 17 | def execute(command: Runnable): Unit = { 18 | command.run() 19 | } 20 | } 21 | 22 | case class ErrorTask(value: Int) extends RuntimeException 23 | 24 | implicit def genErrorTask(implicit I: Gen[Int]): Gen[ErrorTask] = 25 | I.map(x => ErrorTask(x)) 26 | 27 | implicit def equalErrorTask(implicit I: Equal[Int]): Equal[ErrorTask] = 28 | I.contramap(_.value) 29 | 30 | implicit def equalFuture[A](implicit A: Equal[A], E: Equal[ErrorTask]): Equal[Future[A]] = 31 | Equal.equal( (a, b) => ( 32 | Try(Await.result(a, Duration(5, TimeUnit.SECONDS))), 33 | Try(Await.result(b, Duration(5, TimeUnit.SECONDS))) 34 | ) match { 35 | case (Success(va), Success(vb)) => A.equal(va, vb) 36 | case (Failure(ea@(ErrorTask(_))), Failure(eb@ErrorTask(_))) => E.equal(ea, eb) 37 | case _ => false 38 | } 39 | ) 40 | 41 | implicit def genTaskRunner[R](implicit R: Gen[R]): Gen[TaskRunner[R]] = 42 | R.map[TaskRunner[R]] { r => new TaskRunner[R] { 43 | def run[A](task: Task[R, A]): Future[A] = task.execute(r) 44 | } 45 | } 46 | 47 | implicit def equalTask[R, A](implicit F: Equal[Future[A]], R: Gen[TaskRunner[R]]): Equal[Task[R, A]] = 48 | F.contramap(_.run()(R.sample())) 49 | 50 | implicit def genTask[R, A](implicit A: Gen[R => A], E: Gen[ErrorTask]): Gen[Task[R, A]] = 51 | Gen.frequency( 52 | 1 -> E.map[Task[R, A]] {e => new Task[R, A] { 53 | def execute(res: R)(implicit executor: ExecutionContext): Future[A] = 54 | Future.failed(e) 55 | }}, 56 | 20 -> A.map[Task[R, A]] { f => new Task[R, A] { 57 | def execute(res: R)(implicit executor: ExecutionContext): Future[A] = 58 | Future.successful(f(res)) 59 | }} 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /fujitask/src/test/scala/TaskTest.scala: -------------------------------------------------------------------------------- 1 | package fujitask 2 | 3 | import scalaprops._ 4 | import scalaz._ 5 | import scalaz.std.anyVal._ 6 | import Helper._ 7 | 8 | object TaskTest extends Scalaprops { 9 | 10 | implicit def monadTask[R, A] = new Monad[({type L[B] = Task[R, B]})#L] { 11 | def point[B](a: => B): Task[R, B] = Task(a) 12 | def bind[B, C](a: Task[R, B])(f: B => Task[R, C]): Task[R, C] = a.flatMap(f) 13 | } 14 | 15 | type IntTask[A] = Task[Int, A] 16 | val intTaskMonadLawsTest = Properties.list( 17 | scalazlaws.monad.all[IntTask] 18 | ) 19 | 20 | type BooleanTask[A] = Task[Boolean, A] 21 | val booleanTaskMonadLawsTest = Properties.list( 22 | scalazlaws.monad.all[BooleanTask] 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | --------------------------------------------------------------------------------