Tagged: Groovy, Grails and GORM

Human readable past date formating in Grails

Sometimes it better to write ‘2 days ago’ instad of ’28 Jan 2015′.
You can find a lot of solution in How to calculate “time ago” in Java?.

Here is a simplest snippet for making it in Grails from my project with i18n support.
Just create new tag `timeAgo` in your project taglib:

    static encodeAsForTags = [ timeAgo: [taglib:'html'] ]

    final static long ONE_SECOND = 1000;
    final static long ONE_MINUTE = ONE_SECOND * 60;
    final static long ONE_HOUR = ONE_MINUTE * 60;
    final static long ONE_DAY = ONE_HOUR * 24;

    /**
     Converts time (in milliseconds) to human-readable format
     "<w> days, <x> hours, <y> minutes ago" or "just now"
     <code>
     System.out.println(millisToLongDHMS(123));
     System.out.println(millisToLongDHMS((5 * ONE_SECOND) + 123));
     System.out.println(millisToLongDHMS(ONE_DAY + ONE_HOUR));
     System.out.println(millisToLongDHMS(ONE_DAY + 2 * ONE_SECOND));
     System.out.println(millisToLongDHMS(ONE_DAY + ONE_HOUR + (2 * ONE_MINUTE)));
     System.out.println(millisToLongDHMS((4 * ONE_DAY) + (3 * ONE_HOUR) + (2 * ONE_MINUTE) + ONE_SECOND));
     System.out.println(millisToLongDHMS(42 * ONE_DAY));
     </code>
     output :
     0 second
     5 seconds
     1 day, 1 hour
     1 day and 2 seconds
     1 day, 1 hour, 2 minutes
     4 days, 3 hours, 2 minutes
     42 days
     */
    private String millisToLongDHMS(long duration) {
        StringBuffer res = new StringBuffer();
        long temp;
        if (duration >= ONE_MINUTE) {
            temp = duration / ONE_DAY;
            if (temp > 0) {
                duration -= temp * ONE_DAY;
                res.append(temp).append(' ').append(temp > 1 ? g.message(code: 'timeAgo.days') : g.message(code: 'timeAgo.day')).append(duration >= ONE_MINUTE ? ', ' : '')
            }
            temp = duration / ONE_HOUR;
            if (temp > 0) {
                duration -= temp * ONE_HOUR;
                res.append(temp).append(' ').append(temp > 1 ? g.message(code: 'timeAgo.hours') : g.message(code: 'timeAgo.hour')).append(duration >= ONE_MINUTE ? ', ' : '')
            }
            temp = duration / ONE_MINUTE
            if (temp > 0) {
                res.append(temp).append(' ').append(temp > 1 ? g.message(code: 'timeAgo.minutes') : g.message(code: 'timeAgo.minute'))
            }
            res.append(' ').append(g.message(code: 'timeAgo.ago'))
            return res.toString()
        } else {
            return g.message(code: 'timeAgo.justNow')
        }
    }

    /**
     * @emptyTag
     *
     * @attr date the date in past
     */
    def timeAgo = { attrs ->
        Date date = attrs.date
        out << millisToLongDHMS((new Date().getTime()) - date.getTime());
    }

And then you need to add message codes in `messages.properties`:

timeAgo.justNow=just now
timeAgo.minute=minute
timeAgo.minutes=minutes
timeAgo.hour=hour
timeAgo.hours=hours
timeAgo.day=day
timeAgo.days=days
timeAgo.ago=ago
# for Russian
timeAgo.justNow=только что
timeAgo.minute=минуту
timeAgo.minutes=минут
timeAgo.hour=час
timeAgo.hours=часов
timeAgo.day=день
timeAgo.days=дней
timeAgo.ago=назад
# for Ukrainian
timeAgo.justNow=тіки що
timeAgo.minute=хвилину
timeAgo.minutes=хвилин
timeAgo.hour=годину
timeAgo.hours=годин
timeAgo.day=день
timeAgo.days=днів
timeAgo.ago=тому

«Comparing JVM Web Frameworks» by Matt Raible

It’s very cool talk about qestions that every Java developer must decide on new project. Overview of all popular Java frameworks from JSF and Struts to Play and Vaadin.

[Grails] Избегайте использования Environment вне файлов конфигураций

Внутри Grails есть великолепный механизм для условного выполнения кода в зависимости от текущей среды (Environment).
Например внутри DataSource.groovy можно указывать различные настройки базы данных:

