I am trying to implement a simple aggregation root in slick.
But I don't really know what is the best way to do that.
Here is my domain objects:
case class Project(id: UUID,
name: String,
state: ProjectState,
description: String,
team: String,
tags: Set[String]
I would like to store the "tags" in a separate table and build up the "Project" objects from "projects_table" and "project_tags_table"
Here is my table definition:
class ProjectTable(tag: Tag) extends Table[ProjectTableRecord](tag, Some("octopus_service"), "projects") {
def id: Rep[UUID] = column[UUID]("id", O.PrimaryKey)
def name: Rep[String] = column[String]("name")
def state: Rep[ProjectState] = column[ProjectState]("state")
def description: Rep[String] = column[String]("description")
def team: Rep[String] = column[String]("team")
override def * : ProvenShape[ProjectTableRecord] = (id, name, state, description, team, created, lastModified) <> (
(ProjectTableRecord.apply _).tupled, ProjectTableRecord.unapply
)
}
class ProjectTagTable(tag: Tag) extends Table[ProjectTag](tag, Some("octopus_service"), "project_tags") {
def projectID: Rep[UUID] = column[UUID]("project_id")
def name: Rep[String] = column[String]("name")
def project = foreignKey("PROJECT_FK", projectID, TableQuery[ProjectTable])(_.id, onUpdate = ForeignKeyAction.Restrict, onDelete = ForeignKeyAction.Cascade)
override def * : ProvenShape[ProjectTag] = (projectID, name) <> (
ProjectTag.tupled, ProjectTag.unapply
)
}
How can I generate "Project" objects from joining these 2 tables?
Thanks in advance :)
I think there is a misconception on the level of responsibility. Slick allows you to access relational database (to some extent the same way as SQL allows you to do it). It's basically a DAO layer.
Aggregate root is really a level above this (it's a domain thing, not db level thing - although they often are the same to large extent).
So basically you need to have a level above Slick tables that would allow you to perform different queries and aggregate the results into single being.
Before we start though - you should create and store somewhere your TableQuery objects, perhaps like this:
lazy val ProjectTable = TableQuery[ProjectTable]
lazy val ProjectTagTable = TableQuery[ProjectTagTable]
You could put them probably somewhere near you Table definitions.
So first as I mentioned your Aggregate Root being Project needs be pulled by something. Let's call it ProjectRepository.
Let's say it will have a method def load(id: UUID): Future[Project].
This method would perhaps look like this:
class ProjectRepository {
def load(id: UUID): Future[Project] = {
db.run(
for {
project <- ProjectTable.filter(_.id === id).result
tags <- ProjectTagTable.filter(_.projectId === id).result
} yield {
Project(
id = project.id,
name = project.name,
state = project.state,
description = project.description,
team = project.team,
tags = tags.map(_.name)
)
}
)
}
// another example - if you wanted to extract multiple projects
// (in reality you would probably apply some paging here)
def findAll(): Future[Seq[Project]] = {
db.run(
ProjectTable
.join(ProjectTag).on(_.id === _.projectId)
.result
.map { _.groupBy(_._1)
.map { case (project, grouped) =>
Project(
id = project.id,
name = project.name,
state = project.state,
description = project.description,
team = project.team,
tags = grouped.map(_._2.name)
)
}
}
)
}
}
Digression:
If you wanted to have paging in findAll method you would need to do something like this:
ProjectTable
.drop(pageNumber * pageSize)
.take(pageSize)
.join(ProjectTag).on(_.id === _.projectId)
.result
Above would produce sub-query but it is basically typical way how you do paging with multiple joined relations (without subquery you would page over whole result set which is most of the time not what you need!).
Coming back to main part:
Obviously it would be all easier if you defined you defined your Project as:
case class Project(project: ProjectRecord, tags: Seq[ProjectTag])
then your yield would be simply:
yield {
Project(project, tags)
}
but that's definitely a matter of taste (it make actually sense to make it as you did - to hide internal record layout).
Basically there are potentially multiple things that could be improved here. I am not really an expert on DDD but at least from Slick perspective the 1st change that should be done is to change the method:
def load(id: UUID): Future[Project]
to
def load(id: UUID): DBIO[Project]
and perform db.run(...) operation on some higher level. The reason for this is that in Slick as soon as you fire db.run (thus convert DBIO to Future) you loose ability to compose multiple operation within single transaction. Therefore a common pattern is to push DBIO pretty high in application layers, basically up to some business levels which defined transactional boundaries.
Related
I pretty new in using slick and now I faced with the issue how to retrieve some data from two tables.
I have one table
class ExecutionTable(tag: Tag) extends Table[ExecTuple](tag, "execution") {
val id: Rep[String] = column[String]("id")
val executionDefinitionId: Rep[Long] = column[Long]("executionDefinitionId")
// other fields are omitted
def * = ???
}
and another table
class ServiceStatusTable(tag: Tag)
extends Table[(String, Option[String])](tag, "serviceStatus") {
def serviceId: Rep[String] = column[String]("serviceId")
def detail: Rep[String] = column[String]("detail")
def * = (serviceId, detail.?)
}
In Dao I convert data from this two tables to a business object
case class ServiceStatus(
id: String,
detail: Option[String] = None, //other fields
)
like this
private lazy val getServiceStatusCompiled = Compiled {
(id: Rep[String], tenantId: Rep[String]) =>
for {
exec <- getExecutionById(id, tenantId)
status <- serviceStatuses if exec.id === status.serviceId
} yield mapToServiceStatus(exec, status)
}
and later
def getServiceStatus(id: String, tenantId: String)
: Future[Option[ServiceStatus]] = db
.run(getServiceStatusCompiled(id, tenantId).result.transactionally)
.map(_.headOption)
The problem is that not for all entries from table execution exists entry in table serviceStatus. I cannot modify table execution and add to it field details as it is only service specific.
When I run query in case when for entry from execution exists entry in serviceStatus all works as expected. But if there is no entry in serviceStatus, Future[None] is returned.
Question: Is there any option to obtain status in for comprehension as Option depending on existing entry in table serviceStatus or some else workaround?
Usually, in case when join condition does not find corresponding record in the "right" table but the result should still contain the row from "left" table, left join is used.
In your case you can do something like:
Execution
.filter(...execution table filter...)
.joinLeft(ServiceStatus).on(_.id===_.serviceId)
This gives you pair of
(Execution, Rep[Option[ServiceStatus]])
and after query execution:
(Execution, Option[ServiceStatus])
I'm writing a CRUD app using Slick, and I want my update queries to only update a specific set of columns and I use .map().update() for that.
I have a function that returns a tuple of fields that can be updated in my table definition (def writableFields). And I have a funciton that returns a tuple of values to write there extracted from a case class.
It works fine, but it's annoying to create a repo and write the whole update function for every table. I want to create a generic form of this function, and make my table and it's companion object to extend some trait. But I cannot come up with correct type definitions.
Slick expects output of map() to be somehow compatible with the output of update. And I don't know how to make a generic type for tuples.
Is it even possible to accomplish? Or is there an alternative way to limit code duplication? Ideally I want to avoid writing Repos at all and just either instantiate a generic class or call a generic method.
object ProjectsRepo extends BaseRepository[Projects, Project] {
protected val query = lifted.TableQuery[Projects]
def update(id: Long, c: Project): Future[Option[Project]] = {
val q = filterByIdQuery(id).map(_.writableFields)
.update(Projects.mapFormToTable(c))
(db run q).flatMap(
affected =>
if (affected > 0) {
findOneById(id)
} else {
Future(None)
}
)
}
}
class Projects(tag: Tag) extends Table[Project](tag, "projects") with IdentifiableTable[Long] {
val id = column[Long]("id", O.PrimaryKey, O.AutoInc)
val title = column[String]("title")
val slug = column[String]("slug")
val created_at = column[Timestamp]("created_at")
val updated_at = column[Timestamp]("updated_at")
def writableFields =
(
title,
slug
)
def readableFields =
(
id,
created_at,
updated_at
)
def allFields = writableFields ++ readableFields // shapeless
def * = allFields <> (Projects.mapFromTable, (_: Project) => None)
}
object Projects {
def mapFormToTable(c: Project): FormFields =
(
c.title,
c.slug
)
}
I have written a simple Play! 2 REST with Slick application. I have following domain model:
case class Company(id: Option[Long], name: String)
case class Department(id: Option[Long], name: String, companyId: Long)
class Companies(tag: Tag) extends Table[Company](tag, "COMPANY") {
def id = column[Long]("ID", O.AutoInc, O.PrimaryKey)
def name = column[String]("NAME")
def * = (id.?, name) <> (Company.tupled, Company.unapply)
}
val companies = TableQuery[Companies]
class Departments(tag: Tag) extends Table[Department](tag, "DEPARTMENT") {
def id = column[Long]("ID", O.AutoInc, O.PrimaryKey)
def name = column[String]("NAME")
def companyId = column[Long]("COMPANY_ID")
def company = foreignKey("FK_DEPARTMENT_COMPANY", companyId, companies)(_.id)
def * = (id.?, name, companyId) <> (Department.tupled, Department.unapply)
}
val departments = TableQuery[Departments]
and here's my method to query all companies with all related departments:
override def findAll: Future[List[(Company, Department)]] = {
db.run((companies join departments on (_.id === _.companyId)).to[List].result)
}
Unfortuneatelly I want to display data in tree JSON format, so I will have to build query that gets all companies with departments and map them somehow to CompanyDTO, something like that:
case class CompanyDTO(id: Option[Long], name: String, departments: List[Department])
Do you know what is the best solution for this? Should I map List[(Company, Department)] with JSON formatters or should I change my query to use CompanyDTO? If so how can I map results to CompanyDTO?
One-to-Many relationships to my knowledge haven't been known to be fetched in one query in RDBMS. the best you can do while avoiding the n+1 problem is doing it in 2 queries. here's how it'd go in your case:
for {
comps <- companies.result
deps <- comps.map(c => departments.filter(_.companyId === c.id.get))
.reduceLeft((carry,item) => carry unionAll item).result
grouped = deps.groupBy(_.companyId)
} yield comps.map{ c =>
val companyDeps = grouped.getOrElse(c.id.get,Seq()).toList
CompanyDTO(c.id,c.name,companyDeps)
}
there are some fixed parts in this query that you'll come to realize in time. this makes it a good candidate for abstraction, something you can reuse to fetch one-to-many relationships in general in slick.
I'm working with Slick 3 and Play! 2.4 and I have a very common problem that I don't manage to resolve.
I have a table playlist that can be linked to some tracks with the help of a relation table playlistsTracks. I want to be able to get all the playlists with their tracks relation and their tracks. My problem is that I don't manage to get the playlists if they do not have any relations.
Here are the 3 tables:
class Playlists(tag: Tag) extends Table[Playlist](tag, "playlists") {
def id = column[Long]("playlistid", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = (id.?, name) <> ((Playlist.apply _).tupled, Playlist.unapply)
}
class PlaylistsTracks(tag: Tag) extends Table[PlaylistTrack](tag, "playliststracks") {
def playlistId = column[Long]("playlistid")
def trackId = column[UUID]("trackid")
def trackRank = column[Double]("trackrank")
def * = (playlistId, trackId, trackRank) <> ((PlaylistTrack.apply _).tupled, PlaylistTrack.unapply)
def aFK = foreignKey("playlistId", playlistId, playlists)(_.id, onDelete = ForeignKeyAction.Cascade)
def bFK = foreignKey("trackId", trackId, tracks)(_.uuid, onDelete = ForeignKeyAction.Cascade)
}
class Tracks(tag: Tag) extends Table[Track](tag, "tracks") {
def uuid = column[UUID]("trackid", O.PrimaryKey)
def title = column[String]("title")
def * = (uuid, title) <> ((Track.apply _).tupled, Track.unapply)
}
For the moment the snippet of code that get the playlists look like this:
val query = for {
playlist <- playlists
playlistTrack <- playlistsTracks if playlistTrack.playlistId === playlist.id
track <- tracks if playlistTrack.trackId === track.uuid
} yield (playlist, playlistTrack, track)
db.run(query.result) map { println }
It prints something like Vector(Playlist, PlaylistTrack, Track) (what I want) but I solely get the playlists having relations instead of getting all the playlists, even the one without relations as I would like.
I tried a lot of things like using join (or joinFull, joinLeft, joinRight...) but without success, and it is unfortunately difficult to find some example projects with not only very easy relations.
You need to use left join between the Playlists and PlaylistTracks table and use inner join between PlaylistTracks and Tracks.
There are some things missing in the example so I can't actually compile the following, but I think you can try something like:
val query = for {
(playlist, optionalPlaylistTrackAndTrack) <- playlists joinLeft (playlistsTracks join tracks on (_.trackId === _.uuid)) on (_.id === _._1.playlistId)
} yield (playlist, optionalPlaylistTrackAndTrack)
Note that optionalPlaylistTrackAndTrack is an Option of a tuple representing a playlist track and a track. This is because there may be a playlist without a playlistTrack.
I have an idea how my data access layer with Scala Slick should look like, but I'm not sure if it's really possible.
Let's assume I have a User table which has the usual fields like id, email, password, etc.
object Users extends Table[(String, String, Option[String], Boolean)]("User") {
def id = column[String]("id", O.PrimaryKey)
def email = column[String]("email")
def password = column[String]("password")
def active = column[Boolean]("active")
def * = id ~ email ~ password.? ~ active
}
And I wish to query them in different ways, currently the ugly way is to have a new database session, do the for comprehension and then do different if statements to achieve what I want.
e.g.
def getUser(email: String, password: String): Option[User] = {
database withSession { implicit session: Session =>
val queryUser = (for {
user <- Users
if user.email === email &&
user.password === password &&
user.active === true
} //yield and map to user class, etc...
}
def getUser(identifier: String): Option[User] = {
database withSession { implicit session: Session =>
val queryUser = (for {
user <- Users
if user.id === identifier &&
user.active === true
} //yield and map to user class, etc...
}
What I would prefer is to have a private method for the query and then public methods which define queries along the lines of
type UserQuery = User => Boolean
private def getUserByQuery(whereQuery: UserQuery): Option[User] = {
database withSession { implicit session: Session =>
val queryUser = (for {
user <- Users
somehow run whereQuery here to filter
} // yield and boring stuff
}
def getUserByEmailAndPassword(email, pass){ ... define by query and call getUserByQuery ...}
getUserById(id){….}
getUserByFoo{….}
That way, the query logic is encapsulated in the relevant public functions and the actual querying and mapping to the user object is in a reusable function that other people dont need to be concerned with.
The problem I have is trying to refactor the "where" bit's into functions that I can pass around. Trying to do things like select them in intellij and using the refactoring results in some pretty crazy typing going on.
Does anyone have any examples they could show of doing close to what I am trying to achieve?
1) wrapping queries in a def means the query statement is re-generated on every single request, and, since query params are not bound, no prepared statement is passed to the underlying DBMS.
2) you're not taking advantage of composition
Instead, if you define parameterized query vals that def query wrappers call, you can get the best of both worlds.
val uBase = for{
u <- Users
ur <- UserRoles if u.id is ur.UserID
} yield (u,ur)
// composition: generates prepared statement one time, on startup
val byRole = for{ roleGroup <- Parameters[String]
(u,ur) <- uBase
r <- Roles if(r.roleGroup is roleGroup) && (r.id is ur.roleID)
} yield u
def findByRole(roleGroup: RoleGroup): List[User] = {
db withSession { implicit ss:SS=>
byRole(roleGroup.toString).list
}
}
If you need one-off finders for a single property, use:
val byBar = Foo.createFinderBy(_.bar)
val byBaz = Foo.createFinderBy(_.baz)
Can't remember where, maybe on SO, or Slick user group, but I did see a very creative solution that allowed for multiple bound params, basically a createFinderBy on steroids. Not so useful to me though, as the solution was limited to a single mapper/table object.
At any rate composing for comprehensions seems to do what you're trying to do.
I have recently done something similar, one way to do this could be following, write a general select method which takes a predicate
def select(where: Users.type => Column[Boolean]): Option[User] = {
database withSession { implicit session: Session =>
val queryUser = (for {
user <- Users where(user)
} //yield and map to user class, etc...
}
and then write the method which passes the actual predicate as a higher order function
def getUserByEmail(email:String):Option[User]={
select((u: Users.type) => u.*._2 === email)
}
similarly
def getActiveUserByEmail(email:String):Option[User]={
select((u: Users.type) => u.*._2 === email && u.*._4 === true)
}