Scala Слик: проблемы с groupBy и отсутствующие фигуры

Я пытаюсь использовать Slick для запроса отношения "многие ко многим", но я сталкиваюсь с различными ошибками, наиболее заметным из которых является "Не знаю, как распаковать (User, Skill) в T и пакет в G".

Структура таблиц выглядит следующим образом:

case class User(val name: String, val picture: Option[URL], val id: Option[UUID])
object Users extends Table[User]("users") {
  def name = column[String]("name")
  def picture = column[Option[URL]]("picture")
  def id = column[UUID]("id")
  def * = name ~ picture ~ id.? <> (User, User.unapply _)
}

case class Skill(val name: String, val id: Option[UUID])
object Skills extends Table[Skill]("skill") {
  def name = column[String]("name")
  def id = column[UUID]("id")
  def * = name ~ id.? <> (Skill, Skill.unapply _)
}

case class UserSkill(val userId: UUID, val skillId: UUID, val id: Option[UUID])
object UserSkills extends Table[UserSkill]("user_skill") {
  def userId = column[UUID]("userId")
  def skillId = column[UUID]("skillId")
  def id = column[UUID]("id")
  def * = userId ~ skillId ~ id.? <> (UserSkill, UserSkill.unapply _)
  def user = foreignKey("userFK", userId, Users)(_.id)
  def skill = foreignKey("skillFK", skillId, Skills)(_.id)
}

В конечном счете, то, что я хочу достичь, является чем-то вроде

SELECT u.*, group_concat(s.name) FROM user_skill us, users u, skills s WHERE us.skillId = s.id && us.userId = u.id GROUP BY u.id

но прежде чем тратить время на то, чтобы работать с group_concat, я пытался создать более простой запрос (который, как я считаю, по-прежнему действителен...)

SELECT u.* FROM user_skill us, users u, skills s WHERE us.skillId = s.id && us.userId = u.id GROUP BY u.id

Я попытался использовать целый ряд scala для создания этого запроса, но пример того, что вызывает ошибку формы выше,

(for {
  us <- UserSkills
  user <- us.user
  skill <- us.skill
} yield (user, skill)).groupBy(_._1.id).map { case(_, xs) => xs.first }

Аналогично, следующее сообщение создает ошибку упаковки в отношении "Пользователь" вместо "(Пользователь, умение)"

(for {
  us <- UserSkills
  user <- us.user
  skill <- us.skill
} yield (user, skill)).groupBy(_._1.id).map { case(_, xs) => xs.map(_._1).first }

Если у кого-нибудь есть предложения, я был бы очень благодарен: я потратил большую часть сегодняшнего дня и вчера на то, чтобы очистить группы google/google, а также доступный источник, но у меня пока нет решения.

(Кроме того, я использую postgre, поэтому group_concat на самом деле будет string_agg)

ИЗМЕНИТЬ

Итак, похоже, что когда используется groupBy, применяется преобразованная проекция, потому что что-то вроде

(for {
  us <- UserSkills
  u   <- us.user
  s   <- us.skill
} yield (u,s)).map(_._1)

