2

I'm trying to figure out how to port my own closure table implementation from another language to Scala with concurrency in mind.

I have two models, a Node (id | parentID) and a NodeTree (id | ancestor | descendant) where each entry resembles an edge in the tree.

For each new node I must do the following: Query all the ancestors (or filter the TableQuery for them) and then add a NodeTree-Entry (an edge) for each ancestor

Thanks to panther I got this far:

private val nodes = TableQuery[Nodes]

override def create(node: Node): Future[Seq[Int]] =
    {
        val createNodesAction = (
            for
            {
                parent <- nodes
                node <- (nodeTrees returning nodeTrees.map(_.id) into ((ntEntry, ntId) => ntEntry.copy(id = Some(ntId))) += NodeTree(id = None, ancestor = parent.id, descendant = node.id, deleted = None, createdAt = new Timestamp(now.getTime), updatedAt = new Timestamp(now.getTime)))
            } yield (node)
        ).transactionally

        db run createNodesAction
    }

But this yields into a type mismatch;

type mismatch; found : slick.lifted.Rep[Long] required: Option[Long]

Once again: All I want to do is: For each parentNode (= each parent's parent until the last ancestor-node has no parent!) I want to create an entry in the nodeTree so that later I can easily grab all the descendants and ancestors with just another method call that filters through the NodeTree-Table.

(Just a closure table, really)

edit: These are my models

case class Node(id: Option[Long], parentID: Option[Long], level: Option[Long], deleted: Option[Boolean], createdAt: Timestamp, updatedAt: Timestamp)

class Nodes(tag: Tag) extends Table[Node](tag, "nodes")
{
    implicit val dateColumnType = MappedColumnType.base[Timestamp, Long](d => d.getTime, d => new Timestamp(d))

    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def parentID = column[Long]("parent_id")
    def level = column[Long]("level")
    def deleted = column[Boolean]("deleted")
    def createdAt = column[Timestamp]("created_at")
    def updatedAt = column[Timestamp]("updated_at")

    def * = (id.?, parentID.?, level.?, deleted.?, createdAt, updatedAt) <> (Node.tupled, Node.unapply)
}

case class NodeTree(id: Option[Long], ancestor: Option[Long], descendant: Option[Long], deleted: Option[Boolean], createdAt: Timestamp, updatedAt: Timestamp)

class NodeTrees(tag: Tag) extends Table[NodeTree](tag, "nodetree")
{
    implicit val dateColumnType = MappedColumnType.base[Timestamp, Long](d => d.getTime, d => new Timestamp(d))

    def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
    def ancestor = column[Long]("ancestor")
    def descendant = column[Long]("descendant")
    def deleted = column[Boolean]("deleted")
    def createdAt = column[Timestamp]("created_at")
    def updatedAt = column[Timestamp]("updated_at")

    def * = (id.?, ancestor.?, descendant.?, deleted.?, createdAt, updatedAt) <> (NodeTree.tupled, NodeTree.unapply)
}

What I want to do is a closure table (http://technobytz.com/closure_table_store_hierarchical_data.html) that fills it's edges (nodeTree) automatically when I create a node. So I don't want to manually add all these entries to the database, but when I create a node on level 5 I want the whole path (= entries in the nodetree-table) to be created automatically.

I hope that clears stuff up a bit :)

2 Answers2

1

Try this:

override def create(node: Node): Future[Seq[Int]] =
{
    val parents = getAllParents(node)
    val createNodesAction = (
      for {
        parent <- parents
        node <- nodeTrees += NodeTree(id = None, ancestor = parent.id, descendant = node.id)
      } yield (node)
    ).transactionally

   db run createNodesAction
}

You should not have to separately retrieve the parents separately. It can be done in the same session. Above, you can easily replace 'parents' in for-comprehension with the TableQuery you want to work against (with or without filter).

Also note that, here you would be returning sequence of number of rows affected by the insert action. To instead return list of node Ids (assuming you would have marked node Ids as AUTO_INC in db), then you can do something like this:

override def create(node: Node): Future[Seq[Int]] =
{
    val createNodesAction = (
      for {
        parent <- parents
        node <- (nodeTrees returning nodeTrees.map(_.id) into ((ntEntry, ntId) => ntEntry.copy(id = Some(ntId))) += NodeTree(id = None, ancestor = parent.id, descendant = node.id)
      } yield (node)
    ).transactionally

   db run createNodesAction
}

The difference is: (nodeTrees returning nodeTrees.map(_.id) into ((ntEntry, ntId) => ntEntry.copy(id = Some(ntId))) instead of just (nodeTrees) that retrieves and maps auto inc id into result.


Update: Try this:

override def create(node: Node): Future[Seq[Int]] =
{
    def createNodesAction(parentId: Long): DBIOAction[NodeTree, NoStream, Read with Write] = (
      for {
        node <- (nodeTrees returning nodeTrees.map(_.id) into ((ntEntry, ntId) => ntEntry.copy(id = Some(ntId))) += NodeTree(id = None, ancestor = parentId, descendant = node.id)
      } yield (node)
    ).transactionally

   // TODO: Init and pass in 'parents'
   db.run(DBIO.sequence(parents.map(createNodesAction(_.id)))
}
panther
  • 767
  • 5
  • 21
  • I would accept your answer but there are a few problems: First: nt.id = ntId -> ntId is of type Long, while Option[Long] is needed here. Also nt.id = ntId -> reassignment to val :/ Also, could you please elaborate... would the Table query be updated (if more entries are there, would it re-selet?) or would I need to do that manually? –  May 23 '15 at 09:48
  • Edited the answer to address setting optional value and reassignment to val problem. On your question of _selecting multiple entries on insert_, I could not find a way to do that. That might be because ID is probably retrieved using another SQL construct ([last_insert_id](https://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_last-insert-id)). Currently, for selecting other items from the inserted row (like timestamp), I do it manually by running a select query on the retrieved id. – panther May 26 '15 at 19:43
  • value id is not a member of List[models.Node] *sighs* I have to unpack the list or even flatten it? If I try it with the TableQuery I even get: type mismatch; found : slick.lifted.Rep[Long] required: Option[Long] –  May 28 '15 at 16:53
  • On: found : slick.lifted.Rep[Long] required: Option[Long], you will see Rep[T] rather than T when you are trying to use a value before executing it. This is because each type is lifted to Rep[T] for execution before actually running it, and then mapped back to T after execution. Please share the code how you are trying to use it. – panther Jun 01 '15 at 23:36
  • Also, is [this](http://stackoverflow.com/questions/30567365/slick-3-0-0-how-to-query-one-to-many-many-to-many-relations) what you are looking for. – panther Jun 02 '15 at 00:07
  • I'm trying your second example. And... well, for each Node I want to create multiple NodeTree-Entries that reference that very entry. I don't see why this shouldn't be possible. –  Jun 07 '15 at 20:22
  • Sorry, I have no idea what this should do. I can't find "Tables" to import (you meant Table?) And what is "NodesRow"? Should this be the "Nodes" from the TableQuery? Do I need to activate Stream-Support (experimental) for this or something? As said for Tables it only finds something from the google namespace. –  Jun 15 '15 at 20:26
  • I guess, it should be NodeTree in this case. It is basically the case class mapping to the corresponding DB row. – panther Jun 15 '15 at 21:02
  • I would really love to give you the bounty/reputation, because you helped me a lot, but it's still not working and I have no idea why. I tried with Tables.NodeTree and with Table.NodeTree, no luck. Also: db.run(DBIO.sequence(parents.map(createNodesAction(_.id)))) gives me another TypeMismatch (expected Long, actual (Any) => Any) I don't even understand why. –  Jul 02 '15 at 14:24
0

Try changing to this line.

  node <- (nodeTrees returning nodeTrees.map(_.id) into ((ntEntry, ntId) => ntEntry.copy(id = ntId)) += NodeTree(id = None, ancestor = parent.id, descendant = node.id, deleted = None, createdAt = new Timestamp(now.getTime), updatedAt = new Timestamp(now.getTime)))

Does it address the issue? It's hard to tell from your question exactly what your models are.

beefyhalo
  • 1,691
  • 2
  • 21
  • 33
  • 1
    I added my models code to the question, although I don't see how that should help. Anyway, I also added another description on what exactly I want to achieve. Thanks anyway for your help! :) –  Jul 01 '15 at 12:39