Fixtures

В предыдущем посте я рассказывал об исполняемых конфигах ostrich и не упомянул особых их достоинств, кроме type safety и удобства работы с настройками со стороны приложения. На прошлой неделе я наткнулся на ещё один хороший юзкейс для таких конфигов.

Предположим, мы развёртываем наше приложение в разных окружениях (dev, test, prod), и в некоторых из них было бы здорово иметь в базе некоторые начальные данные для упрощения, например, процесса тестирования. Есть несколько достаточно тривиальных, но не очень удобных способов решения этой проблемы, особенно если загрузка этих начальных данных - часть автоматического развёртывания через CI-сервер. В скриптовых языках, где исполняемые файлы настроек - норма, такие данные (называемые fixtures) часто делаются частью конфига.

С ostrich реализация такой штуки становится делом буквально нескольких строк кода:

Итак, для начала добавим соответствующее поле в наш базовый конфиг, он же конфиг по умолчанию. Мы воспользуемся тем, что scala - функциональный язык, а конфиг в свою очередь - это обычный scala-класс.

1
2
3
4
5
6
7
8
9
10
    class DBConfig {

      ...

      /** Нужно ли сбросить и пересоздать базу? */
      var doReset = false

      /** Код, который нужно исполнить, если doReset == true */
      var fixtures: (DB => Unit) = {db => ()}
    }

По умолчанию, поле fixtures - это ничего не делающая функция. Теперь добавим в код, отвечающий за инициализацию базы, обработку новых полей:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

    class DB(val config: DBConfig) {

      Class.forName(config.driver)

      ...

      transaction {
        if (config.doReset) {
          //drop and create tables
          MySchema.drop
          MySchema.create

          //Run fixtures
          logger.info("Running fixtures")
          config.fixtures(this)
        }
      }
    }

Ну и наконец выставим необходимое значение в поле fixtures в нужных конфигах

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    //myserver-test.scala
    new MyServerConfig {

      ...

      dbConfig.doReset = true
      dbConfig.fixtures = { db =>
        //create test user
        val usr = db.createUser("user@example.com", "password", "ru_RU")

        //set user roles
        db.assignRoleForUser(usr.id, Role.ADMIN)
        db.assignRoleForUser(usr.id, Role.STAFF)
      }
    }

Вот, собственно, и всё: никаких самописных форматов для начальных данных, никаких SQL-файлов, которые нужно чинить при каждом изменении схемы. Мы описываем данные самым естественным для нас способом - в виде высокоуровневого кода, работающего с базой и использующего все возможности нашего приложения (и выбранного persistence-фреймворка). Более того, принцип используемый здесь можно легко применить не только к работе с базой, но и к любым другим частям приложения. Нужен тестовый файл в хитром формате, с которым работает наше приложение? Cоздаём его используя соответствующий API и он будет всегда актуален, даже если формат ещё не устаканился и разрабатывается параллельно с приложением.

В общем, с ostrich гибкость настройки приложения ограничена только фантазией разработчика. Я практически уверен, что это не последний пост, посвящённый этой библиотеке.

Comments