// environment specific settings
environments {
    development {
        dataSource {
            dbCreate = "create-drop"
            url = "jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
        }
    }
    production {
        dataSource {
            dbCreate = "update"
            url = "jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
        }
    }
}

Наверняка у вас почти в каждом файле конфигурации есть настройки под среду.

Но очень часто я натыкался на то что класс Environment.current начинает использоваться внутри контроллеров, вьюх и сервисов. А стандартный тег if и вовсе имеет атрибут для проверки текущего окружения:

<g:if env="test"> ... </g:if>

Я постепенно пришёл к тому что этого следует избегать, потому что теряется читаемость и гибкость. Вместо этого лучше явно создать опцию в Config.groovy, включать или выключать её в зависимости от среды а и потом проверять её. Вот например что делает этот код?

    <g:if env="test">
        <meta name="controller" content="${controllerName}"/>
        <meta name="action" content="${actionName}"/>
    </g:if>

На самом деле этот код добавляет имя контроллера и экшена которые отрисовли страницу и они потом проверяются функциональным тестом. Это это бывает очень удобно для дебага.
А теперь давайте мы создадим в файле Config.groovy опцию

environments {
    development {
        com.example.showActionNameInPageMeta = true
    }
    test {
        com.example.showActionNameInPageMeta = true
    }
    production {
        com.example.showActionNameInPageMeta = false
    }
}

и теперь

    <g:if test="${grailsApplication.config.com.example.showActionNameInPageMeta}">
        <meta name="controller" content="${controllerName}"/>
        <meta name="action" content="${actionName}"/>
    </g:if>

В результате эта опция улучшила читаемость кода. Кроме того теперь мы всегда можем добавить новое окружение, например testFunctional для функциональных тестов и внутри включить нашу опцию, при этом не переписывая код.
И что особенно важно, что в случае каких то проблем в продакшене сисадмин может поменять конфигурацию и перезапустить сервер не перекомпилируя приложение.
Кроме того, все такие опции теперь не размазаны по коду а лежат в одном месте где их все можно посмотреть.

В более широком смысле такой подход называется Feature flag, и его активно используют например в Amazon.

[Grails] favicon.ico и robots.txt

Есть две важные мелочи на которые обычно внимания не обращаем, но потом приходится всё таки с ними повозится.

Где должна быть иконка для закладки?

Например favicon.ico — очень важная маленькая вещь. Иконка значительно увеличивает узнаваемость закладки.
По умолчанию в main layout который генерирует Grails есть прописанный путь к favicon:

<html>
<head>
    ...
    <link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon">
...
</head>

Броузер должен понять что фавикон лежит по URL /static/images/favicon.ico

Оказалось что броузеры не любят когда фавикон находится не в корне сайта, т.е. не /favicon.ico. Например если сайт загружен через фрейм или просто открывается картинка с сайта то некоторые броузеры пытаются запросить фавикон с корня сайта.

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

Как решать?

Первое что приходит на ум это прописать в UrlMappings перенаправление на правильный путь к иконке. Но конечно же не стоит этого делать, ведь мы помним что всё что лежит в папке web-app становится доступным по прямо ссылке.
Так что нам достаточно просто перенести фавикон в директорию уровнем выше:

mv grails-app/web-app/images/favicon.ico grails-app/web-app/favicon.ico

И ещё подправить наш лейаут grails-app/views/layouts/main.gsp

<html>
<head>
    ...
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
...
</head>

Теперь фавкион доступен прямо из корня сайта /favicon.ico.
На форумах пишут что на мобильные устройства ещё запрашивают свои иконки в другом разрешении, но у меня сейчас по рукой их нет чтобы проверить.

Защищаемся от поисковика

Наверное многие помнят историю когда все SMS отправленные с сайта Билайна стали доступны через поисковик яндекса. Это произошло по стечению обстоятельств, одним из которых было то что разработчики не запретили поисковикам индексировать страницы отправки СМС.
Чтобы себя уберечь вам следует тоже запретить поисковикам индексировтаь админку вашего сайта или другие приватные разделы.
Для этого в корень вашего сайта вы должны разметсить файл robots.txt с примерно таким содержимым:

User-agent: *
Disallow: /
Allow: /faq/

Поисковики когда будут индексировать ваш сайт будут соблюдать эти правила и не будут индексировать весь ваш сайт кроме страницы /faq/