работает отлично, потому что _._ 1 дает тип Users, у которого есть Shape, так как пользователи являются таблицей. Однако, когда мы вызываем xs.first(как мы это делаем, когда вызываем groupBy), мы фактически возвращаем отображаемый проекционный тип (User, Skill), или если мы сначала применяем карту (_._ 1), получаем тип User, который не является пользователем! Насколько я могу судить, нет формы с пользователем как смешанный тип, потому что единственные фигуры определены для Shape [Column [T], T, Column [T]] и для таблицы T <: TableNode, Shape [ T, NothingContainer # TableNothing, T], как определено в slick.lifted.Shape. Кроме того, если я делаю что-то вроде

(for {
  us <- UserSkills
  u   <- us.user
  s   <- us.skill
} yield (u,s))
  .groupBy(_._1.id)
  .map { case (_, xs) => xs.map(_._1.id).first }

Я получаю странную ошибку формы "NoSuchElementException: key not found: @1515100893", где значение числового ключа меняется каждый раз. Это не тот запрос, который я хочу, но это странная проблема. Тем не менее.

Ответ 1

Попробуйте это. Надеюсь, он может дать то, что вы ожидали. Найдите код Slick ниже классов case.

нажмите здесь для справки относительно снятого вложения.

case class User(val name: String, val picture: Option[URL], val id: Option[UUID])
            class Users(_tableTag: Tag) extends Table[User](_tableTag,"users") {
              def name = column[String]("name")
              def picture = column[Option[URL]]("picture")
              def id = column[UUID]("id")
              def * = name ~ picture ~ id.? <> (User, User.unapply _)
            }
             lazy val userTable = new TableQuery(tag => new Users(tag))

            case class Skill(val name: String, val id: Option[UUID])
            class Skills(_tableTag: Tag) extends Table[Skill](_tableTag,"skill") {
              def name = column[String]("name")
              def id = column[UUID]("id")
              def * = name ~ id.? <> (Skill, Skill.unapply _)
            }
             lazy val skillTable = new TableQuery(tag => new Skills(tag))

            case class UserSkill(val userId: UUID, val skillId: UUID, val id: Option[UUID])
            class UserSkills(_tableTag: Tag) extends Table[UserSkill](_tableTag,"user_skill") {
              def userId = column[UUID]("userId")
              def skillId = column[UUID]("skillId")
              def id = column[UUID]("id")
              def * = userId ~ skillId ~ id.? <> (UserSkill, UserSkill.unapply _)
              def user = foreignKey("userFK", userId, Users)(_.id)
              def skill = foreignKey("skillFK", skillId, Skills)(_.id)
            }
             lazy val userSkillTable = new TableQuery(tag => new UserSkills(tag))






(for {((userSkill, user), skill) <- userSkillTable join userTable.filter on
                    (_.userId === _.id) join skillTable.filter on (_._1.skillId === _.id)
                } yield (userSkill, user, skill)).groupBy(_.2.id)

Ответ 2

Я столкнулся с подобными ситуациями. Хотя мне нравится работать с Scala и Slick, я действительно верю, что есть времена, когда легче денормализовать объект в самой базе данных и привязать таблицу Slick к представлению.

Например, у меня есть приложение, которое имеет объект Tree, который нормализуется в несколько таблиц базы данных. Поскольку мне нравится SQL, я думаю, что это более чистое решение, чем создание простого запроса Scala Slick. Код Scala:

case class DbGFolder(id: String,
                     eTag: String,
                     url: String,
                     iconUrl: String,
                     title: String,
                     owner: String,
                     parents: Option[String],
                     children: Option[String],
                     scions: Option[String],
                     created: LocalDateTime,
                     modified: LocalDateTime)
object DbGFolders extends Table[DbGFolder]("gfolder_view") {
  def id = column[String]("id")
  def eTag = column[String]("e_tag")
  def url = column[String]("url")
  def iconUrl = column[String]("icon_url")
  def title = column[String]("title")
  def owner = column[String]("file_owner")
  def parents = column[String]("parent_str")
  def children = column[String]("child_str")
  def scions = column[String]("scion_str")
  def created = column[LocalDateTime]("created")
  def modified = column[LocalDateTime]("modified")
  def * = id ~ eTag ~ url ~ iconUrl ~ title ~ owner ~ parents.? ~
          children.? ~ scions.? ~ created ~ modified <> (DbGFolder, DbGFolder.unapply _)

  def findAll(implicit s: Session): List[GFolder] = {
    Query(DbGFolders).list().map {v =>
      GFolder(id = v.id,
              eTag = v.eTag,
              url = v.url,
              iconUrl = v.iconUrl,
              title = v.title,
              owner = v.owner,
              parents = v.parents.map { parentStr =>
                parentStr.split(",").toSet }.getOrElse(Set()),
              children = v.children.map{ childStr =>
                childStr.split(",").toSet }.getOrElse(Set()),
              scions = v.scions.map { scionStr =>
                scionStr.split(",").toSet }.getOrElse(Set()),
              created = v.created,
              modified = v.modified)
    }
  }
}

И основное (postgres) представление:

CREATE VIEW scion_view AS
    WITH RECURSIVE scions(id, scion) AS (
      SELECT c.id, c.child
      FROM children AS c
      UNION ALL
      SELECT s.id, c.child
      FROM children AS c, scions AS s
      WHERE c.id = s.scion)
    SELECT * FROM scions ORDER BY id, scion;    

CREATE VIEW gfolder_view AS
  SELECT
    f.id, f.e_tag, f.url, f.icon_url, f.title, m.name, f.file_owner,
    p.parent_str, c.child_str, s.scion_str, f.created, f.modified
  FROM
    gfiles AS f
      JOIN mimes AS m ON (f.mime_type = m.name)
      LEFT JOIN (SELECT DISTINCT id, string_agg(parent, ',' ORDER BY parent) AS parent_str
                 FROM parents GROUP BY id) AS p ON (f.id = p.id)
      LEFT JOIN (SELECT DISTINCT id, string_agg(child, ',' ORDER BY child) AS child_str
                 FROM children GROUP BY id) AS c ON (f.id = c.id)
      LEFT JOIN (SELECT DISTINCT id, string_agg(scion, ',' ORDER BY scion) AS scion_str
                 FROM scion_view GROUP BY id) AS s ON (f.id = s.id)
  WHERE
    m.category = 'folder';