Scala Slick Cake Pattern: более 9000 классов?
Я разрабатываю Play! 2.2 приложения в Scala с Slick 2.0, и я сейчас занимаюсь аспектом доступа к данным, пытаясь использовать Cake Pattern. Это кажется многообещающим, но я действительно чувствую, что мне нужно написать огромную кучу классов / черт / объектов, чтобы достичь чего-то действительно простого. Так что я мог бы использовать немного света на этом.
Взяв очень простой пример с User
Концепция, как я понимаю, мы должны иметь:
case class User(...) //model
class Users extends Table[User]... //Slick Table
object users extends TableQuery[Users] { //Slick Query
//custom queries
}
Пока это абсолютно разумно. Теперь мы добавляем "Cake Patternable" UserRepository
:
trait UserRepository {
val userRepo: UserRepository
class UserRepositoryImpl {
//Here I can do some stuff with slick
def findByName(name: String) = {
users.withFilter(_.name === name).list
}
}
}
Тогда у нас есть UserService
:
trait UserService {
this: UserRepository =>
val userService: UserService
class UserServiceImpl { //
def findByName(name: String) = {
userRepo.findByName(name)
}
}
}
Теперь мы смешиваем все это в объекте:
object UserModule extends UserService with UserRepository {
val userRepo = new UserRepositoryImpl
val userService = new UserServiceImpl
}
Является
UserRepository
действительно полезно? Я мог бы написатьfindByName
как пользовательский запрос вUsers
гладкий объект.Допустим, у меня есть другой набор классов, как это для
Customer
и мне нужно использовать некоторыеUserService
особенности в нем.
Я должен делать:
CustomerService {
this: UserService =>
...
}
или же
CustomerService {
val userService = UserModule.userService
...
}
2 ответа
ОК, это звучит как хорошие цели:
- Аннотация над библиотекой базы данных (пятно, ...)
- Сделайте тестируемую единицу признаков
Вы могли бы сделать что-то вроде этого:
trait UserRepository {
type User
def findByName(name: String): User
}
// Implementation using Slick
trait SlickUserRepository extends UserRepository {
case class User()
def findByName(name: String) = {
// Slick code
}
}
// Implementation using Rough
trait RoughUserRepository extends UserRepository {
case class User()
def findByName(name: String) = {
// Rough code
}
}
Тогда для CustomerRepository
Вы могли бы сделать:
trait CustomerRepository { this: UserRepository =>
}
trait SlickCustomerRepository extends CustomerRepository {
}
trait RoughCustomerRepository extends CustomerRepository {
}
И объедините их, основываясь на своих прихотях:
object UserModuleWithSlick
extends SlickUserRepository
with SlickCustomerRepository
object UserModuleWithRough
extends RoughUserRepository
with RoughCustomerRepository
Вы можете создавать объекты для тестирования юнитов, например, так:
object CustomerRepositoryTest extends CustomerRepository with UserRepository {
type User = // some mock type
def findByName(name: String) = {
// some mock code
}
}
Вы правильно заметили, что существует сильное сходство между
trait CustomerRepository { this: UserRepository =>
}
object Module extends UserRepository with CustomerRepository
а также
trait CustomerRepository {
val userRepository: UserRepository
import userRepository._
}
object UserModule extends UserRepository
object CustomerModule extends CustomerRepository {
val userRepository: UserModule.type = UserModule
}
Это старый компромисс между наследованием и агрегацией, обновленный для мира Scala. У каждого подхода есть свои преимущества и недостатки. Благодаря смешанным чертам вы будете создавать меньше конкретных объектов, которые легче отслеживать (как показано выше, у вас есть только один Module
объект, а не отдельные объекты для пользователей и клиентов). С другой стороны, черты должны быть смешаны во время создания объекта, поэтому вы не можете, например, взять существующий UserRepository
и сделать CustomerRepository
смешивая это - если вам нужно это сделать, вы должны использовать агрегацию. Также обратите внимание, что при агрегации часто требуется указывать одноэлементные типы, как указано выше (: UserModule.type
) для того, чтобы Scala признал, что зависимые от пути типы одинаковы. Еще одна сила, которой обладают черты смешения, заключается в том, что они могут обрабатывать рекурсивные зависимости - как UserModule
и CustomerModule
может предоставить что-то и требовать что-то друг от друга. Это также возможно с агрегацией с использованием ленивых значений, но это более синтаксически удобно с чертами смешивания.
Ознакомьтесь с моей недавно опубликованной шпаргалкой по архитектуре Slick. Он не абстрагируется от драйвера базы данных, но тривиально изменить его таким образом. Просто заверните это в
class Profile(profile: JdbcProfile){
import profile.simple._
lazy val db = ...
// <- cheat sheet code here
}
Вам не нужен рисунок торта. Просто поместите все это в один файл, и вы обойдетесь без него. Шаблон торт позволяет разбить код на разные файлы, если вы готовы платить за синтаксис. Люди также используют шаблон тортов для создания различных конфигураций, включая различные комбинации сервисов, но я не думаю, что это имеет отношение к вам.
Если повторяющиеся синтаксические издержки для таблицы базы данных вас беспокоят, сгенерируйте код. Генератор кода Slick настраивается именно для этой цели:
Если вы хотите смешать рукописный и сгенерированный код, либо подайте рукописный код в генератор кода, либо используйте схему, где сгенерированный код наследуется от рукописного наоборот.
Чтобы заменить Slick чем-то другим, замените методы DAO запросами, использующими другую библиотеку.