Как не сложно догадаться файл robots.txt тоже нужно положить в web-app чтобы он стал доступен по прямой ссылке.

Если вам уже не достаточно простых настроек robots.txt то вам стоит обратить внимание на его XML развитие Sitemaps.
Для Grails даже есть плагин grails-sitemapper, но он выглядит заброшенным так что десять раз перепроверьте его содержимое.

По хорошему эти две вещи нужно сделать частью стандартного Grails. Как нибудь создам тикет в трекере на эту тему.

[Grails] SEQUENCE generator name

Если у доменного объекта не указан способ генерации айдишника тогда Hibernate подбирает его сам. Это называется native стратегия.

Если вы используете не уродскую MySQL, а настоящую базу данных типа PostgreSQL, Oracle или Firebird, то генерация айдишников будет происходить через SEQUENCE.

При генерации БД название для сиквенсов генерируется автоматически и выглядят они примерно так: SYSTEM_SEQUENCE_99A7416D_98E6_485C_8568_1DD6C99BF31C. Т.е. нейминг стратегия мягко говоря неадекватная.
Из-за неё возникает несколько проблем.

Во первых из названия такого сиквенса не понятно для какой именно он таблицы.

Во вторых, я сталкивался с проблемой когда при следующем релизе через database migration накатываются изменения и возникает необходимость сбросить сиквенс, то не ясно по какому имени к нему обращаться.

Как решать?

Переопределить нейминг

Первый вариант создать свой диалект внутри которого прописать правильную генерацию имени сиквенса.
По уму стоило бы всё таки пропатчить эту паскудную генерацию имени сиквенсов в самом хибернейте. Ставлю пивашку тому кто это сделает 🙂

Указать имя сиквенса явно

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

class Book {
    String title

    static mapping = {
        id generator: 'sequence', params: [sequence: 'book_seq']
    }
}

Теперь для таблицы book будет создан сиквенс book_seq.

Естественно, если вы вдруг решите запустить приложение на БД в которой нет сикенсов то отгребёте ошибку типа такой org.hibernate.MappingException: org.hibernate.dialect.MySQL5InnoDBDialect does not support sequences.

Так что такое решение не годится для использования если вам не известная используемая БД, например в плагинах.

Правильное решение

Я сначала подумал что на этом тупик, но пролистав документацию хибернейта дальше обнаружил что решение уже есть.
Если вкратце, то начиная с версии 3.2.3 в хибернейте появился SequenceStyleGenerator который в отличии от нейтив, позволяет указать опции одновременно и для SEQUENCE генератора и для Table не падает с ошибкой на старте.

class Book {
    String title

    static mapping = {
        id generator: 'org.hibernate.id.enhanced.SequenceStyleGenerator', params: [sequence_name: 'book_seq']
    }
}

В документации сказано что ещё следует включить новые генераторы. Для этого в DataSource.groovy нужно добавить опцию

hibernate.id.new_generator_mappings = true

Но если честно, то всё и без этого заработало 🙂

Теперь в MySql рядом с таблицей book создалась таблица book_seq с одной строкой и одним полем next_val которая имитирует сиквенс.

[Grails] i18n: LocaleResolver, Accept-Language

Warning!!! I made plugin grails-locale-configuration-plugin that replace this solution. [Grails] Сегодня опубликовал стабильную версию grails-locale-configuration-plugin
Also this chapter of book may be interest for you

