Статьи

Скриптовые отчеты с Groovy

Groovy стал моим любимым языком сценариев, и в этом блоге я рассматриваю некоторые функции Groovy, которые делают его особенно привлекательным для представления текстовых отчетов. В этом посте будет показано, как с помощью Groovy можно легко представить пользовательские текстовые отчеты о данных, хранящихся в базе данных. По пути я выделю несколько привлекательных особенностей Groovy.

Я использую Oracle Database 11g Express Edition ( XE ) для источника данных в моем примере в этом посте, но можно использовать любой источник данных. В этом примере используется отличная поддержка Groovy для SQL / JDBC и используется образец схемы Oracle ( HR ). Визуальное описание этой примерной схемы доступно в документации примера схемы .

Мой пример использования Groovy для написания сценария создания отчетов включает извлечение данных из образца схемы Oracle HR и представление этих данных с помощью текстового отчета. Одна часть скрипта должна получить эти данные из базы данных, и Groovy добавляет только минимальную церемонию в оператор SQL, необходимый для этого. В следующем фрагменте кода из сценария показано использование многострочного GString Groovy для указания строки запроса SQL в удобном для пользователя формате и для обработки результатов этого запроса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def employeeQueryStr =
'''SELECT e.employee_id, e.first_name, e.last_name,
          e.email, e.phone_number,
          e.hire_date, e.job_id, j.job_title,
          e.salary, e.commission_pct, e.manager_id,
          e.department_id, d.department_name,
          m.first_name AS mgr_first_name, m.last_name AS mgr_last_name
     FROM employees e, departments d, jobs j, employees m
    WHERE e.department_id = d.department_id
      AND e.job_id = j.job_id
      AND e.manager_id = m.employee_id(+)'''
 
def employees = new TreeMap<Long, Employee>()
import groovy.sql.Sql
def sql = Sql.newInstance('jdbc:oracle:thin:@localhost:1521:xe', 'hr', 'hr',
                          'oracle.jdbc.pool.OracleDataSource')
sql.eachRow(employeeQueryStr)
{
   def employeeId = it.employee_id as Long
   def employee = new Employee(employeeId, it.first_name, it.last_name,
                               it.email, it.phone_number,
                               it.hire_date, it.job_id, it.job_title,
                               it.salary, it.commission_pct, it.manager_id as Long,
                               it.department_id as Long, it.department_name,
                               it.mgr_first_name, it.mgr_last_name)
   employees.put(employeeId, employee)
}

Приведенный выше код Groovy добавляет небольшой объем кода поверх оператора SQL SQL. Указанный оператор SELECT объединяет несколько таблиц и также включает внешнее объединение (внешнее объединение необходимо для включения президента в результаты запроса, несмотря на то, что в этой позиции нет руководителя). Подавляющее большинство первой части кода — это оператор SQL, который можно запустить как есть в SQL * Plus или SQL Developer . Нет необходимости в подробном обнаружении исключений и обработке результирующих наборов с поддержкой SQL в Groovy !

В приведенном выше фрагменте кода есть и другие специфичные для Groovy преимущества. Обратите внимание, что оператор import для импорта groovy.sql.Sql был разрешен при необходимости и не должен находиться в верхней части файла сценария. В примере также использовались Sql.newInstance и Sql.eachRow (GString, Closure) . Последний метод позволяет легко применить замыкание к результатам запроса. Специальное слово it является именем по умолчанию для элементов, обрабатываемых в замыкании. В этом случае it можно представить как строку в наборе результатов. К значениям в каждой строке обращаются по именам столбцов базовой базы данных (или псевдонимам в случае mgr_first_name и mgr_last_name ).

Одним из преимуществ Groovy является его бесшовная интеграция с Java. Приведенный выше фрагмент кода также продемонстрировал это с помощью использования Groovy TreeMap , что является преимуществом, поскольку означает, что новые экземпляры Employee, размещенные на карте на основе данных, извлеченных из базы данных, всегда будут доступны в порядке идентификатора сотрудника.

В приведенном выше коде информация, извлеченная из базы данных и обработанная посредством замыкания, сохраняется для каждой строки в недавно созданном объекте Employee . Этот объект Employee предоставляет другое место, чтобы показать краткость Groovy и показан далее.

Employee.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@groovy.transform.Canonical
class Employee
{
   Long employeeId
   String firstName
   String lastName
   String emailAddress
   String phone_number
   Date hireDate
   String jobId
   String jobTitle
   BigDecimal salary
   BigDecimal commissionPercentage
   Long managerId
   Long departmentId
   String departmentName
   String managerFirstName
   String managerLastName
}

