Slick 3.1.x 以事务方式将所有详细信息从一个主控插入另一个主控?

Slick 3.1.x Insert all details from one master to the other, transactionally?

我有一个有趣的用例。基本上我有一个 sourceUser: UserRowtargetUser: UserRow 需要合并。合并意味着将 sourceUser 标记为不活动并将所有源链接帐户(详细信息)复制到目标用户。所有缺失的细节,例如UserRow 可以检查 slick 生成的类型等 in this github project

我可以在 SQL 中做到这一点,没问题,例如

UPDATE "user" SET active=false WHERE user_id=${sourceUserId};
INSERT INTO linked_account(user_id, provider_key, provider_hashed) 
   SELECT ${targetUserId}, provider_key, provider_hashed
   FROM linked_account 
   WHERE user_id=${sourceUserId}

我的 Slick 尝试是通用 dao 和看不到如何以事务方式组合的 slick 动作的混合体。我也找不到同时插入 select 和插入的方法,即此尝试无法编译:

def merge(targetUser: UserRow, sourceUser: UserRow) : Future[Unit] = {
  // deactivate the source user
  update(sourceUser.copy(active = false))

  val action = (for {
    // get the linked accounts to source 
    linkedAccountSource <- LinkedAccount
    // match the source user
    source <- User if source.id === sourceUser.id && linkedAccountSource.userId === source.id
    // insert into target the previously matched source linked 
    // account but having target user as user_id
    // ........................................ doesn't compile here VVVVVVVVVVVVVVVVVVVVVVV
    linkedAccountTarget <- (LinkedAccount += linkedAccountSource.copy(userId = targetUser.id))
    // match the target user attempting to update set active=false
    target <- User if target.id === targetUser.id
  } yield linkedAccountTarget).transactionally
  db.run(action)
}

UPDATE 我已经走到这一步了,但是我遇到了最后一个编译器错误,基本上是想将 targetUser.id 分配给 sourceUser.id[= 的链接帐户结果23=]

def merge(targetUser: UserRow, sourceUser: UserRow) : Future[Unit] = {
  // define an update DBIOAction to deactivate sourceUser
  val updateAction = User.filter(_.id === sourceUser.id).update(sourceUser.copy(active = false))

  // selects all linkedAccount from sourceUser but yield the userId of the targetUser
  val selectAction = (for {
    linkedAccount <- LinkedAccount
    user <- User if user.id === sourceUser.id && user.id === linkedAccount.userId
  } yield (targetUser.id, linkedAccount.providerKey, linkedAccount.providerPassword, linkedAccount.modified)).result

  // define an insert DBIOAction to insert all the selected linked accounts from sourceUser to targetUser
  val insertAction = selectAction.flatMap(LinkedAccount ++= _)

  // combine both actions using >> and transactionally
  db.run((updateAction >> insertAction).transactionally).map(_ => ())
}

和最后一个错误,我尝试在不同的地方做 .as[LinkedAccountRow] 但它不喜欢它:

[error] /home/bravegag/code/play-authenticate-usage-scala/app/dao/UserDao.scala:101: type mismatch;
[error]  found   : Seq[(Long, String, String, Option[java.sql.Timestamp])]
[error]  required: Iterable[generated.Tables.LinkedAccountRow]
[error]       val insertAction = selectAction.flatMap(LinkedAccount ++= _)
[error]                                                                 ^

我现在在 phone,但我认为您应该在列元组上使用 <> 运算符而不是 as[LinkedAccountRow]

<> 将列元组和目标对象之间的函数对转换作为两个参数。对于案例 类,这是 apply.tupledunapply 函数。

调用它最合适的地方应该是在 selectAction 理解的 yield 子句中。

如果你只是改变这个,它会不会全部工作:

...
} yield (targetUser.id, linkedAccount.providerKey, linkedAccount.providerPassword, linkedAccount.modified)).result
...

沿着这些方向:

...
} yield (targetUser.id, linkedAccount.providerKey, linkedAccount.providerPassword, linkedAccount.modified)).result
.map(LinkedAcount.tuppled)
...

您还可以使用巧妙的技巧在一次 SQL 操作中执行 INSERT...SELECT。这将模仿您想要的 SQL:

INSERT INTO linked_account(user_id, provider_key, provider_hashed) 
SELECT ${targetUserId}, provider_key, provider_hashed
FROM linked_account 
WHERE user_id=${sourceUserId}

大概是这样的:

LinkedAccount
      .map(acc => (acc.userId, acc.prividerKey, providerHashed))
      .forceInsertQuery(
           for {
                acc <- LinkedAccount if acc.userId === sourceUser.id
            } yield (acc))
            .map(acc => 
                (acc.userId, acc.privderKey, acc.providerHashed))
       )

这个forceInsertQuery显然是这里的关键。您基本上可以在其中包含任何您想要的东西(例如具有多个连接的查询等),只要它最终产生与原始 mapforceInsertQuery 之前的那个)匹配的投影。

补充说明: 因为 forceInsertQuery 里面的内容实际上是一个普通的 Query 你可以这样做预编译。这将是这样的:

// this would need to be obviously created once, perhaps in your DAO
// as member value
private val query = Compiled(sourceUserId: Rep[Long] => (for {
    acc <- LinkedAccount if acc.userId === sourceUserId
} yield (acc))
    .map(acc => 
        (acc.userId, acc.privderKey, acc.providerHashed)
    )
)
...
// and inside your method
LinkedAccount
      .map(acc => (acc.userId, acc.prividerKey, providerHashed))
      .forceInsertQuery( query(sourceUser.id) )