В каждом броузере пользователь может настроить предпочтительные языки. Например в хроме Settings / Languages (chrome://settings/languages) они выглядят так:
chrome languages settings
Тут указано что пользователь хочет видеть американский английский, или любой английский, но если его не будет тогда на русском, а если и на русском не будет, тогда давайте уже на украинском.

Эти параметры передаются в каждом запросе на сервер через заголовок Accept-Language:

Accept-Language:en-US,en;q=0.8,ru;q=0.6,uk;q=0.4

С помощью параметра q (quality value) мы передаём приоритет от 0 до 1.

Grails умеет работать с этим заголовком и автоматически переключать локаль. Если есть интернационализация для этого языка то она сразу же автоматически показывается и всё хорошо. А вот если не находится i18n/messages_xx.properties для нужной локали, тогда отображается текст по умолчанию из i18n/messages.properties, обычно английский.

Хоть текст и будет английский, но системная локаль будет установлена в ту которую пользователь запросил больше всего.
Например, у нас есть пользователь у которого украинский язык на первом месте и русский на втором.
Запросив страницу Grails запомнит украинскую локаль в сессии, но отобразит всё на английском.
Это не всегда хорошо, особенно если ваш сайт жёстко поддерживает только несколько локалей. Например в зависимости от локали вы разные флажки отображаете.
Скорее всего что захотите ограничить локали которые поддерживаете.
Для этого создайте в файле Config.groovy опцию supportedLocales:

supportedLocales = [Locale.ENGLISH, new Locale('RU')]

А теперь создадим фильтр который будет проверять локаль

class LocaleResolverFilterFilters {

    def filters = {
        all(controller: '*', action: '*') {
            before = {
                // Сначала ищем такую же локаль, если не нашли то локаль с тем же языком, если не нашли то по умолчанию английский
                Locale selectedLocale
                LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request)
                List&amp;amp;amp;lt;Locale&amp;amp;amp;gt; supportedLocales = grailsApplication.config.supportedLocales
                if (request.locale in supportedLocales) {
                    selectedLocale = request.locale
                } else {
                    selectedLocale = findLocaleWithSameLanguage(request, supportedLocales)
                }
                selectedLocale = selectedLocale ?: Locale.ENGLISH
                localeResolver.setLocale(request, response, selectedLocale)
            }
        }
    }

    private Locale findLocaleWithSameLanguage(HttpServletRequest request, List&amp;amp;amp;lt;Locale&amp;amp;amp;gt; supportedLocales) {
        supportedLocales.find({ it.language == request.locale.language })
    }
}

В дальнейшем из кода получить локаль мы можем через объект запроса request.locale а список всех локалей предпочитаемых пользователем через request.locales.
Ту локаль с которой мы отрисовали страницу можно увидеть через объект ответа: response.locale.
Смотрите демо приложение на гитхабе.

Grails request.locale

Что ещё почитать по теме:

[Grails] toString() method on domain class with id in output

Groovy has a cool annotation @ToString.
But I found strange behavior when trying to include id field to output of toString().
For example we have some domain class that marked with @ToString:

@ToString(includeNames = true, includeFields = true,  includes = ['id'])
class User {
...
}

def user = new User()
user.id = 1 // id can't be bonded as a param of constructor, that's why we should set it directly
// Let's test generated toString()!
assert == 'User(id:1)' // assert fails!

But this assertion will fail: toString() skips id field. Maybe it happens because of id field is not declared explicitly in User class. It added implicitly by Grails.
I found workaround — just declare id field explicitly:

@ToString(includeNames = true, includeFields = true,  includes = ['id'])
class User {
    Long id
...
}

def user = new User()
user.id = 1
assert == 'User(id:1)' // assert successful 🙂

I dont know is this a bug or feature, but hope this workaround will help you.

[Grails] Параметры пейджинации как модель и простая фильтрация

Допустим есть простой доменный класс:

class User {
    String email
    String name
    Date dateCreated
}

Сгенерируем контроллер, и посмотрим на list() action который выгребает из БД всех пользователей.

class UserController {
...
    def list() {
        respond User.list(), model:[userInstanceCount: User.count()]
    }
...
}

Посмотрим что получилось в броузере:
Grails 2.3. list action

Что если мы хотим добавить пейджинацию? Очень просто, GORM метод list() имеет встроенную поддержку пейджинации. Например, у нас есть постраничный список пользователей. На каждой странице отображается по десять. Мы хотим выбрать третью страницу отсортированных по имени в восходящем порядке:

def books = User.list(offset: 20, max: 10, sort: 'name', order: 'asc')

offset — это сколько записей пропустить, max — сколько записей выбрать.
Тег добавляет пейджинатор который передаёт эти параметры в параметрах GET запроса. От туда мы их сразу передаём в list():

class UserController {
...
    def list() {
        respond User.list(params), model:[userInstanceCount: User.count()]
    }
...
}

Тут могут возникнуть неприятности: хакер может задать очень большой max и такими большими запросами к БД положить сайт. Для этого нужно ограничивать max:

class UserController {
...
    def list() {
        params.max = Math.min(params.max ?: 10, 100)
        respond User.list(params), model:[userInstanceCount: User.count()]
    }
...
}

Хитрая конструкция Math.min(params.max ?: 10, 100) делает следующее:
Если max не задан, устанавливаем его по умолчанию в десять.
Если max задан больше 100, то он будет установлен в сто.
Простая защита для корректирования параметра max.