Список кода, который только что показан, представляет собой весь класс! Поддержка свойств Groovy делает методы получения / установки автоматически доступными для всех определенных атрибутов класса. Как я уже говорил в предыдущем сообщении в блоге , аннотация @Canonical представляет собой Groovy AST (преобразование), которое автоматически создает несколько полезных общих методов для этого класса [ equals(Object) , hashCode() и toString() ]. Явного конструктора не существует, потому что @Canonical также обрабатывает это, предоставляя конструктор, который принимает аргументы этого класса в порядке, указанном в их объявлениях. Трудно представить себе сценарий, в котором было бы легче легко и быстро создать объект для хранения полученных значений данных в сценарии.

Драйвер JDBC необходим для этого сценария для извлечения этих данных из Oracle Database XE, и JAR для этого драйвера может быть указан в пути к классам при запуске сценария Groovy. Тем не менее, мне нравится, чтобы мои скрипты были настолько автономными, насколько это возможно, и это делает механизм загрузки корневых путей в Groovy привлекательным. Это может быть использовано внутри этого скрипта (вместо того, чтобы указывать его внешне при вызове скрипта), как показано ниже:

1
2
this.class.classLoader.rootLoader.addURL(
   new URL('file:///C:/oraclexe/app/oracle/product/11.2.0/server/jdbc/lib/ojdbc6.jar'))

Дополнительное примечание. Другим изящным подходом для доступа к соответствующему зависимому JAR или библиотеке является использование аннотации Groovy, предоставляемой Grape @Grab . Я не использовал это здесь, потому что JAR JDBC Oracle не доступен ни в каких законных центральных репозиториях Maven, о которых мне известно. Пример использования этого подхода, когда в общедоступном репозитории Maven доступна зависимость, приведен в моем блоге « Простое внедрение Groovy Logger и защита журнала» .

Поскольку данные извлекаются из базы данных и помещаются в коллекцию простых объектов Groovy, созданных для хранения этих данных и обеспечения легкого доступа к ним, почти пора начинать представлять эти данные в текстовом отчете. Некоторые константы, определенные в скрипте, показаны в следующем фрагменте кода скрипта.

1
2
3
4
5
6
7
8
int TOTAL_WIDTH = 120
String HEADER_ROW_SEPARATOR = '='.multiply(TOTAL_WIDTH)
String ROW_SEPARATOR = '-'.multiply(TOTAL_WIDTH)
String COLUMN_SEPARATOR = '|'
int COLUMN_SEPARATOR_SIZE = COLUMN_SEPARATOR.size()
int COLUMN_WIDTH = 22
int TOTAL_NUM_COLUMNS = 5
int BALANCE_COLUMN_WIDTH = TOTAL_WIDTH-(TOTAL_NUM_COLUMNS-1)*COLUMN_WIDTH-COLUMN_SEPARATOR_SIZE*(TOTAL_NUM_COLUMNS-1)-2

Декларация констант, только что показанная, иллюстрирует больше преимуществ Groovy. С одной стороны, константы являются статически типизированными, демонстрируя гибкость Groovy в определении типов как статически, так и динамически. Еще одна особенность Groovy, на которую стоит обратить особое внимание в последнем фрагменте кода, — это использование метода String.multiply (Number) в литеральных строках. Все, даже строки и числа, являются объектами в Groovy. Метод multiply позволяет легко создать строку из этого числа того же повторяющегося символа.

Первая часть текстового вывода — это заголовок. Следующие строки скрипта Groovy записывают эту информацию заголовка в стандартный вывод.

1
2
3
4
5
6
7
8
9
println '\n\n${HEADER_ROW_SEPARATOR}'
println '${COLUMN_SEPARATOR}${'HR SCHEMA EMPLOYEES'.center(TOTAL_WIDTH-2*COLUMN_SEPARATOR_SIZE)}${COLUMN_SEPARATOR}'
println HEADER_ROW_SEPARATOR
print '${COLUMN_SEPARATOR}${'EMPLOYEE ID/HIRE DATE'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
print '${'EMPLOYEE NAME'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
print '${'TITLE/DEPARTMENT'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
print '${'SALARY INFO'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
println '${'CONTACT INFO'.center(BALANCE_COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
println HEADER_ROW_SEPARATOR

