Какой хороший способ совместить нумерацию страниц и группировку без запроса в Slick 3.0?

Для упрощения скажем, у меня есть три таблицы:

val postTable = TableQuery[Posts]
val postTagTable = TableQuery[PostTags]
val tagTable = TableQuery[Tags]

У одного сообщения может быть несколько тегов, а postTagTable содержит только отношение.

Теперь я мог запросить сообщения и теги следующим образом:

val query = for {
    post <- postTable
    postTag <- postTagTable if post.id === postTag.postId
    tag <- tagTable if postTag.tagId === tag.id
} yield (post, tag)

val postTags = db.run(query.result).map {
    case result: Seq[(Post,Tag)] => 
        result.groupBy(_._1).map {
            case (post, postTagSeq) => (post, postTagSeq.map(_._2))
        }
}

Что дало бы мне Future[Seq[(Post, Seq(Tag))]].

Все идет нормально.

Но что, если я хочу добавить нумерацию страниц для сообщений? Так как один Post может иметь несколько Tags с приведенным выше запросом, я не знаю, сколько строк нужно take из запроса, чтобы получить, скажем, 10 Posts.

Кто-нибудь знает хороший способ получить тот же результат с определенным количеством сообщений в одном запросе?

На самом деле я даже не уверен, как бы я подошел к этому в собственном SQL без вложенных запросов, поэтому, если у кого-то есть предложение в этом направлении, я также был бы рад его услышать.

Спасибо!

ИЗМЕНИТЬ

Просто чтобы вы знали, какой запрос я сейчас делаю:

val pageQuery = postTable drop(page * pageSize) take(pageSize)

val query = for {
    pagePost <- pageQuery
    post <- postTable if pagePost.id === post.id
    postTag <- postTagTable if post.id === postTag.postId
    tag <- tagTable if postTag.tagId === tag.id
} yield (post, tag)

val postTags = db.run(query.result).map {
    case result: Seq[(Post,Tag)] => 
        result.groupBy(_._1).map {
            case (post, postTagSeq) => (post, postTagSeq.map(_._2))
        }
}

Но это, очевидно, приводит к вложенному запросу. А вот этого хотелось бы избежать.

ИЗМЕНИТЬ 2

Другое решение с двумя запросами, которое было бы возможно:

val pageQuery = postTable drop(page * pageSize) map(_.id) take(pageSize)

db.run(pageQuery.result) flatMap {
    case ids: Seq[Int] => 
        val query = for {
            post <- postTable if post.id inSetBind ids
            postTag <- postTagTable if post.id === postTag.postId
            tag <- tagTable if postTag.tagId === tag.id
        } yield (post, tag)

        val postTags = db.run(query.result).map {
                case result: Seq[(Post,Tag)] => 
                     result.groupBy(_._1).map {
                         case (post, postTagSeq) => (post, postTagSeq.map(_._2))
                     }
        }
 }

Но это потребует двух обращений к базе данных и использования оператора in, поэтому, вероятно, это не так хорошо, как запрос на соединение.

Какие-либо предложения?


person thwiegan    schedule 14.08.2015    source источник
comment
groupBy Слика в этом случае не помогает? Если вы сделаете groupBy по почте по запросу, а затем take?   -  person User    schedule 15.08.2015
comment
Если я делаю groupBy для запроса, мне нужно использовать карту для агрегирования всего, что я не сгруппировал. Поэтому, если бы я группировал сообщения (по запросу), я не мог бы получить теги как список, а только, например. посчитай их.   -  person thwiegan    schedule 15.08.2015


Ответы (2)


Вы можете сделать так:

  def findPagination(from: Int, to: Int): Future[Seq[(Post, Seq[Tag])]] = {
    val query:DBIO[Seq[(Album,Seq[Genre])]] = postRepository.findAll(from, to).flatMap{posts=>
      DBIO.sequence(
        posts.map{ post=>
          tagRepository.findByPostId(post.id).map(tags=>(post,tags))
        }
      )
    }
    db.run(query)
  }

Внутри PostRepository

def findAll(from: Int, limit: Int): DBIO[Seq[Post]] = postTable.drop(from).take(limit).result

Внутри TagRepository

  def findByPostId(id: Int): DBIO[Seq[Tag]] = {
    val query = for {
      tag <- tagTable
      pstTag <- postTagTable if pstTag.postId === id && tag.id === pstTag.tagId
    } yield tag
    query.result
  }

ИЗМЕНИТЬ

Я думаю, вы не можете сделать это без подзапроса в одном запросе. Ваше текущее решение является лучшим. Также вы можете оптимизировать свой запрос, удалив ненужное «соединение».

val query = for {
    pagePost <- pageQuery
    postTag <- postTagTable if pagePost.id === postTag.postId
    tag <- tagTable if postTag.tagId === tag.id
} yield (pagePost, tag)

И вы получите примерно следующий SQL (Slick 3.0.1):

SELECT x2.`postname`,
       x2.`id`,
       x3.`tagname`,
       x3.`id`
FROM
  (SELECT x4.`postname` AS `postname`, x4.`id` AS `id`
   FROM `POST` x4 LIMIT 10, 1) x2,
     `POST_TAG` x5,
     `TAG` x3
WHERE (x2.`id` = x5.`postId`)
  AND (x5.`tagId` = x3.`id`)

Возможно, в вашем случае более эффективно предварительно скомпилировать этот запрос http://slick.typesafe.com/doc/3.0.0/queries.html#compiled-queries

person Igor Mielientiev    schedule 15.08.2015
comment
Но даже если он попадет в базу данных только один раз, это приведет к n+1 операторам (где n — это pageSize), не так ли? - person thwiegan; 15.08.2015
comment
Да, вы получаете n+1 обращений к базе данных. 1 хит для получения постов с разбивкой на страницы и n для получения тегов для каждого поста - person Igor Mielientiev; 15.08.2015
comment
Итак, вы избегаете вложенного запроса, но сделать пару обращений к базе данных еще хуже. - person thwiegan; 15.08.2015

У меня такая же проблема. Может быть еще интересно:

        val query = for {
      ((p, pt), t) <- posts.filter({x => x.userid === userId}).sortBy({x=>x.createdate.desc}).take(count).
                      joinLeft (postsTags).on((x, y)=>x.postid === y.postid).
                      joinLeft (tags).on(_._2.map(_.tagid) === _.tagid)
    } yield (p.?, t.map(_.?))
    //val query = posts filter({x => x.userid === userId}) sortBy({x=>x.createdate.desc}) take(count)
    try db.run(query result)
    catch{
      case ex:Exception => {
        log error("ex:", ex)
        Future[Seq[(Option[PostsRow], Option[Option[TagsRow]])]] {
          Seq((None, None))
        }
      }
    }

затем результат этого запроса:

result onComplete {
case Success(x) => {
  x groupBy { x => x._1 } map {x=>(x._1, x._2.map(_._2))}

}
case Failure(err) => {
  log error s"$err"
}

}

он возвращает последовательность следующим образом: Seq[(Post, Seq[Tag]), (Post, Seq[Tag])........]

person Luger    schedule 15.03.2016