Если у вас контроллером уже много то эта строчка начинает повторятся. Мелочь, но коробит, DRY. Во вторых, на моём текущем проекте высокие требования по безопасности, и все параметры из запросов мы обязательно оборачиваем в Command object где их строго типизируем.
Так что даже на такую мелочь было решено написать простенький команд обжект:

@Validateable
class ListParams {

    public static final int MAX_DEFAULT = 10
    public static final int MAX_HIGH_LIMIT = 100

    Integer max = MAX_DEFAULT
    Integer offset
    String sort
    String order

    static constraints = {
        max(nullable: true, max: MAX_HIGH_LIMIT)
        offset(nullable: true, min: 0)
        sort(nullable: true, blank: false)
        order(nullable: true, blank: false, inList: ['asc', 'desc'])
    }

    Map <String, Object> getParams() {
        return [max: correctMax, offset: offset, sort: sort, order: order]
    }

    Integer getCorrectMax() {
        return Math.min(max ?: MAX_DEFAULT, MAX_HIGH_LIMIT)
    }
}

И принимаем его аргументом в list():

    def index(ListParams listParams) {
        respond User.list(listParams.params), model:[userInstanceCount: User.count()]
    }

Стоило ли оно того? Наверное, да. Любая типизация и абстракция позволяет нам лучше протестировать и сделать более безопасное приложение.
Самое интересное начинается далее, когда вам начинают быть нужными фильтры. В таком случае можно создать простой класс UserListFilter отнаследованный от ListParams и добавляющий свои поля. Например на форме фильтра у нас есть ещё поле поиска по email, по имени и

@Validateable
class UserListFilter extends ListParams {
    String email
    String name
    Date dateCreatedFrom
    Date dateCreatedTo
}

И принимать сразу его через аргумент в контроллере, где Grails автоматически всё сконвертирует, сбиндит и провалидирует:

class UserController {
...
    def index(UserListFilter filter) {
        def criteria = User.where {
            if (filter.email) {
                email == filter.email
            }
            if (filter.name) {
                name =~ '%' + filter.name + '%'
            }
            if (filter.dateCreatedFrom) {
                dateCreated >= filter.dateCreatedFrom
            }
            if (filter.dateCreatedTo) {
                dateCreated <= filter.dateCreatedTo
            }
        }
        respond criteria.list(filter.params), model: [userInstanceCount: criteria.count(), filter: filter]
    }
...
}

Результат:

Такой подход сократит вам много кода, и сделает его более безопасным и объектно ориентированным.
Пример демо проекта я выложил на гитхаб.

Software Environments

Программа может работать в разных условиях (environments), например у программиста на компьютере, где Windows и пару гибибайт оперативки, или на реальном «производственном» (production) сервере, с настоящей БД, с миллионом пользователей, мощным железом и под каким нибудь FreeBSD.
Соответсвенно на уровне конфигурации и кода нужно учитывать такие разные Environments.
Например если произошла ошибка то на компьютере программиста (development environment) мы вываливаем ему весь stacktrace, а вот на работающем сайте (production) мы пишем все ошибки только в логи, а перед пользователями извеняемся.

Так вот, какие чаще всего нужны Environments?
К сожалению я нигде в интернете не нашёл хорошего объяснения какие environments нужны и как их конфигурировать. Очень много и хорошо описано в документации к Grails но там описанные только самые базовые случаи.
Ещё немного описано в Википедии.
Поэтому постараюсь описать их все исходя из моего опыта.

Development, dev

Компьютер программиста
БД создаётся in-memory и каждый раз удаляется при выключении программы.
Пример конфигурации:

development {
    dataSource {
        url = 'jdbc:h2:mem:devDb' // Драйвер БД: H2, база в памяти, называние БД devDb
        dbCreate = 'create'       // Создать схему БД автоматически
        loggingSql = true         // Логировать SQL запросы
    }
    log4j = {
        all 'grails.app'  // Логируем всё из пакетов нашего приложения           
        root {
            all 'stdout'  // Выводим только в консоль
        }
    }
}

Здесь опция dbCreate = ‘create’ указывает что БД будет каждый раз снова создаваться снова.
прямо в оперативной памяти. При выходе из программы она будет уничтожатся.