Приведенный выше код демонстрирует некоторые захватывающие функции Groovy. Один из моих любимых аспектов поддержки GString в Groovy — это возможность использовать Ant-подобные выражения ${} для предоставления исполняемого кода, встроенного в String. Приведенный выше код также демонстрирует поддержку Groovy GDK String для метода center (Number), который автоматически центрирует данную строку в соответствии с указанным количеством символов. Это мощная функция для удобного написания привлекательного текста.

С данными, извлеченными и доступными в нашей структуре данных, и с определенными константами, часть вывода может начинаться. Следующий фрагмент кода показывает использование стандартных коллекций Groovy для each метода, чтобы разрешить итерацию по ранее заполненному TreeMap с закрытием, примененным к каждой итерации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
employees.each
{ id, employee ->
   // first line in each output row
   def idStr = id as String
   print '${COLUMN_SEPARATOR}${idStr.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   def employeeName = employee.firstName + ' ' + employee.lastName
   print '${employeeName.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   def jobTitle = employee.jobTitle.replace('Vice President', 'VP').replace('Assistant', 'Asst').replace('Representative', 'Rep')
   print '${jobTitle.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   def salary = '$' + (employee.salary as String)
   print '${salary.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   println '${employee.phone_number.center(BALANCE_COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
 
   // second line in each output row
   print '${COLUMN_SEPARATOR}${employee.hireDate.getDateString().center(COLUMN_WIDTH)}'
   def managerName = employee.managerFirstName ? 'Mgr: ${employee.managerFirstName[0]}. ${employee.managerLastName}' : 'Answers to No One'
   print '${COLUMN_SEPARATOR}${managerName.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   print '${employee.departmentName.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   String commissionPercentage = employee.commissionPercentage ?: 'No Commission'
   print '${commissionPercentage.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   println '${employee.emailAddress.center(BALANCE_COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   println ROW_SEPARATOR
}

В последнем фрагменте кода данные, полученные из базы данных, выводятся в относительно привлекательном текстовом формате. В примере показано, как можно назвать дескрипторы в замыкании, чтобы они были более осмысленными. В этом случае они называются id и employee и представляют ключ ( Long ) и значение ( Employee ) каждой записи в TreeMap .

В последнем фрагменте кода есть и другие функции Groovy, о которых стоит упомянуть. Представление комиссии использует оператор Элвиса Groovy ( ?: :), Который делает даже условный троичный вид Java многословным. В этом примере, если процент вознаграждения сотрудника соответствует стандартам Groovy, то используется этот процент; в противном случае печатается «Без комиссии».

Обработка даты проката дает еще одну возможность рекламировать преимущества Groovy GDK. В этом случае Groovy GDK Date.getDateString () используется для легкого доступа к только для даты части класса Date (время не желательно для даты проката) без явного использования средства форматирования String. Ницца!

Последний пример кода также демонстрирует использование ключевого слова as для приведения (приведения) переменных более читабельным образом, а также демонстрирует большее использование возможностей Java, в этом случае используется метод Java String replace (CharSequence, CharSequence) . Однако в этом примере Groovy снова добавляет доброту String. В этом примере демонстрируется, что Groovy поддерживает извлечение только первой буквы имени менеджера с использованием нотации (массива) индекса ( [0] ), чтобы получить только первый символ из строки.

До сих пор в этом посте я демонстрировал фрагменты всего сценария, а также объяснял различные функции Groovy, которые демонстрируются в каждом фрагменте. Далее показан весь сценарий, и за этим списком кода следует снимок экрана с отображением вывода при выполнении сценария. Полный код класса Groovy Employee был показан ранее.

generateReport.groovy: полный скрипт

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env groovy
 
// Add JDBC driver to classpath as part of this script's bootstrapping.
// WARNING: This location needs to be adjusted for specific user environment.
this.class.classLoader.rootLoader.addURL(
   new URL('file:///C:/oraclexe/app/oracle/product/11.2.0/server/jdbc/lib/ojdbc6.jar'))
 
int TOTAL_WIDTH = 120
String HEADER_ROW_SEPARATOR = '='.multiply(TOTAL_WIDTH)
String ROW_SEPARATOR = '-'.multiply(TOTAL_WIDTH)
String COLUMN_SEPARATOR = '|'
int COLUMN_SEPARATOR_SIZE = COLUMN_SEPARATOR.size()
int COLUMN_WIDTH = 22
int TOTAL_NUM_COLUMNS = 5
int BALANCE_COLUMN_WIDTH = TOTAL_WIDTH-(TOTAL_NUM_COLUMNS-1)*COLUMN_WIDTH-COLUMN_SEPARATOR_SIZE*(TOTAL_NUM_COLUMNS-1)-2
 
// Get instance of Groovy's Sql class
import groovy.sql.Sql
def sql = Sql.newInstance('jdbc:oracle:thin:@localhost:1521:xe', 'hr', 'hr',
                          'oracle.jdbc.pool.OracleDataSource')
 
def employeeQueryStr =
'''SELECT e.employee_id, e.first_name, e.last_name,
          e.email, e.phone_number,
          e.hire_date, e.job_id, j.job_title,
          e.salary, e.commission_pct, e.manager_id,
          e.department_id, d.department_name,
          m.first_name AS mgr_first_name, m.last_name AS mgr_last_name
     FROM employees e, departments d, jobs j, employees m
    WHERE e.department_id = d.department_id
      AND e.job_id = j.job_id
      AND e.manager_id = m.employee_id(+)'''
 
def employees = new TreeMap<Long, Employee>()
sql.eachRow(employeeQueryStr)
{
   def employeeId = it.employee_id as Long
   def employee = new Employee(employeeId, it.first_name, it.last_name,
                               it.email, it.phone_number,
                               it.hire_date, it.job_id, it.job_title,
                               it.salary, it.commission_pct, it.manager_id as Long,
                               it.department_id as Long, it.department_name,
                               it.mgr_first_name, it.mgr_last_name)
   employees.put(employeeId, employee)
}
 
println '\n\n${HEADER_ROW_SEPARATOR}'
println '${COLUMN_SEPARATOR}${'HR SCHEMA EMPLOYEES'.center(TOTAL_WIDTH-2*COLUMN_SEPARATOR_SIZE)}${COLUMN_SEPARATOR}'
println HEADER_ROW_SEPARATOR
print '${COLUMN_SEPARATOR}${'EMPLOYEE ID/HIRE DATE'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
print '${'EMPLOYEE NAME'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
print '${'TITLE/DEPARTMENT'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
print '${'SALARY INFO'.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
println '${'CONTACT INFO'.center(BALANCE_COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
println HEADER_ROW_SEPARATOR
 
employees.each
{ id, employee ->
   // first line in each row
   def idStr = id as String
   print '${COLUMN_SEPARATOR}${idStr.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   def employeeName = employee.firstName + ' ' + employee.lastName
   print '${employeeName.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   def jobTitle = employee.jobTitle.replace('Vice President', 'VP').replace('Assistant', 'Asst').replace('Representative', 'Rep')
   print '${jobTitle.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   def salary = '$' + (employee.salary as String)
   print '${salary.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   println '${employee.phone_number.center(BALANCE_COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
 
   // second line in each row
   print '${COLUMN_SEPARATOR}${employee.hireDate.getDateString().center(COLUMN_WIDTH)}'
   def managerName = employee.managerFirstName ? 'Mgr: ${employee.managerFirstName[0]}. ${employee.managerLastName}' : 'Answers to No One'
   print '${COLUMN_SEPARATOR}${managerName.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   print '${employee.departmentName.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   String commissionPercentage = employee.commissionPercentage ?: 'No Commission'
   print '${commissionPercentage.center(COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   println '${employee.emailAddress.center(BALANCE_COLUMN_WIDTH)}${COLUMN_SEPARATOR}'
   println ROW_SEPARATOR
}

В этом посте я попытался показать, как Groovy предоставляет множество функций и другую поддержку синтаксиса, которые облегчают написание сценариев для создания удобочитаемых и относительно привлекательных результатов. Для более общих скриптов Groovy, которые обеспечивают поддержку вывода текста, см. Форматирование простых табличных текстовых данных . Хотя это хорошие общие решения, цель моего поста состояла в том, чтобы показать, что это легко и не занимает много времени для написания настраиваемых сценариев для генерации нестандартного вывода текста с помощью Groovy. Небольшие элементы Groovy, такие как простое центрирование строки, простое преобразование даты в строку, извлечение любого нужного символа из строки на основе записи положения массива и легкий доступ к данным базы данных, делают Groovy мощным инструментом для создания текстовых отчетов.

Ссылка: Отчеты по сценарию с Groovy от нашего партнера JCG Дастина Маркса в блоге Inspired by Actual Events .