在 slick 3.0 中更新多对多连接 table

Updating a many-to-many join table in slick 3.0

我的数据库结构在 DreamTag 之间具有 多对多关系

Dreams 和 Tags 被保存在单独的 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 及其 Tags,但是 我很难以完全非-阻塞方式.

这是我执行检索的代码,因为这可能会阐明一些事情:

  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)

场景

现在我正处于希望能够更新 DreamTag 列表的阶段,我正在努力。

鉴于现有标签列表为 ["one", "two", "three"] 并且正在更新为 ["two", "three", "four"] 我想:

  1. 删除 "one" 的 Tag,如果没有其他 Dream 引用它。
  2. 不要触摸 "two" 和 "three" 的条目,因为 TagDreamTag 条目已经存在。
  3. 创建 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?

大体上,您有以下三种选择:

  1. 在数据库中查看存在哪些标签,将它们与您想要的状态进行比较,并计算一组插入和删除操作。

  2. 删除所有标签,插入你想达到的状态。

  3. 将问题移至 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 个查询(如果您的数据库可以为您执行逻辑)。如果这甚至可能,查询将更加复杂。