More on dbCreate
Hibernate can automatically create the database tables required for your domain model. You have some control over when and how it does this through the dbCreate property, which can take these values:
create — Drops the existing schemaCreates the schema on startup, dropping existing tables, indexes, etc. first.
create-drop — Same as create, but also drops the tables when the application shuts down cleanly.
update — Creates missing tables and indexes, and updates the current schema without dropping any tables or data. Note that this can’t properly handle many schema changes like column renames (you’re left with the old column containing the existing data).
validate — Makes no changes to your database. Compares the configuration with the existing database schema and reports warnings.
Можно играться этими опциями.
Например если вы не хотите чтобы данные очищались, а просто обновилась схема то ставьте update и сохраняйте БД в файле.
Если у вас уже production БД то ставьте только validate а саму миграцию БД делайте с помощью SQL скриптов через инструменты DBMaintain или LiquidBase.

Test

Для прогона автоматических тестов.
Тут стоит отметить что тесты бывают:

  • модульные (unit) — им не нужно никаких конфигураций Environments по определению
  • интеграционные (integration, i11n) — вот для них обычно уже нужна БД а то и полностью стартовать сервер.
  • функциональные (functional) — это когда имитируются действия пользователя, и проверяется что все кнопочки работают как должны.

Т.е. environment нужен для integration и functional и в идеале разный.

Production, prod

Настоящий сайт.
Тут стоит отметить что здесь нельзя хранить настоящие пароли от БД. Я бы рекомендовал просто указать имя DSN, например для JNDI или ODBC.
Логирование нужно настроить в файл и на email. А вот в консоль логировать бессмысленно.

production {
    grails.serverURL = 'http://greenpay.com/'
    portMapper.httpPort = 80
    portMapper.httpsPort = 443
    dataSource {
        jndiName = "java:comp/env/myDataSource"
    }
    log4j = {
        warn 'grails.app'
        root {
            warn 'main', 'smtp'
            additivity = true
        }
    }
}

Staging, stg

Это тестовый сервер с железом, конфигурацией и данными максимально приближёнными к production.
Обычно данные берутся скриптом с реального сервера, и только персональная информация (пароли, email) перетираются тестовыми в целях безопасности.
На staging проводят нагрузочное тестирование и проверяются что реальные данные не поломают приложение. Например может оказатся что у вас в реальной БД есть пользователь с именем длинней чем размер поля в новой версии БД.

QA

Сборка программы для QA инженеров (тестировщиков).
Не путать с test environment — то для автоматических тестов. Автотесты не могут поймать все дефекты, особенно связанные с внешним видом.
Тут можно выделить два случая QA…

QA dev

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

QA release

За пару недель перед релизом новая разработка замораживается (code freeze) или уже ведётся в других ветках кода для следующего релиза.
За это время тестировщики делают регрессионное тестирование где проверяют что старый функционал работает как и прежде и ничего не поломалось.

HEAD

Continuous Integration, CI — это сервер который автоматически собирает ваш проект после каждого изменения кода (pull). HEAD — это последняя версия кода, т.е.
head environment это версия приложения максимально соответствующая последнему состоянию кода.
Зачем это нужно? Ну если у вас в команде несколько человек то бывает удобно когда есть общий доступный сервер на котором быстренько можно что-то глянуть.
Т.е. похоже на QA но обновляется чаще и имеет право содержать ошибки.

Demo, Acceptance

Демо версия для сдачи итерации заказчику. Этот сервер должен быть доступен заказчику из интернета.
Всё должно быть сделано так чтобы не произошло «демо эффекта» когда ты показываешь заказчику новый функционал и вдруг он выдаёт ошибку.
Поэтому БД всегда создаётся новая. Презентационные данные должны быть красивыми и заранее оттестированными и отрепетированными.
Известные баги нужно обходить стороной. Заказчику главное увидеть как оно работает, а то что там ещё есть баги это не так важно. Важными они станут только в ветке QA release.

Это самые базовые случаи разных сред и конфигураций которые я видел. Разумеется их может быть больше. На это влияет структура проекта, процесс разработки и особенности серверов.
А вообще это вроде как относится к понятию Configuration Managment, но как я вижу на практике этим термином просто называют инструменты Chef и Puppet которые хранят конфигурацию как код.

Буду рад услышать если вы можете дополнить эту информацию или подскажете где можно почитать.
Расскажите как у вас настроено на проекте 🙂 Спасибо.

UPD Также почитайте мой совет Избегайте использования Environment вне файлов конфигураций