Squeryl: Двигаемся дальше

Итак, продолжаем наше знакомство со Sueryl (начало тут). Для начала, вернёмся к селектам и рассмотрим одну из интереснейших фич Squeryl.

Составные селекты

Итак, select в любой его форме возвращает нам объект класса Query (кроме lookup, возвращающего Option). Этот класс играет сразу две роли. Во-первых, из него можно просто получить результат выборки, используя его в качестве обычной коллекции (он расширяет трейт Iterable). А во-вторых (и это самое интересное) - его можно использовать для построения более сложных запросов к базе. Например, подсунуть его в from вместо таблицы:

1
2
3
4
5
6
7
8
9

    val rated =
      users.where( _.rating.~ >= 0)

    val vasyasRated =
      from(rated) ( u =>
        select(u)
        where(u.name like "%Vasya%")
      )

В этом примере мы использовали запрос rated для построения второго запроса. (Кстати, в первом посте я не упомянул о маленьком но полезном кусочке синтаксического сахара: упрощённом синтаксисе для select. Пример его использования можно увидеть в первом запросе.)

Можно и использовать результат одного селекта внутри блока where второго:

1
2
3
4
5
6
7
    val vasyasRated =
      from(users) ( u =>
        select(u)
        where(u.id in
          from(rated) (r => select(r.id))
        )
    )

Ну и, в конце концов можно добавить к Query разные полезные модификаторы. Для постраничной выборки:

1
    from(users) ( u => ... ).page(offset, pageLength)

для выборки уникальных строк:

1
    from(users) ( u => ... ).distinct

или для выборки элементов для обновления (директива FOR UPDATE в SQL):

1
2
    from(users) ( u => ... ).forUpdate

Всё это, при желании, даёт возможность писать код максимально соответствующий принципу DRY (“don’t repeat yourself”, если вдруг кто не знает).

Агрегация

Агрегация - это, наверное, единственная часть DSL, где Squeryl отходит от SQL: вместо использования select, агрегирующие функции описываются в конструкии compute.

1
2
3
4
5
6
7
8
9
    val ratingDistribution =
      from(users) ( u =>
        groupBy(u.rating)
        compute(count(u.id))
      )

    ratingDistribution foreach { r=>
      println("%s: %s" format (r.key, r.measures))
    }

Результатом такого кода становится объект вида Query[ GroupWithMeasures[Int, Int] ], который можно использовать явно, обращаясь к полям key и measures, либо приведя его к коллекции типа Map, вызовом метода toMap.

Join

С джойнами тоже всё довольно просто. Обычный INNER JOIN так же как и в SQL можно делать неявно, просто сделав select из двух таблиц и добавив условие в where:

1
2
3
4
5
    from(users, posts) ( (u,p) =>
      select(u.name, p)
      where(u.id === p.userId)
    )

Можно использовать OUTER JOIN и всё, к чему вы привыкли в SQL:

1
2
3
4
5
    from(users, avatars.leftOuter) ( (u,a) =>
      select(u, a.url)
      on(u.id === a.map(_.userId))
    )

В случае использования внешнего джойна, для юзера не всегда находится соответствующая аватарка, поэтому a в этом примере имеет тип Option.

Relations

Зачастую, одна из самых сложных для понимания и использования вещей в обычных ORM - отношения one-to-many и many-to-many между таблицами. Именно там кроется большая часть граблей, на которые наступают неопытные разработчики.

Подобно scala collections, имеющим две ипостаси: immutable и mutable, отношения в Squeryl делятся на stateless, являющиеся, по-сути, заранее подготовленными запросами (Query) и stateful хранящими все связанные записи в памяти в виде коллекции (Iterable).

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

1
2
3
4
5
6
7
8
9
10
11

    object MySchema extends Schema {
      val users = table[User]
      val posts = table[Post]

      val userPosts =
        oneToManyRelation(users, posts) via ( (u,p) =>
          u.id === p.userId
        )
    }

В этом посте я возьму для примера отношения one-to-many. DSL для описания many-to-many не сильно от него отличается и хорошо описан в документации.

1
2
3
4
5
6
7
8
9
    case class User (....) extends KeyedEntity[Long] {
      //OneToMany[Post] < Query
      lazy val posts = MySchema.userPosts.left(this)
    }

    case class User (....) extends KeyedEntity[Long] {
      //ManyToOne[User] < Query
      lazy val user = MySchema.userPosts.right(this)
    }

Теперь мы можем использовать наш релейшен так же как и обычный select.

1
2
3
    for (p <- user.posts)
      println(p.title)

Кроме этого, stateless relations имеют несколько полезных методов для манипулирования дочерними элементами:

  • assign - привязывает дочерний элемент родительскому, выставляя значение foreign key (имеет смысл только если foreign key - изменяемое поле)
  • associate - делает assign + сохраняет эту связь в базе
  • deleteAll - удаляет все дочерние элементы из базы

Объявление stateful relation не сильно сложнее:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    case class User (....) extends KeyedEntity[Long] {

      //StatefulOneToMany[Post] < Iterable[Post]
      lazy val posts = MySchema.userPosts.leftStateful(this)

    }

    case User (....) extends KeyedEntity[Long] {

      //StetefulManyToOne[User]
      lazy val user = MySchema.userPosts.rightStateful(this)

    }

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

Недостатки

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

Один раз, когда мне надо было вынести один из часто используемых паттернов для апдейтов в отдельный метод, магия DSL отказалась работать и мне пришлось расковырять библиотеку и извлечь на свет страннейший артефакт чёрной магии под названием (осторожно): createEqualityExpressionWithLastAccessedFieldReferenceAndConstant. Вам страшно? Мне тоже. Хорошо что такой случай был пока один, и у вас есть все шансы не напороться даже на него (уж больно он специфический).

А вот с казалось бы элементарными, но не всегда работающими implicit conversions, являющимися неотъемлемой частью любого скаловского DSL, дело обстоит хуже. Приведу один только пример. В этом кусочке условия в where я пытаюсь использовать ещё одну замечательную фичу - динамический запрос (то есть запрос, части которого включаются по условию, обычно при заполнении Option, подробнее тут):

1
2
    where ( u.flag === boolOpt.? )

Очевидно, здесь DSL вступает в конфликт с синтаксическим сахаром для булевских условий (поскольку для строковых полей всё работает). Перепробовав несколько вариантов, я пришёл к вот такому вот монструозному решению:

1
2
3
4
    not(not(u.flag)).inhibitWhen(boolOpt != Some(true)) 
    and
    not(u.flag).inhibitWhen(boolOpt != Some(false))

Обратите внимание на изящный двойной not, который пришлось использовать в качестве подсказки компилятору. Судя по всему, я оказался не единственным, кто столкнулся с этой проблемой: на StackOveflow мне сообщили что в следующей версии этот сахар будет выключен и посоветовали более изящное решение с применением уже знакомого нам оператора ~:

1
2
    where ( u.flag === boolOpt.~.? )

Заключение

Несмотря на некоторые недостатки, умолчать о которых было бы просто нечестно, Squeryl - замечательная библиотека, позволяющая решить кучу типичных проблем при использовании реляционных БД, не увеличивая сложность и размер кода. Лично я буду использовать её и дальше, что и вам советую делать.

Эти два поста, посвящённые squeryl были в некотором роде заготовкой для выступления на нашем Scala-евенте (хотя это ещё вопрос, что вышло полуфабрикатом, а что полноценным туториалом). Видео моего (и не только) выступления можно найти в небольшом отчёте с него, а слайды у меня на дропбоксе (кавайные белочки инсайде). Удачи и до встреч.

Comments