Обновление таблицы соединений многие-ко-многим в Slick 3.0 - PullRequest
0 голосов
/ 22 февраля 2020

У меня есть структура базы данных со отношением «многие ко многим» между Dream с и Tag с.

Dream с и Tag с сохраняются в отдельных таблицах, и между ними, как обычно, в такой ситуации есть таблица соединения, с классом 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 s , и я изо всех сил.

Учитывая ситуацию, когда существующий список тегов ["one", "two", "three"] и обновляется до ["two", "three", "four"] Я хочу:

  1. Удалить Tag для «один», если другие Dream s не ссылаются на него.
  2. Не трогайте записи для «два» и «три», как Tag и DreamTag записи уже существуют.
  3. Создайте Tag "четверку", если она не существует, и добавьте для нее запись в таблицу соединений.

Я думаю, что мне нужно сделайте что-то вроде list1.diff(list2) и list2.diff(list1), но для этого потребуется сначала выполнить get, что кажется неправильным.

Возможно, я ошибаюсь - Лучше всего просто удалить все записи в объединении таблицу для этого Dream, а затем создать каждый элемент в новом списке, или есть хороший способ изменить два списка (предыдущий и существующий) и выполнить удаление / добавление в зависимости от ситуации?

Спасибо за помощь.

NB Да, Tag - это очень раздражающее имя класса, поскольку оно конфликтует с 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)

1 Ответ

1 голос
/ 26 февраля 2020

Я изо всех сил пытался сделать это полностью неблокирующим образом.

Я вижу, у вас есть вызов 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)))
  }

. Лучше всего просто очистить все записи в таблице соединений для этой Мечты, а затем создать каждый элемент в новом списке, или есть хороший способ различить два списка (предыдущий и существующий) и выполнить удаление / добавление в зависимости от ситуации?

В общем, у вас есть три варианта:

  1. Просмотрите базу данных, чтобы увидеть, какие существуют теги, сравните их с желаемым состоянием и вычислите набор операций вставки и удаления.

  2. Удалите все теги и вставьте состояние, которого вы хотите достичь.

  3. Переместите проблему в SQL, чтобы вы вставляли теги там, где их еще нет в таблице, и удаляли теги, которых нет существовать в желаемом состоянии. Вам нужно взглянуть на возможности вашей базы данных и, вероятно, нужно использовать Plain SQL в Slick, чтобы получить эффект. Я не уверен, что будет вставка для добавления тегов (возможно, MERGE или upsert некоторого вида), но удаление будет иметь вид: delete from tags where tag not in (1,2), если вы хотите конечное состояние только теги 1 и 2.

Сделки выключены:

  • Для 1 вам нужно выполнить 1 запрос для извлечения существующих тегов, а затем 1 запрос на удаление, и в минимум 1 для вставок. Это изменит наименьшее количество строк, но будет наибольшим числом запросов.

  • Для 2 вы будете выполнять как минимум 2 запроса: удаление и 1 (потенциально) для объемной вставки. Это изменит наибольшее количество строк.

  • Для 3 вы будете выполнять постоянные 2 запроса (если ваша база данных может выполнить для вас logi c). Если это даже возможно, запросы будут более сложными.

...