стратегия загрузки связанных сущностей с помощью slick 2

Я использую play 2.3 с slick 2.1.

У меня есть два связанных объекта — Message и User (упрощенный пример домена). Сообщения пишут пользователи. Рекомендуемый способ (единственный способ?) выражения такого отношения — использование явного userId в Message

Мои классы и сопоставления таблиц выглядят так:

case class Message (
    text: String,
    userId: Int,
    date: Timestamp = new Timestamp(new Date().getTime()),
    id: Option[Int] = None) {}

case class User (
    userName: String,
    displayName: String,
    passHash: String,
    creationDate: Timestamp = new Timestamp(new Date().getTime()),
    lastVisitDate: Option[Timestamp] = None,
    // etc
    id: Option[Int] = None){}

class MessageTable(tag: Tag) extends Table[Message](tag, "messages") {
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
    def text = column[String]("text")
    def userId = column[Int]("user_id")
    def date = column[Timestamp]("creation_date")

    def * = (title, text, userId, date, id.?) <> (Post.tupled, Post.unapply 
    def author = foreignKey("message_user_fk", userId, TableQuery[UserTable])(_.id)
}

class UserTable(tag: Tag) extends Table[User](tag, "USER") {
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
    def username = column[String]("username")
    def passHash = column[String]("password")
    def displayname = column[String]("display_name")

    def * = (username, passHash,created, lastvisit, ..., id.?) <> (User.tupled, User.unapply)
}

И удобный вспомогательный объект:

object db {
    object users extends TableQuery(new UserTable(_)) {
        // helper methods, compiled queries
    }
    object messages extends TableQuery(new MessageTable(_)) {
        // helper methods, compiled queries
    }
}

Теперь все это идеально внутренне, но если я хочу отобразить фактическое сообщение, я хочу, чтобы класс сообщения мог возвращать свое имя автора при использовании в шаблонах.

Вот мои соображения:

  1. Я не хочу (и я в любом случае не мог) обходить неявное пятно Session там, где ему не место - например, шаблонный движок и классы моделей.
  2. Я хотел бы избежать запроса дополнительных данных для сообщений один за другим в этом конкретном случае

Я больше знаком с Hibernate, чем со Slick; в Hibernate я бы использовал выборку соединения. С Slick лучшая идея, которую я придумал, — использовать другой класс держателя данных для отображения:

class LoadedMessage (user:User, message:Message) {
    // getters
    def text = message.text
    def date = message.date
    def author = user.displayName
}
object LoadedMessage {
    def apply( u:User , m:Message ) = new LoadedMessage(u, m)
}

и заполните его результатами запроса на соединение:

val messageList: List[LoadedMessage] = (
    for (
        u <- db.users;
        m <- db.messages if u.id === m.userId
    ) yield (u, m))
    .sortBy({ case (u, m) => m.date })
    .drop(n)
    .take(amount)
    .list.map { case (u, m) => LoadedMessage(u, m) }

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

Каков общий подход? Есть ли способ сократить дополнительные классы и действительно сделать модель способной возвращать свои ассоциации?


person enlait    schedule 06.07.2014    source источник
comment
Только одно соображение: Hibernate — это ORM, а Slick — это слой, который позволяет вам писать безопасные запросы, вы не можете ожидать тех же функций, и в этом случае, в частности, вы не можете ожидать, что Slick будет извлекать объекты для вас, если вы не укажете, что в запросе с использованием соединений. Если вам больше нравятся ORM, взгляните, например, на SQueryl.   -  person Ende Neu    schedule 06.07.2014
comment
Мне не нравится писать запросы или соединять таблицы. Меня больше смущает, как эффективно представлять результаты   -  person enlait    schedule 06.07.2014
comment
Спасибо за подсказку, я тоже посмотрю на Squeryl. Я только начал играться с Play, и почти весь стек незнаком.   -  person enlait    schedule 06.07.2014


Ответы (1)


После моего комментария:

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

Два других подхода, которые приходят на ум, это

