├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── slicker-postgres └── src │ ├── test │ ├── scala │ │ └── io │ │ │ └── slicker │ │ │ └── postgres │ │ │ ├── models │ │ │ ├── User.scala │ │ │ ├── UsersRepository.scala │ │ │ └── UserTable.scala │ │ │ ├── AwaitHelper.scala │ │ │ ├── package.scala │ │ │ └── PostgreSQLRepositorySpec.scala │ └── resources │ │ ├── application.conf │ │ └── logback.xml │ └── main │ └── scala │ └── io │ └── slicker │ └── postgres │ ├── TableWithId.scala │ ├── SimpleRecordTable.scala │ ├── PostgresDriver.scala │ ├── RecordTable.scala │ └── PostgreSQLRepository.scala ├── dev └── docker-compose.yml ├── .gitignore ├── .travis.yml ├── slicker-core └── src │ └── main │ └── scala │ └── io │ └── slicker │ └── core │ ├── Entity.scala │ ├── generic │ └── EntityGen.scala │ ├── PageRequest.scala │ ├── Repository.scala │ └── sort │ ├── gen │ └── SortMacros.scala │ └── Sort.scala ├── docs ├── slickpg.md ├── entity.md ├── index.md └── pagerequest.md ├── slicker-monad └── src │ └── main │ └── scala │ └── io │ └── slicker │ └── instances │ └── EitherT.scala ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.3" 2 | -------------------------------------------------------------------------------- /slicker-postgres/src/test/scala/io/slicker/postgres/models/User.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres.models 2 | 3 | case class User(id: Option[Long], name: String) -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | postgres: 4 | image: postgres:9.6 5 | container_name: 'slick-postgres' 6 | ports: 7 | - '5432:5432' 8 | environment: 9 | - POSTGRES_PASSWORD=test 10 | - POSTGRES_USER=test -------------------------------------------------------------------------------- /slicker-postgres/src/main/scala/io/slicker/postgres/TableWithId.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import io.slicker.postgres.PostgresDriver.api._ 4 | 5 | abstract class TableWithId[Id, R](tag: Tag, tableName: String) extends Table[R](tag, tableName) { 6 | def id: Rep[Id] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | #Idea specific 20 | .idea 21 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | Classpaths.typesafeReleases, 3 | Classpaths.sbtPluginReleases, 4 | Resolver.sonatypeRepo("snapshots") 5 | ) 6 | 7 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3") 8 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 9 | addSbtPlugin("org.scalastyle" % "scalastyle-sbt-plugin" % "0.8.0") -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - oraclejdk8 3 | 4 | language: scala 5 | 6 | scala: 7 | - 2.11.8 8 | - 2.12.1 9 | 10 | services: 11 | - postgresql 12 | 13 | addons: 14 | postgresql: "9.4" 15 | 16 | before_script: 17 | - psql -c "create database test;" -U postgres 18 | - psql -c "create user test with password 'test'; grant all privileges on database test to test;" -U postgres 19 | -------------------------------------------------------------------------------- /slicker-postgres/src/test/scala/io/slicker/postgres/AwaitHelper.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import scala.concurrent.duration.Duration 4 | import scala.concurrent.{Await, Future} 5 | 6 | trait AwaitHelper { 7 | 8 | implicit class AwaitFuture[A](f: Future[A]) { 9 | 10 | def await(implicit timeout: Duration): A = Await.result[A](f, timeout) 11 | 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /slicker-postgres/src/test/scala/io/slicker/postgres/models/UsersRepository.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres.models 2 | 3 | import io.slicker.postgres.PostgresDriver.api._ 4 | import io.slicker.postgres.PostgreSQLRepository 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | class UsersRepository extends PostgreSQLRepository(new UserTable){ 9 | 10 | override protected implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global 11 | 12 | } 13 | -------------------------------------------------------------------------------- /slicker-postgres/src/test/scala/io/slicker/postgres/package.scala: -------------------------------------------------------------------------------- 1 | package io.slicker 2 | 3 | import io.slicker.postgres.PostgresDriver.api._ 4 | 5 | import scala.concurrent.Future 6 | 7 | package object postgres { 8 | 9 | private val db = Database.forConfig("db") 10 | 11 | implicit class RunDBIO[R, S <: NoStream, E <: Effect](dBIO: DBIOAction[R, S, E]) { 12 | 13 | def run: Future[R] = db.run(dBIO) 14 | 15 | } 16 | 17 | def closeConnection(): Unit = db.close() 18 | 19 | } 20 | -------------------------------------------------------------------------------- /slicker-postgres/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | db { 2 | dataSourceClass: "org.postgresql.ds.PGSimpleDataSource" 3 | properties: { 4 | serverName: localhost 5 | serverName: ${?POSTGRES_SERVERNAME} 6 | portNumber: 5432 7 | portNumber: ${?POSTGRES_PORT} 8 | databaseName: test 9 | databaseName: ${?POSTGRES_DATABASE} 10 | user: test 11 | user: ${?POSTGRES_USER} 12 | password: test 13 | password: ${?POSTGRES_PASSWORD} 14 | } 15 | numThreads: 10 16 | queueSize: 500 17 | } -------------------------------------------------------------------------------- /slicker-core/src/main/scala/io/slicker/core/Entity.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.core 2 | 3 | import io.slicker.core.generic.EntityGen 4 | 5 | /** 6 | * Basic trait for every entity that should be used with repository 7 | * 8 | * @tparam Id Type of ID parameter 9 | */ 10 | trait Entity[E, Id] { 11 | def id(e: E): Option[Id] 12 | } 13 | 14 | object Entity { 15 | 16 | /** 17 | * Automatically derive [[Entity]] type class if class `E` has `id: Option[A]` field. 18 | */ 19 | implicit def deriveEntity[E, Id](implicit entityGen: EntityGen.Aux[E, Id]): Entity[E, Id] = entityGen() 20 | 21 | } -------------------------------------------------------------------------------- /slicker-postgres/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/slickpg.md: -------------------------------------------------------------------------------- 1 | Slick-pg 2 | ======= 3 | 4 | __Slicker-postgres__ module is distributed with [Slick-pg](https://github.com/tminglei/slick-pg) driver 5 | that provides support for a PostgreSQL specific data types, operations and functions. 6 | 7 | To use out-of-the-box implementation of driver just add following line: 8 | ```scala 9 | import io.slicker.postgres.PostgresDriver.api._ 10 | ``` 11 | 12 | But it's also possible to use own driver with Slicker. 13 | More info could be found in [official repository](https://github.com/tminglei/slick-pg). 14 | 15 | More 16 | ====== 17 | 18 | * [Quick start](index.md) 19 | * [Entity ID](entity.md) 20 | * [Pagination and sorting](pagerequest.md) -------------------------------------------------------------------------------- /slicker-postgres/src/main/scala/io/slicker/postgres/SimpleRecordTable.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import io.slicker.postgres.PostgresDriver.api._ 4 | 5 | /** 6 | * Basic trait for records entities 7 | * 8 | * @tparam Id Type of ID parameter of entity 9 | * @tparam Business Type of entity 10 | * @tparam T Type of slick table 11 | */ 12 | trait SimpleRecordTable[Id, Business, T <: TableWithId[Id, Business]] extends RecordTable[Id, Business, Business, T] { 13 | 14 | /** 15 | * Instance of slick table query 16 | */ 17 | val tableQuery: TableQuery[T] 18 | 19 | /** 20 | * Conversion from business entity to database 21 | */ 22 | def toDatabase(business: Business): Business = business 23 | 24 | /** 25 | * Conversion from database entity to business 26 | */ 27 | def toBusiness(database: Business): Business = database 28 | 29 | } 30 | -------------------------------------------------------------------------------- /slicker-postgres/src/test/scala/io/slicker/postgres/models/UserTable.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres.models 2 | 3 | import io.slicker.core.sort.Sort 4 | import io.slicker.postgres.PostgresDriver.api._ 5 | import io.slicker.postgres.{SimpleRecordTable, TableWithId} 6 | import slick.lifted.ProvenShape 7 | 8 | import scala.language.experimental.macros 9 | 10 | class UserTable extends SimpleRecordTable[Long, User, Users] { 11 | 12 | override val tableQuery: TableQuery[Users] = TableQuery[Users] 13 | 14 | override def sort: Sort[Users] = Sort.auto[Users] 15 | 16 | } 17 | 18 | class Users(tag: Tag) extends TableWithId[Long, User](tag, "users") { 19 | 20 | override def id: Rep[Long] = column[Long]("id", O.AutoInc, O.PrimaryKey) 21 | 22 | def name: Rep[String] = column[String]("name") 23 | 24 | override def * : ProvenShape[User] = (id.?, name) <> (User.tupled, User.unapply) 25 | 26 | } -------------------------------------------------------------------------------- /slicker-postgres/src/main/scala/io/slicker/postgres/PostgresDriver.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import com.github.tminglei.slickpg._ 4 | 5 | trait PostgresDriver extends ExPostgresProfile 6 | with PgArraySupport 7 | with PgDate2Support 8 | with PgRangeSupport 9 | with PgHStoreSupport 10 | with PgSearchSupport 11 | with PgNetSupport 12 | with PgEnumSupport 13 | with PgLTreeSupport 14 | with PgCompositeSupport { 15 | 16 | override val api = new SlickAPI {} 17 | 18 | trait SlickAPI extends API with ArrayImplicits 19 | with DateTimeImplicits 20 | with NetImplicits 21 | with LTreeImplicits 22 | with RangeImplicits 23 | with HStoreImplicits 24 | with SearchImplicits 25 | with SearchAssistants { 26 | implicit val strListTypeMapper = new SimpleArrayJdbcType[String]("text").to(_.toList) 27 | } 28 | 29 | } 30 | 31 | object PostgresDriver extends PostgresDriver -------------------------------------------------------------------------------- /slicker-core/src/main/scala/io/slicker/core/generic/EntityGen.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.core.generic 2 | 3 | import io.slicker.core.Entity 4 | import shapeless.ops.record.Selector 5 | import shapeless.{DepFn0, HList, LabelledGeneric, Witness} 6 | 7 | trait EntityGen[E] extends DepFn0 { 8 | 9 | type Id 10 | type Out = Entity[E, Id] 11 | 12 | } 13 | 14 | object EntityGen { 15 | 16 | type Aux[E, I] = EntityGen[E] { 17 | type Id = I 18 | } 19 | 20 | implicit def mkEntity[E, I, H <: HList](implicit 21 | gen: LabelledGeneric.Aux[E, H], 22 | selector: Selector.Aux[H, Witness.`'id`.T, Option[I]]): EntityGen.Aux[E, I] = new EntityGen[E] { 23 | type Id = I 24 | 25 | def apply(): Out = { 26 | new Entity[E, Id] { 27 | override def id(e: E): Option[Id] = { 28 | val repr = gen.to(e) 29 | selector(repr) 30 | } 31 | } 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /slicker-postgres/src/main/scala/io/slicker/postgres/RecordTable.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import io.slicker.core.sort.Sort 4 | import io.slicker.postgres.PostgresDriver.api._ 5 | 6 | /** 7 | * Basic trait for records entities 8 | * 9 | * @tparam Id Type of ID parameter of entity 10 | * @tparam Business Type of business entity 11 | * @tparam Database Type of record entity 12 | * @tparam T Type of slick table 13 | */ 14 | trait RecordTable[Id, Business, Database, T <: TableWithId[Id, Database]] { 15 | 16 | /** 17 | * Instance of slick table query 18 | */ 19 | val tableQuery: TableQuery[T] 20 | 21 | /** 22 | * Conversion from business entity to database 23 | */ 24 | def toDatabase(business: Business): Database 25 | 26 | /** 27 | * Conversion from database entity to business 28 | */ 29 | def toBusiness(database: Database): Business 30 | 31 | /** 32 | * [[Sort]] object that will return [[slick.lifted.Ordered]] 33 | * for some input string as field name to sort by 34 | */ 35 | def sort: Sort[T] = Sort.empty[T] 36 | 37 | } 38 | -------------------------------------------------------------------------------- /docs/entity.md: -------------------------------------------------------------------------------- 1 | Entity ID 2 | ====== 3 | 4 | To provide methods such as `findById`, `save`, `remove` and so on, Slicker 5 | has to determine `id` field of model that used in repository. 6 | 7 | 8 | There is a type-class `io.slick.core.Entity` that should return 9 | for some type `E` its `id` field value of type `Option[Id]`. 10 | 11 | ```scala 12 | trait Entity[E, Id] { 13 | def id(e: E): Option[Id] 14 | } 15 | ``` 16 | 17 | Basically, for each model with field named `id` of type `Option[A]` Slicker is able 18 | to derive this type class automatically using Shapeless library. 19 | So there is no need to inherit some trait or use annotations. 20 | 21 | But in case if there is no such optional field or `id` field has another name, it's 22 | possible to implement `Entity` type-class manually with companion object and implicit value: 23 | 24 | ```scala 25 | 26 | case class Catalog(cid: Long, name: String) 27 | 28 | object Catalog { 29 | 30 | implicit val entity: Entity[Catalog, Long] = { 31 | new Entity[Catalog, Long] { 32 | def id(e: Catalog): Option[Long] = Some(catalog.cid) 33 | } 34 | } 35 | 36 | } 37 | ``` 38 | 39 | Compiler will automatically resolve this `Entity` instance to use with repository. 40 | 41 | More 42 | ====== 43 | 44 | * [Quick start](index.md) 45 | * [Pagination and sorting](pagerequest.md) 46 | * [Slick-pg Driver](slickpg.md) -------------------------------------------------------------------------------- /slicker-core/src/main/scala/io/slicker/core/PageRequest.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.core 2 | 3 | /** 4 | * Page request that allows to set limit/offset values for a request in terms of pagination 5 | * 6 | * @param page Page number. Should always be equal or greater than 1 7 | * @param perPage Number of entities to get from request. Should always be equal or greater than 0 8 | * @param sort Fields to sort by 9 | */ 10 | case class PageRequest(page: Int, perPage: Int, sort: Seq[(String, SortDirection)] = Seq.empty) { 11 | 12 | /** 13 | * Offset value for SQL queries 14 | */ 15 | def offset: Int = (page - 1) * perPage 16 | 17 | /** 18 | * Limit value for SQL queries 19 | */ 20 | def limit: Int = perPage 21 | 22 | } 23 | 24 | object PageRequest { 25 | 26 | /** 27 | * Requesting all pages 28 | */ 29 | val ALL = new PageRequest(1, Int.MaxValue) 30 | 31 | /** 32 | * Requesting first page 33 | */ 34 | val FIRSTPAGE = new PageRequest(1, 10) 35 | 36 | def apply(sort: Seq[(String, SortDirection)]): PageRequest = PageRequest(1, Int.MaxValue, sort) 37 | 38 | } 39 | 40 | sealed abstract class SortDirection(val name: String) { 41 | 42 | def isAsc: Boolean = this match { 43 | case SortDirection.Asc => true 44 | case SortDirection.Desc => false 45 | } 46 | 47 | def isDesc: Boolean = !isAsc 48 | 49 | } 50 | 51 | object SortDirection { 52 | 53 | case object Asc extends SortDirection("asc") 54 | 55 | case object Desc extends SortDirection("desc") 56 | 57 | def apply(name: String): SortDirection = name match { 58 | case Asc.name => Asc 59 | case Desc.name => Desc 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /slicker-monad/src/main/scala/io/slicker/instances/EitherT.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.instances 2 | 3 | import slick.dbio.{DBIOAction, Effect, NoStream} 4 | 5 | import scala.concurrent.ExecutionContext 6 | 7 | class EitherT[A, B, E <: Effect](val value: DBIOAction[Either[A, B], NoStream, E]) { 8 | 9 | def map[C](f: B => C)(implicit 10 | ec: ExecutionContext): EitherT[A, C, E] = new EitherT(value.map({ 11 | case Right(b) => Right(f(b)) 12 | case Left(a) => Left(a) 13 | })) 14 | 15 | def flatMap[C, F <: Effect](f: B => EitherT[A, C, F])(implicit 16 | ec: ExecutionContext): EitherT[A, C, E with F] = { 17 | new EitherT(value.flatMap { 18 | case Right(b) => f(b).value 19 | case Left(a) => DBIOAction.successful(Left(a)) 20 | }) 21 | } 22 | 23 | def flatMapF[C, F <: Effect](f: B => DBIOAction[Either[A, C], NoStream, F])(implicit 24 | ec: ExecutionContext): EitherT[A, C, E with F] = { 25 | new EitherT(value.flatMap { 26 | case Right(b) => f(b) 27 | case Left(a) => DBIOAction.successful(Left(a)) 28 | }) 29 | } 30 | 31 | def ensure(onFailure: => A)(predicate: B => Boolean)(implicit 32 | ec: ExecutionContext): EitherT[A, B, E] = { 33 | new EitherT(value.map { 34 | case Right(b) if predicate(b) => Right(b) 35 | case _ => Left(onFailure) 36 | }) 37 | } 38 | 39 | def assure(onFailure: B => A)(predicate: B => Boolean)(implicit 40 | ec: ExecutionContext): EitherT[A, B, E] = { 41 | new EitherT(value.map { 42 | case Right(b) if predicate(b) => Right(b) 43 | case Right(b) => Left(onFailure(b)) 44 | case Left(a) => Left(a) 45 | }) 46 | } 47 | 48 | def leftMap[D](f: A => D)(implicit 49 | ec: ExecutionContext): EitherT[D, B, E] = new EitherT(value.map { 50 | case Right(b) => Right(b) 51 | case Left(a) => Left(f(a)) 52 | }) 53 | 54 | } 55 | 56 | object EitherT { 57 | def apply[A, B, E <: Effect](dBIOAction: DBIOAction[Either[A, B], NoStream, E]) = new EitherT(dBIOAction) 58 | } -------------------------------------------------------------------------------- /slicker-core/src/main/scala/io/slicker/core/Repository.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.core 2 | 3 | import slick.dbio._ 4 | 5 | /** 6 | * Public interface for each repository implementation 7 | * 8 | * @tparam Id Type of ID parameter 9 | * @tparam E Type of entity class 10 | */ 11 | trait Repository[Id, E] { 12 | 13 | type WriteAction[A] = DBIOAction[A, NoStream, Effect.Write] 14 | 15 | type ReadAction[A] = DBIOAction[A, NoStream, Effect.Read] 16 | 17 | /** 18 | * Insert or updates entity in DB 19 | * 20 | * @param e entity 21 | * @return inserted entity with new ID or updated entity 22 | */ 23 | def save(e: E): WriteAction[E] 24 | 25 | /** 26 | * Insert or updates entities in DB 27 | * 28 | * @param es entities 29 | * @return inserted or updated entities 30 | */ 31 | def save(es: Seq[E]): WriteAction[Seq[E]] 32 | 33 | /** 34 | * Find all entities 35 | * 36 | * @param pageRequest request could be limited with offset/limit 37 | * @return All entities in table in respect with pageRequest parameter 38 | */ 39 | def findAll(pageRequest: PageRequest = PageRequest.ALL): ReadAction[Seq[E]] 40 | 41 | /** 42 | * Find entity by id 43 | * 44 | * @param id 45 | * @return Option with entity. None in case if there is no such entity. 46 | */ 47 | def findById(id: Id): ReadAction[Option[E]] 48 | 49 | /** 50 | * Remove entity by id 51 | * 52 | * @param id 53 | * @return True in case if entity was removed 54 | */ 55 | def removeById(id: Id): WriteAction[Boolean] 56 | 57 | /** 58 | * Remove entity 59 | * 60 | * @param e 61 | * @return True in case if entity was removed 62 | */ 63 | def remove(e: E): WriteAction[Boolean] 64 | 65 | /** 66 | * Remove entities 67 | * 68 | * @param es 69 | * @return True in case if at least one of entities was removed 70 | */ 71 | def remove(es: Seq[E]): WriteAction[Boolean] 72 | 73 | /** 74 | * Remove all entities in table 75 | * @return True in case if at least one of entities was removed 76 | */ 77 | def removeAll(): WriteAction[Boolean] 78 | 79 | /** 80 | * Count all rows in table 81 | * 82 | * @return Number of rows 83 | */ 84 | def countAll: ReadAction[Int] 85 | 86 | } 87 | -------------------------------------------------------------------------------- /slicker-core/src/main/scala/io/slicker/core/sort/gen/SortMacros.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.core.sort.gen 2 | 3 | import io.slicker.core.sort.Sort 4 | import slick.lifted.Rep 5 | 6 | import scala.reflect.macros.blackbox.Context 7 | 8 | object SortMacros { 9 | 10 | /** 11 | * Build [[io.slicker.core.Fields]] for all fields in table `T` 12 | */ 13 | def fullImpl[T: c.WeakTypeTag](c: Context): c.Expr[Sort[T]] = { 14 | 15 | import c.universe._ 16 | 17 | val T = weakTypeOf[T] 18 | val rep = weakTypeOf[Rep[_]] 19 | val terms = T.decls.filter({ 20 | case m if m.isMethod => 21 | val method = m.asMethod 22 | method.isPublic && !method.isConstructor && method.returnType <:< rep 23 | case v if !v.isMethod => 24 | v.isPublic && v.typeSignature <:< rep 25 | }).map(_.asTerm).toSeq 26 | 27 | val cases = terms.map({ method => 28 | val term = q"table.${method.asTerm.name}" 29 | val ordered = q"if(sortDirection.isAsc) Some($term.asc) else Some($term.desc)" 30 | val name = method.name.decodedName.toString 31 | cq"""$name => $ordered""" 32 | }) ++ Seq(cq"_ => Option.empty[slick.lifted.Ordered]") 33 | 34 | c.Expr[Sort[T]] { 35 | q""" 36 | new io.slicker.core.sort.Sort[$T] { 37 | protected def ordered(table: $T, field: String, sortDirection: io.slicker.core.SortDirection): Option[slick.lifted.Ordered] = { 38 | field match { 39 | case ..$cases 40 | } 41 | } 42 | } 43 | """ 44 | } 45 | 46 | } 47 | 48 | /** 49 | * Build [[Sort]] only for fields in `Seq[_]` 50 | */ 51 | def partialImpl[T: c.WeakTypeTag](c: Context)(f: c.Expr[T => Seq[_]]): c.Expr[Sort[T]] = { 52 | 53 | import c.universe._ 54 | 55 | val T = weakTypeOf[T] 56 | val function = q"$f" 57 | val arg = function.children.head.asInstanceOf[ValDef] 58 | val argName = arg.name 59 | val functionResult = function.children.last.asInstanceOf[Apply] 60 | val cases = functionResult.args.collect { 61 | case Select(Ident(prefix), method: TermName) if prefix == argName => 62 | val term = q"table.$method" 63 | val ordered = q"if(sortDirection.isAsc) Some($term.asc) else Some($term.desc)" 64 | val name = method.decodedName.toString 65 | cq"$name => $ordered" 66 | } ++ Seq(cq"_ => Option.empty[slick.lifted.Ordered]") 67 | 68 | c.Expr[Sort[T]] { 69 | q""" 70 | new io.slicker.core.sort.Sort[$T] { 71 | protected def ordered(table: $T, field: String, sortDirection: io.slicker.core.SortDirection): Option[slick.lifted.Ordered] = { 72 | field match { 73 | case ..$cases 74 | } 75 | } 76 | } 77 | """ 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Slicker 2 | ====== 3 | 4 | Slicker allows to abstract over well-known [Slick](http://slick.lightbend.com/) library to decrease the 5 | number of boilerplate using repository pattern and standart out-of-the-box 6 | CRUD operations when dealing with SQL databases. 7 | 8 | Quick start 9 | ====== 10 | 11 | First add following lines to your build.sbt: 12 | ``` 13 | libraryDependencies ++= Seq( 14 | "com.github.imliar" %% "slicker-core" % "0.3", 15 | "com.github.imliar" %% "slicker-postgres" % "0.3" 16 | ) 17 | ``` 18 | 19 | Then define table and entity that we're going to use: 20 | 21 | ```scala 22 | import io.slicker.postgres.PostgresDriver.api._ 23 | import io.slicker.postgres.{SimpleRecordTable, TableWithId} 24 | import io.slicker.core.sort.Sort 25 | import slick.lifted.ProvenShape 26 | 27 | case class User(id: Option[Long], name: String, email: String) 28 | 29 | class UserTable extends SimpleRecordTable[Long, User, Users] { 30 | 31 | override val tableQuery: TableQuery[Users] = TableQuery[Users] 32 | 33 | override val sort: Sort[Users] = Sort.auto[Users] 34 | 35 | } 36 | 37 | class Users(tag: Tag) extends TableWithId[Long, User](tag, "users") { 38 | 39 | override def id: Rep[Long] = column[Long]("id", O.AutoInc, O.PrimaryKey) 40 | 41 | def name: Rep[String] = column[String]("name") 42 | 43 | def email: Rep[String] = column[String]("email") 44 | 45 | override def * : ProvenShape[User] = (id.?, name, email) <> (User.tupled, User.unapply) 46 | 47 | } 48 | ``` 49 | 50 | Now define repository interface as a next step. It's not required but just a good practice 51 | to split intention and implementation. 52 | 53 | ```scala 54 | import io.slicker.core.Repository 55 | import io.slicker.PageRequest 56 | 57 | trait UsersRepository extends Repository[Long, User] { 58 | 59 | def findAllByName(name: String, pageRequest: PageRequest): ReadAction[Seq[User]] 60 | 61 | def findOneByEmail(email: String): ReadAction[Option[User]] 62 | 63 | } 64 | ``` 65 | 66 | Finally implement that interface 67 | 68 | ```scala 69 | import io.slicker.postgres.PostgresDriver.api._ 70 | import io.slicker.postgres.PostgreSQLRepository 71 | 72 | import scala.concurrent.ExecutionContext 73 | 74 | class UsersRepositoryImpl extends PostgreSQLRepository(new UserTable) with UsersRepository { 75 | 76 | //just an example. you could provide your own EC 77 | override protected implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global 78 | 79 | override def findAllByName(name: String, pageRequest: PageRequest): ReadAction[Seq[User]] = findAllBy((_.name === name), pageRequest) 80 | 81 | override def findOneByEmail(email: String): ReadAction[Option[User]] = findOneBy(_.email === email) 82 | 83 | } 84 | ``` 85 | 86 | Repository itself __returns DBIOAction__ so it's up to user to execute this action using Slick `Database`. 87 | It's also possible to compose multiple actions together and execute them in a single transaction i.e. 88 | More documentation about Slick could be found on [official website](http://slick.lightbend.com/doc/3.1.1/gettingstarted.html#querying). 89 | 90 | More 91 | ====== 92 | 93 | * [Entity ID](entity.md) 94 | * [Pagination and sorting](pagerequest.md) 95 | * [Slick-pg Driver](slickpg.md) -------------------------------------------------------------------------------- /docs/pagerequest.md: -------------------------------------------------------------------------------- 1 | Pagination and sorting 2 | ====== 3 | 4 | Slicker provides pagination (limit and offset) and sorting with `io.slicker.core.PageRequest` class as 5 | part of Repository interface. 6 | 7 | ## Pagination 8 | 9 | Public method `findAll` and protected method `findAllBy` take `PageRequest` 10 | as an additional optional argument equal to `PageRequest.ALL` by default. 11 | 12 | `PageRequest` class has following signature: 13 | 14 | ```scala 15 | case class PageRequest(page: Int, perPage: Int, sort: Seq[(String, SortDirection)] = Seq.empty) 16 | ``` 17 | 18 | So it's sufficient to pass current page (starting from __1__) and number of items per page. 19 | In addition there are additional values in companion object: 20 | 21 | ```scala 22 | object PageRequest { 23 | 24 | /** 25 | * Requesting all pages 26 | */ 27 | val ALL = new PageRequest(1, Int.MaxValue) 28 | 29 | /** 30 | * Requesting first page 31 | */ 32 | val FIRSTPAGE = new PageRequest(1, 10) 33 | 34 | } 35 | ``` 36 | 37 | ## Sorting 38 | 39 | Beside pagination support `PageRequest` also provides sorting ability 40 | using `sort` argument of type `Seq[(String, SortDirection)]`, where 41 | sort direction enum could be `Asc` or `Desc`. 42 | 43 | `RecordTable` defines `def sort: Sort` method that should return `Sort` object 44 | for current table. By default it's always empty, so sorting wouldn't work without 45 | overriding this method. This behaviour is based on assumption that it could be dangerous 46 | to provide full sorting to user without indices on corresponding SQL table because 47 | of possible performance issues. 48 | 49 | In case if there is a need in sorting using `PageRequest`, first it's required to 50 | override this method in descendant class: 51 | 52 | ```scala 53 | class UserTable extends SimpleRecordTable[Long, User, Users] { 54 | 55 | override val tableQuery: TableQuery[Users] = TableQuery[Users] 56 | 57 | override def sort: Sort[Users] = Sort.auto[Users] 58 | 59 | } 60 | 61 | class Users(tag: Tag) extends TableWithId[Long, User](tag, "users") { 62 | 63 | override def id: Rep[Long] = column[Long]("user_id", O.AutoInc, O.PrimaryKey) 64 | 65 | def name: Rep[String] = column[String]("user_name") 66 | 67 | override def * : ProvenShape[User] = (id.?, name) <> (User.tupled, User.unapply) 68 | 69 | } 70 | ``` 71 | 72 | ### Autosorting 73 | 74 | `Sort.auto[Users]` generates `Sort` object using macros. It builds following pattern matching: 75 | ``` 76 | field match { 77 | case "id" => table.id 78 | case "name" => table.name 79 | } 80 | ``` 81 | So inside of `sort` collection of `PageRequest` should be a key with "id" or "name". 82 | Basically, every "key" should be a __name__ of method of column definition in `Table`. It's not a 83 | database column name, but method name of `Users` class. It's fair only for auto/semiauto cases. 84 | Macros builds cases for __all__ columns. 85 | 86 | ### Semiauto sorting 87 | 88 | ```scala 89 | override def sort: Sort[Users] = Sort.semiauto[Users](users => Seq(users.id)) 90 | ``` 91 | 92 | `Sort.semiauto` generates `Sort` object using macros and list of provided columns. 93 | But in this case it builds cases only for columns in returning list. 94 | 95 | ### Manual sorting 96 | 97 | ```scala 98 | override def sort: Sort[Users] = Sort.manual[Users](users => { 99 | case "user_id" => users.id 100 | case "user_name" => users.name 101 | }) 102 | ``` 103 | 104 | This time no macros involved and it's also possible to define own keys for sorting, but requires 105 | more code to write. 106 | 107 | More 108 | ====== 109 | 110 | * [Quick start](index.md) 111 | * [Entity ID](entity.md) 112 | * [Slick-pg Driver](slickpg.md) -------------------------------------------------------------------------------- /slicker-postgres/src/test/scala/io/slicker/postgres/PostgreSQLRepositorySpec.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import io.slicker.core.{PageRequest, SortDirection} 4 | import io.slicker.postgres.PostgresDriver.api._ 5 | import io.slicker.postgres.models.{User, UserTable, UsersRepository} 6 | import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, FlatSpec, Matchers} 7 | 8 | import scala.concurrent.duration._ 9 | 10 | class PostgreSQLRepositorySpec extends FlatSpec with Matchers with AwaitHelper with BeforeAndAfter with BeforeAndAfterAll { 11 | 12 | private val userTable = new UserTable 13 | private val userRepo = new UsersRepository 14 | private implicit val timeout = 10.seconds 15 | 16 | override def afterAll(): Unit = { 17 | closeConnection() 18 | } 19 | 20 | before { 21 | userTable.tableQuery.schema.create.run.await 22 | } 23 | 24 | after { 25 | userTable.tableQuery.schema.drop.run.await 26 | } 27 | 28 | it should "return no rows for empty table" in { 29 | userRepo.findAll().run.await shouldBe Seq.empty[User] 30 | } 31 | 32 | it should "return zero as count result for empty table" in { 33 | userRepo.countAll.run.await shouldBe 0 34 | } 35 | 36 | it should "save entity, return ID and find it by ID" in { 37 | val user = userRepo.save(User(None, "name")).run.await 38 | userRepo.findById(user.id.get).run.await.get shouldBe user 39 | } 40 | 41 | it should "save multiple entities, return IDs and find them by ids" in { 42 | val users = userRepo.save(Seq(User(None, "name"), User(None, "name2"))).run.await 43 | users.flatMap(_.id).flatMap(userRepo.findById(_).run.await) shouldBe users 44 | } 45 | 46 | it should "save multiple entities and find them in descending order" in { 47 | val users = userRepo.save(Seq(User(None, "name"), User(None, "name2"))).run.await 48 | val sort = Seq("name" -> SortDirection.Desc) 49 | userRepo.findAll(PageRequest(sort)).run.await shouldBe users.sortBy(_.name).reverse 50 | } 51 | 52 | it should "update saved entity" in { 53 | val user = userRepo.save(User(None, "name")).run.await 54 | val updatedUser = userRepo.save(user.copy(name = "name2")).run.await 55 | updatedUser shouldBe user.copy(name = "name2") 56 | updatedUser shouldBe userRepo.findById(user.id.get).run.await.get 57 | } 58 | 59 | it should "update only single entity" in { 60 | val user = userRepo.save(User(None, "name")).run.await 61 | val user2 = userRepo.save(User(None, "name2")).run.await 62 | val updatedUser = userRepo.save(user.copy(name = "name3")).run.await 63 | 64 | updatedUser shouldBe user.copy(name = "name3") 65 | updatedUser shouldBe userRepo.findById(user.id.get).run.await.get 66 | 67 | user2 shouldBe userRepo.findById(user2.id.get).run.await.get 68 | } 69 | 70 | it should "remove entity by id" in { 71 | val user = userRepo.save(User(None, "name")).run.await 72 | userRepo.removeById(user.id.get).run.await 73 | 74 | userRepo.findById(user.id.get).run.await shouldBe None 75 | } 76 | 77 | it should "remove entity" in { 78 | val user = userRepo.save(User(None, "name")).run.await 79 | userRepo.remove(user).run.await 80 | 81 | userRepo.findById(user.id.get).run.await shouldBe None 82 | } 83 | 84 | it should "remove only single entity" in { 85 | val user = userRepo.save(User(None, "name")).run.await 86 | val user2 = userRepo.save(User(None, "name2")).run.await 87 | userRepo.remove(user).run.await 88 | 89 | user2 shouldBe userRepo.findById(user2.id.get).run.await.get 90 | } 91 | 92 | it should "remove all entities" in { 93 | userRepo.save(User(None, "name")).run.await 94 | userRepo.removeAll().run.await 95 | 96 | userRepo.countAll.run.await shouldBe 0 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /slicker-core/src/main/scala/io/slicker/core/sort/Sort.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.core.sort 2 | 3 | import io.slicker.core.SortDirection 4 | import io.slicker.core.sort.gen.SortMacros 5 | import slick.ast.Ordering 6 | import slick.lifted.Ordered 7 | 8 | import scala.language.experimental.macros 9 | import scala.language.implicitConversions 10 | 11 | /** 12 | * For some table `T` provide sorting mechanic 13 | * 14 | * @tparam T table 15 | */ 16 | trait Sort[T] { 17 | 18 | /** 19 | * Build [[slick.lifted.Ordered]] for given table & field with provided direction 20 | * 21 | * @param table table with fields 22 | * @param field field to sort by 23 | * @param sortDirection direction (asc or desc) 24 | * @return If it's possible to sort by `field` of table `T`, return [[slick.lifted.Ordered]]. In other case return `None` 25 | */ 26 | protected def ordered(table: T, field: String, sortDirection: io.slicker.core.SortDirection): Option[slick.lifted.Ordered] 27 | 28 | /** 29 | * Build [[slick.lifted.Ordered]] for given table & fields with provided direction 30 | * 31 | * @param table table with fields 32 | * @param fields fields to sort by with direction for each field 33 | * @return Will return non-empty [[slick.lifted.Ordered]] if it's possible to sort at least by one field. 34 | */ 35 | def apply(table: T, fields: Seq[(String, SortDirection)]): Ordered = { 36 | val os = fields.flatMap({ 37 | case (field, direction) => ordered(table, field, direction) 38 | }) 39 | os.foldLeft(new Ordered(columns = IndexedSeq.empty))((p, c) => { 40 | new Ordered(columns = p.columns ++ c.columns) 41 | }) 42 | } 43 | 44 | } 45 | 46 | object Sort { 47 | 48 | /** 49 | * Manually build [[Sort]] for table `T` using PartialFunction 50 | * 51 | * {{{ 52 | * Sort.manual[Users](userTable => { 53 | * case "photo" => userTable.photo 54 | * case "email" => userTable.email 55 | * }) 56 | * }}} 57 | * 58 | * @return [[Sort]] instance that will apply sorting only for names/columns in provided partial function 59 | */ 60 | def manual[T](pf: T => PartialFunction[String, Ordered]): Sort[T] = { 61 | new Sort[T] { 62 | override protected def ordered(table: T, field: String, sortDirection: SortDirection): Option[Ordered] = { 63 | pf(table).lift(field).map { ordered => 64 | new Ordered(columns = ordered.columns.map { 65 | case (node, _) => (node, sortDirectionToOrdering(sortDirection)) 66 | }) 67 | } 68 | } 69 | } 70 | } 71 | 72 | private def sortDirectionToOrdering(sortDirection: SortDirection): Ordering = { 73 | sortDirection match { 74 | case SortDirection.Asc => Ordering().asc 75 | case SortDirection.Desc => Ordering().desc 76 | } 77 | } 78 | 79 | /** 80 | * Build [[Sort]] for table `T` semiautomatically. 81 | * It will use provided method names as sorting names. All others fields will be ignored. 82 | * 83 | * {{{ 84 | * Sort.semiauto[Users](table => Seq(table.id, table.name, table.email)) 85 | * }}} 86 | * 87 | * @return [[Sort]] instance that will apply sorting only for columns in provided sequence 88 | */ 89 | def semiauto[T](f: T => Seq[_]): Sort[T] = macro SortMacros.partialImpl[T] 90 | 91 | /** 92 | * Build [[Sort]] for table `T` automatically 93 | * It will build [[Sort]] for all columns in table, using method names as sorting names. 94 | * 95 | * {{ 96 | * Sort.auto[Users] 97 | * }} 98 | * 99 | * @return [[Sort]] instance that will apply sorting for all columns in provided table `T` 100 | */ 101 | def auto[T]: Sort[T] = macro SortMacros.fullImpl[T] 102 | 103 | /** 104 | * Build empty [[Sort]] for table `T`. Default behaviour for all tables. 105 | * 106 | * @return [[Sort]] instance that will always return no fields to sort by 107 | */ 108 | def empty[T]: Sort[T] = new Sort[T] { 109 | override protected def ordered(table: T, field: String, sortDirection: SortDirection): Option[Ordered] = None 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slicker [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.imliar/slicker-core_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.imliar/slicker-core_2.11) [![Build Status](https://travis-ci.org/ImLiar/slicker.svg?branch=master)](https://travis-ci.org/ImLiar/slicker) 2 | ====== 3 | Using [Slick](http://slick.lightbend.com/) in repository manner. 4 | 5 | Repository vs DAO 6 | ----- 7 | DAO or _Data Access Object_ is a quick and nice solution to access and represent 8 | database inside of your code. Close to the tables, persistence included. Sounds like a silver bullet. 9 | But what if inside of your business domain you want to abstract over data mapping and the way your 10 | entity is built? May be you have multiple database sources and don't want to care managing sources on the 11 | business level. 12 | 13 | Here comes the repository layer which allows to hide details of database access 14 | and get rid of duplication of query code. 15 | 16 | You could read more about both patterns here: 17 | 18 | - http://blog.sapiensworks.com/post/2012/11/01/Repository-vs-DAO.aspx 19 | - https://thinkinginobjects.com/2012/08/26/dont-use-dao-use-repository/ 20 | 21 | Point of using Slicker 22 | ----- 23 | 24 | Slick `TableQuery` in fact is an implementation of DAO staying close to SQL and 25 | exposing SQL logic. Slicker doesn't reject this approach but hides usage 26 | of Slick tables under the hood providing sufficient abstraction for simple CRUD operations but 27 | still with possibility of doing manual work. 28 | 29 | Example 30 | ------ 31 | 32 | First add following lines to your build.sbt: 33 | ``` 34 | libraryDependencies ++= Seq( 35 | "com.github.imliar" %% "slicker-core" % "0.3", 36 | "com.github.imliar" %% "slicker-postgres" % "0.3" 37 | ) 38 | ``` 39 | 40 | Then define table and entity that we're going to use: 41 | 42 | ```scala 43 | import io.slicker.postgres.PostgresDriver.api._ 44 | import io.slicker.postgres.{SimpleRecordTable, TableWithId} 45 | import slick.lifted.ProvenShape 46 | 47 | case class User(id: Option[Long], name: String) 48 | 49 | class UserTable extends SimpleRecordTable[Long, User, Users] { 50 | 51 | override val tableQuery: TableQuery[Users] = TableQuery[Users] 52 | 53 | } 54 | 55 | class Users(tag: Tag) extends TableWithId[Long, User](tag, "users") { 56 | 57 | override def id: Rep[Long] = column[Long]("id", O.AutoInc, O.PrimaryKey) 58 | 59 | def name: Rep[String] = column[String]("name") 60 | 61 | override def * : ProvenShape[User] = (id.?, name) <> (User.tupled, User.unapply) 62 | 63 | } 64 | ``` 65 | 66 | `UserTable` will be used by Slicker `Repository` to convert business <> database entities and 67 | operate with `TableQuery`. In this case business model is equal to database one, so it's enough 68 | to use `SimpleRecordTable`. 69 | `TableWithId` defines `id` column with provided type so `Repository` could operate with entity id. 70 | 71 | Now define repository interface as a next step. It's not required but just a good practice 72 | to split intention and implementation. 73 | 74 | ```scala 75 | import io.slicker.core.Repository 76 | 77 | trait UsersRepository extends Repository[Long, User] { 78 | 79 | def findOneByName(name: String): ReadAction[Option[User]] 80 | 81 | } 82 | ``` 83 | 84 | Except manually provided `findAllByName` method `Repository` interface also 85 | defines useful everyday [methods](https://github.com/ImLiar/slicker/blob/master/slicker-core/src/main/scala/io/slicker/core/Repository.scala). 86 | As a result you will get Slick `DBIOAction`. Running of action is up to user. 87 | 88 | Finally define a repository implementation: 89 | 90 | ```scala 91 | import io.slicker.postgres.PostgresDriver.api._ 92 | import io.slicker.postgres.PostgreSQLRepository 93 | 94 | import scala.concurrent.ExecutionContext 95 | 96 | class UsersRepositoryImpl extends PostgreSQLRepository(new UserTable) with UsersRepository { 97 | 98 | //just an example. you could provide your own EC 99 | override protected implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global 100 | 101 | override def findOneByName(name: String): ReadAction[Option[User]] = findOneBy(_.name === name) 102 | 103 | } 104 | ``` 105 | 106 | `findOneBy` is one of methods that come with [PostgreSQLRepository](https://github.com/ImLiar/slicker/blob/master/slicker-postgres/src/main/scala/io/slicker/postgres/PostgreSQLRepository.scala) 107 | and supposed to provide easy-to-use CRUD operations. 108 | 109 | Documentation 110 | ------ 111 | 112 | Detailed documentation could be found [here](https://github.com/ImLiar/slicker/blob/master/docs/index.md). 113 | 114 | Limitations 115 | ------ 116 | 117 | - Only PostgreSQL implementation available. It'll be easy to support other DBs creating new subproject. 118 | -------------------------------------------------------------------------------- /slicker-postgres/src/main/scala/io/slicker/postgres/PostgreSQLRepository.scala: -------------------------------------------------------------------------------- 1 | package io.slicker.postgres 2 | 3 | import io.slicker.core._ 4 | import io.slicker.postgres.PostgresDriver.api._ 5 | import slick.ast.BaseTypedType 6 | import slick.dbio.DBIOAction 7 | import slick.lifted.CanBeQueryCondition 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.language.higherKinds 11 | 12 | abstract class PostgreSQLRepository[Id : BaseTypedType, E, R, T <: TableWithId[Id, R]](protected val table: RecordTable[Id, E, R, T])(implicit entity: Entity[E, Id]) 13 | extends Repository[Id, E] { 14 | 15 | protected implicit val ec: ExecutionContext 16 | 17 | protected val tableQuery: TableQuery[T] = table.tableQuery 18 | 19 | protected def id(e: E): Option[Id] = implicitly[Entity[E, Id]].id(e) 20 | 21 | /** 22 | * Insert or updates entity in DB 23 | * 24 | * @param e entity 25 | * @return inserted entity with new ID or updated entity 26 | */ 27 | def save(e: E): WriteAction[E] = { 28 | id(e) match { 29 | case Some(id) => 30 | tableQuery 31 | .filter(_.id === id) 32 | .update(table.toDatabase(e)) 33 | .map(_ => e) 34 | case None => 35 | tableQuery 36 | .returning(tableQuery) 37 | .into((_, res) => res) 38 | .+=(table.toDatabase(e)) 39 | .map(table.toBusiness) 40 | } 41 | } 42 | 43 | /** 44 | * Insert or updates entities in DB 45 | * 46 | * @param es entities 47 | * @return inserted or updated entities 48 | */ 49 | def save(es: Seq[E]): WriteAction[Seq[E]] = { 50 | DBIOAction.sequence(es.map(save)) 51 | } 52 | 53 | /** 54 | * Find all entities 55 | * 56 | * @param pageRequest request could be limited with offset/limit 57 | * @return All entities in table in respect with pageRequest parameter 58 | */ 59 | def findAll(pageRequest: PageRequest = PageRequest.ALL): ReadAction[Seq[E]] = { 60 | tableQuery 61 | .map(identity) 62 | .withPageRequest(pageRequest) 63 | .result 64 | .map(_.map(table.toBusiness)) 65 | } 66 | 67 | /** 68 | * Find entity by id 69 | * 70 | * @param id 71 | * @return Option with entity. None in case if there is no such entity. 72 | */ 73 | def findById(id: Id): ReadAction[Option[E]] = { 74 | findOneBy(_.id === id) 75 | } 76 | 77 | /** 78 | * Remove entity by id 79 | * 80 | * @param id 81 | * @return True in case if entity was removed 82 | */ 83 | def removeById(id: Id): WriteAction[Boolean] = { 84 | removeBy(_.id === id) 85 | } 86 | 87 | /** 88 | * Remove entity 89 | * 90 | * @param e 91 | * @return True in case if entity was removed 92 | */ 93 | def remove(e: E): WriteAction[Boolean] = { 94 | removeById(id(e).getOrElse(throw new IllegalStateException("Entity is required to have an id for removal"))) 95 | } 96 | 97 | /** 98 | * Remove entities 99 | * 100 | * @param es 101 | * @return True in case if at least one of entities was removed 102 | */ 103 | def remove(es: Seq[E]): WriteAction[Boolean] = { 104 | DBIOAction.sequence(es.map(remove)).map(_ => true) 105 | } 106 | 107 | /** 108 | * Remove all entities in table 109 | * 110 | * @return True in case if at least one of entities was removed 111 | */ 112 | def removeAll(): WriteAction[Boolean] = { 113 | tableQuery.delete.map(_ > 0) 114 | } 115 | 116 | /** 117 | * Count all rows in table 118 | * 119 | * @return Number of rows 120 | */ 121 | def countAll: ReadAction[Int] = { 122 | tableQuery.length.result 123 | } 124 | 125 | /** 126 | * Remove rows from table by predicate. 127 | * {{ 128 | * removeBy(_.foo === "bar") 129 | * }} 130 | * 131 | * @param f predicate 132 | * @param canBeQueryCondition Proof that result of predicate could be a query condition 133 | * @tparam P Type of predicate. it's not a boolean but slick internal 134 | * @return true in case if one more rows were removed 135 | */ 136 | protected def removeBy[P <: Rep[_]](f: T => P)(implicit canBeQueryCondition: CanBeQueryCondition[P]): WriteAction[Boolean] = { 137 | tableQuery.filter(f).delete.map(_ > 0) 138 | } 139 | 140 | /** 141 | * Count rows by predicate. 142 | * {{ 143 | * countBy(_.foo === "bar") 144 | * }} 145 | * 146 | * @param f predicate 147 | * @param canBeQueryCondition Proof that result of predicate could be a query condition 148 | * @tparam P Type of predicate. it's not a boolean but slick internal 149 | * @return Number of rows that satisfied predicate 150 | */ 151 | protected def countBy[P <: Rep[_]](f: T => P)(implicit canBeQueryCondition: CanBeQueryCondition[P]): ReadAction[Int] = { 152 | tableQuery.filter(f).length.result 153 | } 154 | 155 | /** 156 | * Find one entity by predicate 157 | * 158 | * @param f predicate 159 | * @param canBeQueryCondition proof that result of predicate could be a query condition 160 | * @tparam P type of predicate. it's not a boolean but slick internal 161 | * @return First entity that satisfied given predicate. If there is no such entity, None will be returned 162 | */ 163 | protected def findOneBy[P <: Rep[_]](f: T => P)(implicit canBeQueryCondition: CanBeQueryCondition[P]): ReadAction[Option[E]] = { 164 | tableQuery.filter(f).result.headOption.map(_.map(table.toBusiness)) 165 | } 166 | 167 | /** 168 | * Find all entities by predicate 169 | * 170 | * @param f predicate 171 | * @param pageRequest page request to limit/offset query 172 | * @param canBeQueryCondition proof that result of predicate could be a query condition 173 | * @tparam P type of predicate. it's not a boolean but slick internal 174 | * @return 175 | */ 176 | protected def findAllBy[P <: Rep[_]](f: T => P, pageRequest: PageRequest = PageRequest.ALL) 177 | (implicit canBeQueryCondition: CanBeQueryCondition[P]): ReadAction[Seq[E]] = { 178 | tableQuery 179 | .filter(f) 180 | .withPageRequest(pageRequest) 181 | .result.map(_.map(table.toBusiness)) 182 | } 183 | 184 | /** 185 | * Helper for using PageRequest along with slick 186 | */ 187 | protected implicit class QueryWithPageRequest[UQ, CQ[_]](q: Query[T, UQ, CQ]) { 188 | def withPageRequest(pr: PageRequest): Query[T, UQ, CQ] = { 189 | val withOffset = if(pr.perPage == Int.MaxValue) { 190 | q 191 | } else { 192 | q.drop(pr.offset).take(pr.perPage) 193 | } 194 | withOffset.sortBy(t => table.sort(t, pr.sort)) 195 | } 196 | } 197 | 198 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------