2

I'm trying to derive a newly inserted column's value from a previously inserted column's value. For instance, if I had this database, where every row (except the first row) must reference the ID of another row. I would like to increment the Height column by one from the value of Height in the row that Previous ID references

----------
ID | Previous ID | Height

0  | null        | 123   

1  | 0           | 124 

2  | 1           | 125 

3  | 1           | 125

Note how height is incremented by one from the previous value of height with the primary key ID. Is there any easy way to do this with Slick? A table could look something like this

case class ExampleCaseClass(id: Option[Long], previousId: Long)

class ExampleTable(tag: Tag)  extends Table[ExampleCaseClass](tag,"example") { 

def id = column[Long]("id",O.PrimaryKey, O.AutoInc) 

def previousId = column[Long]("previous_id")

//this is the column that needs to be derived based on the height of 'previousId'
def height = column[Long]("height") 

}

Can this be done simply?

tksfz
  • 2,932
  • 1
  • 23
  • 25
Chris Stewart
  • 1,641
  • 2
  • 28
  • 60
  • When do you calculate height? While inserting - do you only know ID and PreviousID and the height should be auto-calculated? (and once calculated it will be read only?) – Piotr Reszke Sep 24 '16 at 19:19
  • @PiotrR It should be auto calculated, it should be incremented by 1. When inserting, you only know the 'previous id' the specific application I'm referring to is a blockchain, an append only data structure where each block references the previous block (thus the previous ID), and the block we are inserting increments the height of the blockchain by one – Chris Stewart Sep 24 '16 at 21:46
  • Do you just want to query the height of previous_id and then insert the new row with height + 1? And you want to do that in one statement? – tksfz Sep 25 '16 at 15:33
  • @tksfz Yes, that sounds right. The new height should be auto calculated (to any user that is inserting a new row). I don't want users of the table to have to query the previous id every time and then increment the height – Chris Stewart Sep 25 '16 at 18:20

2 Answers2

1

There are two parts to this:

  1. How do you insert a row using values that depend on some other row?
  2. How do you package that up in such a way that the logic is "transparent" to users of your table?

There are two approaches for (1):

  • (1a) Do the query and the insert in separate SQL statements, with two round-trips to the database. (Fine performance-wise in almost all cases.)

  • (1b) Do the query and insert in one SQL statement using a INSERT INTO ... SELECT statement. (Marginally faster in certain uncommon situations).

Aside from performance, the two require different syntactic approaches. Both approaches in Slick are discussed in the two answers for:

Scala+Slick 3: Inserting the result of one query into another table

I actually think (1b) is more concise anyway. I mention (1a) to give you another option to explore. (1b) is accomplished using forceInsertQuery as described in:

http://slick.lightbend.com/doc/3.1.1/queries.html#inserting

val example = TableQuery[ExampleTable]
def insertAuto(previousId: Int) = {
  val query = example.filter(_.id == previousId).map(r => (previousId, r.height))
  DBIO.seq(example.forceInsertQuery(query))
}

(Note that when using INSERT INTO .. SELECT you can mix user-supplied literal values with queried values simply by embedding the literals in the SELECT clause.)

There might be an issue where we need to supply the id even though it's auto-incremented because the docs seem to suggest forceInsertQuery requires explicit values even for autoinc columns.

Now, how you want to package up that def to make it convenient for your users to invoke, I'm not actually sure. That should be a simpler thing to accomplish. You could consider making this an extension method on TableQuery[ExampleTable]:

implicit class RichExampleTable(example: TableQuery[ExampleTable]) {
  // def extension methods here
}

But this does require your users to invoke the insertAuto method rather than use "direct" Slick statements. I don't think there's any way around that.

Community
  • 1
  • 1
tksfz
  • 2,932
  • 1
  • 23
  • 25
  • I guess I should have mentioned this in the OP, but the `height` field isn't exposed in the case class `ExampleCaseClass`. I was wondering if there could be something placed on the column in the same way that `O.AutoInc` is used, except with a relation to another column. – Chris Stewart Sep 26 '16 at 13:33
  • Perhaps this could be done with some sort of `foreignKey` relation on the same table? – Chris Stewart Sep 26 '16 at 13:46
1

You can use a plain sql query to do this, if you don't want to expose the height field:

def insertExample(previousId: Int): DBIO[Int] = {
  sqlu"insert into example (previous_id, height) select $previousId, height + 1 from example where id = $previousId"
}

Another way of implement this is adding a database trigger. By this way, you can use a plain insert, and the database will do the auto increment:

CREATE TRIGGER auto_height BEFORE INSERT ON example
FOR EACH ROW
SET NEW.height = 1 + (SELECT height FROM example WHERE id = NEW.previous_id);
thirstycrow
  • 2,806
  • 15
  • 18
  • Hmm looks like Slick doesn't allow you to model triggers – Chris Stewart Sep 27 '16 at 17:36
  • Well I guess the idea was that the trigger is only on the database - slick does insert dummy height (example: height=0 or null) and the db trigger overwrites it. – Piotr Reszke Sep 27 '16 at 19:04
  • @thirstycrow One issue i've found with this is it fails silently if there isn't a `previousId` in the database, is there a simple way to make this not fail silently? – Chris Stewart Sep 28 '16 at 18:39
  • You can tell whether a record is actually inserted by checking the # of affected rows. – thirstycrow Sep 29 '16 at 02:11
  • Or you may use a slightly different query: ```sqlu"insert into example (previous_id, height) select a.previous_id, b.height + 1 from (select $previousId as previous_id) a left join example b on a.previous_id = b.id"```. When the previous record does not exist, the ```select``` returns ```NULL``` for the ```height``` column. If you created the table with the ```height``` not nullable, it may cause an error like this: ```ERROR 1048 (23000): Column 'height' cannot be null``` – thirstycrow Sep 29 '16 at 02:27