在 slick 3.0 中更新多对多连接 table
Updating a many-to-many join table in slick 3.0
我的数据库结构在 Dream
和 Tag
之间具有 多对多关系 。
Dream
s 和 Tag
s 被保存在单独的 tables 中,并且在这种情况下它们之间像往常一样有一个连接 table,与class DreamTag
表示连接:
protected class DreamTagTable(tag: Tag) extends Table[DreamTag](tag, "dreamtags") {
def dreamId = column[Long]("dream_id")
def dream = foreignKey("dreams", dreamId, dreams)(_.id)
def tagId = column[Long]("tag_id")
def tag = foreignKey("tags", tagId, tags)(_.id)
// default projection
def * = (dreamId, tagId) <> ((DreamTag.apply _).tupled, DreamTag.unapply)
}
我已经成功地执行了适当的双重 JOIN
来检索 Dream
及其 Tag
s,但是 我很难以完全非-阻塞方式.
这是我执行检索的代码,因为这可能会阐明一些事情:
def createWithTags(form: DreamForm): Future[Seq[Int]] = db.run {
logger.info(s"Creating dream [$form]")
// action to put the dream
val dreamAction: DBIO[Dream] =
dreams.map(d => (d.title, d.content, d.date, d.userId, d.emotion))
.returning(dreams.map(_.id))
.into((fields, id) => Dream(id, fields._1, fields._2, fields._3, fields._4, fields._5))
.+=((form.title, form.content, form.date, form.userId, form.emotion))
// action to put any tags that don't already exist (create a single action)
val tagActions: DBIO[Seq[MyTag]] =
DBIO.sequence(form.tags.map(text => createTagIfNotExistsAction(text)))
// zip allows us to get the results of both actions in a tuple
val zipAction: DBIO[(Dream, Seq[MyTag])] = dreamAction.zip(tagActions)
// put the entries into the join table, if the zipAction succeeds
val dreamTagsAction = exec(zipAction.asTry) match {
case Success(value) => value match {
case (dream, tags) =>
DBIO.sequence(tags.map(tag => createDreamTagAction(dream, tag)))
}
case Failure(exception) => throw exception
}
dreamTagsAction
}
private def createTagIfNotExistsAction(text: String): DBIO[MyTag] = {
tags.filter(_.text === text).result.headOption.flatMap {
case Some(t: MyTag) => DBIO.successful(t)
case None =>
tags.map(t => (t.text))
.returning(tags.map(_.id))
.into((text, id) => MyTag(id, text)) += text
}
}
private def createDreamTagAction(dream: Dream, tag: MyTag): DBIO[Int] = {
dreamTags += DreamTag(dream.id, tag.id)
}
/**
* Helper method for executing an async action in a blocking way
*/
private def exec[T](action: DBIO[T]): T = Await.result(db.run(action), 2.seconds)
场景
现在我正处于希望能够更新 Dream
和 Tag
列表的阶段,我正在努力。
鉴于现有标签列表为 ["one", "two", "three"]
并且正在更新为 ["two", "three", "four"]
我想:
- 删除 "one" 的
Tag
,如果没有其他 Dream
引用它。
- 不要触摸 "two" 和 "three" 的条目,因为
Tag
和 DreamTag
条目已经存在。
- 创建
Tag
"four" 如果它不存在,并为它添加一个条目到连接 table。
我想我需要做类似 list1.diff(list2)
和 list2.diff(list1)
的事情,但这需要先执行 get,这似乎是错误的。
也许我的想法是错误的 - 是否最好只清除此 Dream
的联接 table 中的所有条目,然后在新列表中创建每个项目,或者有没有一种很好的方法来区分两个列表(以前的和现有的)并适当地执行 deletes/adds?
感谢您的帮助。
N.B。是的,Tag
是一个超级烦人的 class 名字,因为它与 slick.lifted.Tag
!
冲突
更新-我的解决方案:
我选择了理查德在他的回答中提到的选项 2...
// action to put any tags that don't already exist (create a single action)
val tagActions: DBIO[Seq[MyTag]] =
DBIO.sequence(form.tags.map(text => createTagIfNotExistsAction(text)))
// zip allows us to get the results of both actions in a tuple
val zipAction: DBIO[(Int, Seq[MyTag])] = dreamAction.zip(tagActions)
// first clear away the existing dreamtags
val deleteExistingDreamTags = dreamTags
.filter(_.dreamId === dreamId)
.delete
// put the entries into the join table, if the zipAction succeeds
val dreamTagsAction = zipAction.flatMap {
case (_, tags) =>
DBIO.sequence(tags.map(tag => createDreamTagAction(dreamId, tag)))
}
deleteExistingDreamTags.andThen(dreamTagsAction)
I struggled to do it in a fully non-blocking manner.
我看到您有一个 eval
呼叫正在阻塞。我看起来可以用 flatMap
:
代替
case class Dream()
case class MyTag()
val zipAction: DBIO[(Dream, Seq[MyTag])] =
DBIO.successful( (Dream(), MyTag() :: MyTag() :: Nil) )
def createDreamTagAction(dream: Dream)(tag: MyTag): DBIO[Int] =
DBIO.successful(1)
val action: DBIO[Seq[Int]] =
zipAction.flatMap {
case (dream, tags) => DBIO.sequence(tags.map(createDreamTagAction(dream)))
}
Is it best to just clear all entries in the join table for this Dream and then create every item in the new list, or is there a nice way to diff the two lists (previous and existing) and perform the deletes/adds as appropriate?
大体上,您有以下三种选择:
在数据库中查看存在哪些标签,将它们与您想要的状态进行比较,并计算一组插入和删除操作。
删除所有标签,插入你想达到的状态。
将问题移至 SQL,这样您就可以在 table 中不存在的标签中插入标签,并删除在您想要的状态下不存在的标签.您需要查看数据库的功能,并且可能需要在 Slick 中使用 Plain SQL 才能获得效果。我不确定用于添加标签的插入是什么(可能是 MERGE 或某种类型的更新插入),但删除将采用以下形式:delete from tags where tag not in (1,2)
如果您只想要标签 1 和 2 的最终状态。
权衡:
对于 1,您需要 运行 1 次查询来获取现有标签,然后进行 1 次删除查询,至少 1 次插入查询。这将改变最小的行数,但将是最大的查询数。
对于 2,您将执行至少 2 个查询:一个删除和 1 个(可能)用于批量插入。这将更改最大行数。
对于 3,您将执行常量 2 个查询(如果您的数据库可以为您执行逻辑)。如果这甚至可能,查询将更加复杂。
我的数据库结构在 Dream
和 Tag
之间具有 多对多关系 。
Dream
s 和 Tag
s 被保存在单独的 tables 中,并且在这种情况下它们之间像往常一样有一个连接 table,与class DreamTag
表示连接:
protected class DreamTagTable(tag: Tag) extends Table[DreamTag](tag, "dreamtags") {
def dreamId = column[Long]("dream_id")
def dream = foreignKey("dreams", dreamId, dreams)(_.id)
def tagId = column[Long]("tag_id")
def tag = foreignKey("tags", tagId, tags)(_.id)
// default projection
def * = (dreamId, tagId) <> ((DreamTag.apply _).tupled, DreamTag.unapply)
}
我已经成功地执行了适当的双重 JOIN
来检索 Dream
及其 Tag
s,但是 我很难以完全非-阻塞方式.
这是我执行检索的代码,因为这可能会阐明一些事情:
def createWithTags(form: DreamForm): Future[Seq[Int]] = db.run {
logger.info(s"Creating dream [$form]")
// action to put the dream
val dreamAction: DBIO[Dream] =
dreams.map(d => (d.title, d.content, d.date, d.userId, d.emotion))
.returning(dreams.map(_.id))
.into((fields, id) => Dream(id, fields._1, fields._2, fields._3, fields._4, fields._5))
.+=((form.title, form.content, form.date, form.userId, form.emotion))
// action to put any tags that don't already exist (create a single action)
val tagActions: DBIO[Seq[MyTag]] =
DBIO.sequence(form.tags.map(text => createTagIfNotExistsAction(text)))
// zip allows us to get the results of both actions in a tuple
val zipAction: DBIO[(Dream, Seq[MyTag])] = dreamAction.zip(tagActions)
// put the entries into the join table, if the zipAction succeeds
val dreamTagsAction = exec(zipAction.asTry) match {
case Success(value) => value match {
case (dream, tags) =>
DBIO.sequence(tags.map(tag => createDreamTagAction(dream, tag)))
}
case Failure(exception) => throw exception
}
dreamTagsAction
}
private def createTagIfNotExistsAction(text: String): DBIO[MyTag] = {
tags.filter(_.text === text).result.headOption.flatMap {
case Some(t: MyTag) => DBIO.successful(t)
case None =>
tags.map(t => (t.text))
.returning(tags.map(_.id))
.into((text, id) => MyTag(id, text)) += text
}
}
private def createDreamTagAction(dream: Dream, tag: MyTag): DBIO[Int] = {
dreamTags += DreamTag(dream.id, tag.id)
}
/**
* Helper method for executing an async action in a blocking way
*/
private def exec[T](action: DBIO[T]): T = Await.result(db.run(action), 2.seconds)
场景
现在我正处于希望能够更新 Dream
和 Tag
列表的阶段,我正在努力。
鉴于现有标签列表为 ["one", "two", "three"]
并且正在更新为 ["two", "three", "four"]
我想:
- 删除 "one" 的
Tag
,如果没有其他Dream
引用它。 - 不要触摸 "two" 和 "three" 的条目,因为
Tag
和DreamTag
条目已经存在。 - 创建
Tag
"four" 如果它不存在,并为它添加一个条目到连接 table。
我想我需要做类似 list1.diff(list2)
和 list2.diff(list1)
的事情,但这需要先执行 get,这似乎是错误的。
也许我的想法是错误的 - 是否最好只清除此 Dream
的联接 table 中的所有条目,然后在新列表中创建每个项目,或者有没有一种很好的方法来区分两个列表(以前的和现有的)并适当地执行 deletes/adds?
感谢您的帮助。
N.B。是的,Tag
是一个超级烦人的 class 名字,因为它与 slick.lifted.Tag
!
更新-我的解决方案:
我选择了理查德在他的回答中提到的选项 2...
// action to put any tags that don't already exist (create a single action)
val tagActions: DBIO[Seq[MyTag]] =
DBIO.sequence(form.tags.map(text => createTagIfNotExistsAction(text)))
// zip allows us to get the results of both actions in a tuple
val zipAction: DBIO[(Int, Seq[MyTag])] = dreamAction.zip(tagActions)
// first clear away the existing dreamtags
val deleteExistingDreamTags = dreamTags
.filter(_.dreamId === dreamId)
.delete
// put the entries into the join table, if the zipAction succeeds
val dreamTagsAction = zipAction.flatMap {
case (_, tags) =>
DBIO.sequence(tags.map(tag => createDreamTagAction(dreamId, tag)))
}
deleteExistingDreamTags.andThen(dreamTagsAction)
I struggled to do it in a fully non-blocking manner.
我看到您有一个 eval
呼叫正在阻塞。我看起来可以用 flatMap
:
case class Dream()
case class MyTag()
val zipAction: DBIO[(Dream, Seq[MyTag])] =
DBIO.successful( (Dream(), MyTag() :: MyTag() :: Nil) )
def createDreamTagAction(dream: Dream)(tag: MyTag): DBIO[Int] =
DBIO.successful(1)
val action: DBIO[Seq[Int]] =
zipAction.flatMap {
case (dream, tags) => DBIO.sequence(tags.map(createDreamTagAction(dream)))
}
Is it best to just clear all entries in the join table for this Dream and then create every item in the new list, or is there a nice way to diff the two lists (previous and existing) and perform the deletes/adds as appropriate?
大体上,您有以下三种选择:
在数据库中查看存在哪些标签,将它们与您想要的状态进行比较,并计算一组插入和删除操作。
删除所有标签,插入你想达到的状态。
将问题移至 SQL,这样您就可以在 table 中不存在的标签中插入标签,并删除在您想要的状态下不存在的标签.您需要查看数据库的功能,并且可能需要在 Slick 中使用 Plain SQL 才能获得效果。我不确定用于添加标签的插入是什么(可能是 MERGE 或某种类型的更新插入),但删除将采用以下形式:
delete from tags where tag not in (1,2)
如果您只想要标签 1 和 2 的最终状态。
权衡:
对于 1,您需要 运行 1 次查询来获取现有标签,然后进行 1 次删除查询,至少 1 次插入查询。这将改变最小的行数,但将是最大的查询数。
对于 2,您将执行至少 2 个查询:一个删除和 1 个(可能)用于批量插入。这将更改最大行数。
对于 3,您将执行常量 2 个查询(如果您的数据库可以为您执行逻辑)。如果这甚至可能,查询将更加复杂。