  1. запрашивать поля вместо объектов, это менее разборчиво, и я обычно ненавижу работать с кортежами (или тройками в этом случае), потому что я нахожу нотацию _._1 менее разборчивой, чем MyClass.myField. Это означает сделать что-то вроде этого:

    val messageList = (
      for (
        u <- db.users;
        m <- db.messages if u.id === m.userId
      ) yield (u.displayName, m.date, m.text))
      .sortBy({ case (name, date, text) => date })
      .drop(n)
      .take(amount)
      .list()
    

    Который вернет тройку, а затем может быть передан на ваш взгляд, что возможно, но я бы никогда этого не сделал.

  2. Другой вариант - передать кортеж объектов, очень похожий на ваш подход, за исключением последнего map

    val messageList: List[(User, Message)] = (
      for (
        u <- db.users;
        m <- db.messages if u.id === m.userId
      ) yield (u, m))
      .sortBy({ case (u, m) => m.date })
      .drop(n)
      .take(amount)
      .list()
    

    Здесь вы можете передать в представлении что-то вроде этого:

    @(messagesAndAuthors: List[(User, Message)])
    

    а затем получить доступ к данным, используя функции доступа к кортежам и классам, но снова ваш шаблон будет набором _1, и снова это ужасно читать, плюс вам нужно сделать что-то вроде messagesAndAuthors._1.name только для того, чтобы получить одно значение.

В конце концов, я предпочитаю передавать переменные настолько чисто, насколько это возможно в моих представлениях (и я полагаю, что это один из немногих общепринятых принципов в информатике) и скрывать логику, используемую в моделях. путь на мой взгляд. Кейс-классы — очень мощный инструмент, и приятно иметь его, для вас он может выглядеть не СУХИМ и более многословным, чем другие подходы (например, Hibernate, о котором вы говорили), но подумайте, что когда разработчик будет читать ваш код, это будет так же просто, как он может попасть в этот момент, и этот дополнительный маленький класс сэкономит часы ударов головой.

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

Примечание: код не проверен, возможны ошибки.

person Ende Neu    schedule 06.07.2014
comment
Я тоже использую play-slick, так что неявный Session в контроллере не представляет особых проблем. Моя первоначальная попытка состояла в том, чтобы реализовать ленивую загрузку ассоциаций, но мне не очень удалось передать неявные Session в представлениях. Теперь, когда я думаю об этом, это была ужасная идея. Я понимаю, что с play и slick все должно быть загружено, прежде чем я передам его во вьюху. - person enlait; 06.07.2014
comment
Что касается вашего второго подхода - я также могу подумать о том, чтобы сделать что-то вроде простого case class MessageWithAuthor (u:User, m:Message){}, который содержит меньше кода, но доступ к нему не такой красивый. Все же лучше, чем кортежи, но я боюсь, что это может закончиться адом, как MessageWithAuthor, MessageWithResponses, MessageWithParent и т. д. Или, может быть, сделать MessageWithAllRelatedStuff со связанными вещами Optional - но тогда я потеряю проверку времени компиляции, что я действительно загружаю вещи, которые я использовать. - person enlait; 06.07.2014
comment
Вы можете использовать класс case, который инкапсулирует User и Message, но это будет не более DRYier, чем ваш предыдущий подход, вместо этого доступ будет более подробным (что-то вроде MessageWithAuthor.u.name). Если вы действительно беспокоитесь о том, чтобы получить класс case для каждой ситуации, вы должны использовать кортежи, в конце концов, это компромисс, если вам нужно 3 или 4 класса case для выполнения работы, это то, что я бы сделал, для меня удобочитаемость важнее, чем DRYness (до определенной длины), но опять же, если ваш вариант использования имеет все возможные комбинации, кортежи - это путь. - person Ende Neu; 06.07.2014
comment
Я попробовал это с кортежами; это не так уж и плохо, так как я могу разложить их по шаблонам с помощью для понимания и сопоставления. В конце концов, имеет смысл использовать кортежи для странных комбинаций и классы для обычных. - person enlait; 06.07.2014