-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51


Создание приложений с использованием Firebird, jOOQ и Spring MVC

Вторник, 27 Июня 2017 г. 09:37 + в цитатник
Всем привет. На этот раз будет описан процесс создания web приложения на языке Java с использованием фреймворка Spring MVC, библиотеки jOOQ и СУБД Firebird.

Для упрощения разработки вы можете воспользоваться одной из распространённых IDE для Java (NetBeans, IntelliJ IDEA, Eclipse, JDeveloper или др.). Лично я использовал NetBeans. Для тестирования и отладки нам так же потребуется установить один и веб-серверов или серверов приложения (Apache Tomcat или Glass Fish) Создаём проект на основе шаблона Maven проекта веб-приложения.


После создания проекта на основе шаблона необходимо преобразовать его структуру папок так чтобы она была корректной для MVC Spring 4. Если проект создавался в среде NetBeans 8.2, то необходимо выполнить следующие шаги:
  1. Удалить файл index.html
  2. Создать папку WEB-INF внутри папки Web Pages
  3. Внутри папки WEB-INF создать папки jsp, jspf и resources
  4. Внутри папки resources создаём папки js и CSS
  5. Внутри папки jsp создаём файл index.jsp


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



В папке WEB-INF/jsp будут размещаться jsp странице, а в папке jspf части страниц, которые будут подключены в другие странице с помощью инструкции



Папка resource предназначена для размещения статических веб ресурсов. В папке WEB-INF/resources/css будут размещаться файлы каскадных таблиц стилей, в папке WEB-INF/resources/fonts – файлы шрифтов, в папке WEB-INF/resources/js – файлы JavaScript и сторонние JavaScript библиотеки.

Теперь поправим файл pom.xml и пропишем в него общие свойства приложения, зависимости от пакетов библиотек (Spring MVC, Jaybird, JDBC пул, JOOQ) и свойства JDBC подключения.

pom.xml


    4.0.0

    ru.ibase
    fbjavaex
    1.0-SNAPSHOT
    war

    Firebird Java Example

    
        ${project.build.directory}/endorsed
        UTF-8
        4.3.4.RELEASE
        1.2
        3.0.1    
        jdbc:firebirdsql://localhost:3050/examples    
        org.firebirdsql.jdbc.FBDriver
        SYSDBA
        masterkey
    
    
    
    
        
            javax
            javaee-web-api
            7.0
            provided
        
        
          
            javax.servlet  
            javax.servlet-api  
            ${javax.servlet.version}  
            provided 
         
         
          
            jstl  
            jstl  
            ${jstl.version}  
           
        
        
        
            com.fasterxml.jackson.core
            jackson-core
            2.8.5
        

        
            com.fasterxml.jackson.core
            jackson-annotations
            2.8.5
        

        
            com.fasterxml.jackson.core
            jackson-databind
            2.8.5
                
                   
        
        
        
        
            org.springframework
            spring-core
            ${spring.version}
        
 
        
            org.springframework
            spring-web
            ${spring.version}
        
 
        
            org.springframework
            spring-webmvc
            ${spring.version}
        
        
        
            org.springframework
            spring-context
            ${spring.version}
        
    
        
            org.springframework
            spring-jdbc
            ${spring.version}
                
        
        
        
        
            org.firebirdsql.jdbc
            jaybird-jdk18
            3.0.0
          
        
          
        
        
            commons-dbcp
            commons-dbcp
            1.4
                  
        
           
                
        
            org.jooq
            jooq
            3.9.2
        
        
        
            org.jooq
            jooq-meta
            3.9.2
        
        
        
            org.jooq
            jooq-codegen
            3.9.2
           
        
        
        
            junit
            junit
            4.11
            jar
            test
        
    
        
            org.springframework
            spring-test
            ${spring.version}
            test
                               
    

                      
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.1
                
                    1.7
                    1.7
                    
                        ${endorsed.dir}
                    
                
            
            
                org.apache.maven.plugins
                maven-war-plugin
                2.3
                
                    false
                
            
            
                org.apache.maven.plugins
                maven-dependency-plugin
                2.6
                
                    
                        validate
                        
                            copy
                        
                        
                            ${endorsed.dir}
                            true
                            
                                
                                    javax
                                    javaee-endorsed-api
                                    7.0
                                    jar
                                
                            
                        
                    
                
            
        
    




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



Мне не очень нравится конфигурирование через xml, поэтому я буду работать через классы конфигурации Java.

WebAppConfig.java
package ru.ibase.fbjavaex.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.List;

@Configuration
@ComponentScan("ru.ibase.fbjavaex")
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List> httpMessageConverters) {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        jsonConverter.setObjectMapper(objectMapper);
        httpMessageConverters.add(jsonConverter);
    }

    @Bean
    public UrlBasedViewResolver setupViewResolver() {
        UrlBasedViewResolver resolver = new UrlBasedViewResolver();
        resolver.setPrefix("/WEB-INF/jsp/");
        resolver.setSuffix(".jsp");
        resolver.setViewClass(JstlView.class);
        return resolver;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/WEB-INF/resources/");
    }
}



В данном конфигурационном классе мы задаём место поиска веб ресурсов и JSP представлений. Метод configureMessageConverters устанавливает, что дата должна сериализоваться в строковое представление (по умолчанию сериализуется в числовом представлении как timestamp).

Теперь избавимся от файла Web.xml вместо него создадим файл WebInitializer.java.

WebInitializer.java
package ru.ibase.fbjavaex.config;

import javax.servlet.ServletContext;  
import javax.servlet.ServletException;  
import javax.servlet.ServletRegistration.Dynamic;  
  
import org.springframework.web.WebApplicationInitializer;  
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;  
import org.springframework.web.servlet.DispatcherServlet;  
  
public class WebInitializer implements WebApplicationInitializer {
    
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {        
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();  
        ctx.register(WebAppConfig.class);  
        ctx.setServletContext(servletContext);    
        Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));  
        servlet.addMapping("/");  
        servlet.setLoadOnStartup(1);
    }
    
}



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

Генерации классов для работы с базой данных через jOOQ



Работу с базой данных будем вести с помощью библиотеки jOOQ. jOOQ позволяет строить SQL запросы из объектов jOOQ и кода (наподобие LINQ). jOOQ имеет более тесную интеграцию с базой данных, чем ORM, поэтому кроме простых CRUD SQL запросов используемых в Active Record, позволяет использовать дополнительные возможности. Например, jOOQ умеет работать с хранимыми процедурами и функциями, последовательностями, использовать оконные функции и другие, специфичные для определённой СУБД, возможности. Полная документация по работе с jOOQ находится по ссылке.

Классы jOOQ для работы с базой данных генерируются на основе схемы базы данных. Наше приложение будет работать с базой данных, модель которой представлена на рисунке ниже.

image

Помимо таблиц, наша база данных содержит также хранимые процедуры и последовательности. В конце данной статьи приведена ссылка на скрипт создания базы данных.

Внимание!

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


Для генерации классов jOOQ, работающих с нашей БД, необходимо скачать следующие бинарные файлы по ссылке или через maven репозиторий:
  • jooq-3.9.2.jar
    Главная библиотека, которая включается в наше приложение для работы с jOOQ.
  • jooq-meta-3.9.2.jar
    Утилита, которая включается в вашу сборку для навигации по схеме базы данных через сгенерированные объекты.
  • jooq-codegen-3.9.2.jar
    Утилита, которая включается в вашу сборку для генерации схемы базы данных.


Кроме того для подключения к БД Firebird через JDBC вам потребуется скачать драйвер jaybird-full-3.0.0.jar.

Теперь надо создать файл конфигурации example.xml, который будет использован для генерации классов схемы БД.

example.xml


  
  
    org.firebirdsql.jdbc.FBDriver
    jdbc:firebirdsql://localhost:3050/examples
    SYSDBA
    masterkey
    
      
        charSet
        utf-8
      
    
  

  
    org.jooq.util.JavaGenerator

    
      
      org.jooq.util.firebird.FirebirdDatabase

      

      
      .*

      
      
	   RDB\$.*
	 | MON\$.*
	 | SEC\$.*
      
    

    
      
      ru.ibase.fbjavaex.exampledb

      
      e:/OpenServer/domains/localhost/fbjavaex/src/main/java/
    
  




Теперь переходим в командную строку и выполняем следующую команду:
java -cp jooq-3.9.2.jar;jooq-meta-3.9.2.jar;jooq-codegen-3.9.2.jar;jaybird-full-3.0.0.jar;. org.jooq.util.GenerationTool example.xml


Данная команда создаст необходимые классы и позволит писать на языке Java запросы к объектам БД. Подробнее с процессом генерации классов вы можете ознакомиться по ссылке Code generation.

Конфигурация IoC контейнеров



В Spring внедрение зависимостей (Dependency Injection (DI)) осуществляется через Spring IoC (Inversion of Control) контейнер. Внедрение зависимостей, является процессом, согласно которому объекты определяют свои зависимости, т.е. объекты, с которыми они работают, через аргументы конструктора/фабричного метода или свойства, которые были установлены или возвращены фабричным методом. Затем контейнер inject(далее «внедряет») эти зависимости при создании бина. Подробнее о внедрении зависимостей вы можете ознакомиться в главе The IoC container.

Я не сторонник xml конфигурации, поэтому мы будем использовать подход на основе аннотаций и Java-конфигурации. Основными признаками и частями Java-конфигурации IoC контейнера являются классы с аннотацией @Configuration и методы с аннотацией Bean. Аннотация Bean используется для указания того, что метод создает, настраивает и инициализирует новый объект, управляемый Spring IoC контейнером. Такие методы можно использовать как в классах с аннотацией @Configuration. Наш IoC контейнер будет возвращать пул подключений, менеджер транзакций, транслятор исключений (преобразует исключения SQLException в специфичные для Spring исключения DataAccessException), DSL контекст (стартовая точка, для построения всех запросов используя Fluent API), а также менеджеры для реализации бизнес логики и гриды для отображения данных.
JooqConfig.java
/**
 * Конфигурация IoC контейнера
 * для осуществления внедрения зависимостей.
 */

package ru.ibase.fbjavaex.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.impl.DefaultDSLContext;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.SQLDialect;
import org.jooq.impl.DefaultExecuteListenerProvider;

import ru.ibase.fbjavaex.exception.ExceptionTranslator;

import ru.ibase.fbjavaex.managers.*;
import ru.ibase.fbjavaex.jqgrid.*;

/**
 * Конфигурационный класс Spring IoC контейнера
 */
@Configuration
public class JooqConfig {


    /**
     * Возвращает пул коннектов
     *
     * @return 
     */
    @Bean(name = "dataSource")
    public DataSource getDataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        // определяем конфигурацию подключения
        dataSource.setUrl("jdbc:firebirdsql://localhost:3050/examples");
        dataSource.setDriverClassName("org.firebirdsql.jdbc.FBDriver");
        dataSource.setUsername("SYSDBA");
        dataSource.setPassword("masterkey");
        dataSource.setConnectionProperties("charSet=utf-8");
        return dataSource;
    }

    /**
     * Возращает менеджер транзакций
     * 
     * @return 
     */
    @Bean(name = "transactionManager")
    public DataSourceTransactionManager getTransactionManager() {
        return new DataSourceTransactionManager(getDataSource());
    }

    @Bean(name = "transactionAwareDataSource")
    public TransactionAwareDataSourceProxy getTransactionAwareDataSource() {
        return new TransactionAwareDataSourceProxy(getDataSource());
    }

    /**
     * Возвращает провайдер подключений
     * 
     * @return 
     */
    @Bean(name = "connectionProvider")
    public DataSourceConnectionProvider getConnectionProvider() {
        return new DataSourceConnectionProvider(getTransactionAwareDataSource());
    }

    /**
     * Возвращает транслятор исключений
     * 
     * @return 
     */
    @Bean(name = "exceptionTranslator")
    public ExceptionTranslator getExceptionTranslator() {
        return new ExceptionTranslator();
    }

    /**
     * Возвращает конфигурацию DSL контекста
     *
     * @return 
     */
    @Bean(name = "dslConfig")
    public org.jooq.Configuration getDslConfig() {
        DefaultConfiguration config = new DefaultConfiguration();
        // используем диалект SQL СУБД Firebird
        config.setSQLDialect(SQLDialect.FIREBIRD);
        config.setConnectionProvider(getConnectionProvider());
        DefaultExecuteListenerProvider listenerProvider = new DefaultExecuteListenerProvider(getExceptionTranslator());
        config.setExecuteListenerProvider(listenerProvider);
        return config;
    }

    /**
     * Возвращает DSL контекст
     *
     * @return 
     */
    @Bean(name = "dsl")
    public DSLContext getDsl() {
        org.jooq.Configuration config = this.getDslConfig();
        return new DefaultDSLContext(config);
    }

    /**
     * Возвращает менеджер заказчиков
     * 
     * @return 
     */
    @Bean(name = "customerManager")
    public CustomerManager getCustomerManager() {
        return new CustomerManager();
    }

    /**
     * Возвращает грид с заказчиками
     * 
     * @return 
     */
    @Bean(name = "customerGrid")
    public JqGridCustomer getCustomerGrid() {
        return new JqGridCustomer();
    }

    /**
     * Возвращает менеджер продуктов
     * 
     * @return 
     */
    @Bean(name = "productManager")
    public ProductManager getProductManager() {
        return new ProductManager();
    }

    /**
     * Возвращает грид с товарами
     * 
     * @return 
     */
    @Bean(name = "productGrid")
    public JqGridProduct getProductGrid() {
        return new JqGridProduct();
    }

    /**
     * Возвращает менеджер счёт фактур
     * 
     * @return 
     */
    @Bean(name = "invoiceManager")
    public InvoiceManager getInvoiceManager() {
        return new InvoiceManager();
    }

    /**
     * Возвращает грид с заголовками счёт фактур
     * 
     * @return 
     */
    @Bean(name = "invoiceGrid")
    public JqGridInvoice getInvoiceGrid() {
        return new JqGridInvoice();
    }

    /**
     * Возвращает грид с позициями счёт фактуры
     * 
     * @return 
     */
    @Bean(name = "invoiceLineGrid")
    public JqGridInvoiceLine getInvoiceLineGrid() {
        return new JqGridInvoiceLine();
    }

    /**
     * Возвращает рабочий период
     * 
     * @return 
     */
    @Bean(name = "workingPeriod")
    public WorkingPeriod getWorkingPeriod() {
        return new WorkingPeriod();
    }

}



Построение SQL запросов используя jOOQ



Прежде чем рассматривать реализацию менеджеров и сеток (grids) расскажем, как работать с базой данных через jOOQ. Здесь будут изложены лишь краткие сведения о построении запросов, полную документацию по этому вопросу вы можете найти в главе sql-building документации jOOQ.

Класс org.jooq.impl.DSL является основным классом, от которого вы будете создавать все объекты jOOQ. Он выступает в роли статической фабрики для табличных выражений, выражений столбцов (или полей), условных выражений и многих других частей запроса.

DSLContext ссылается на объект org.jooq.Configuration, который настраивает поведение jOOQ, при выполнении запросов. В отличие от статического DSL, DSLContext позволяет создавать SQL-операторы, которые уже «настроены» и готовы к выполнению. В нашем приложении DSLContext создаётся в классе конфигурации JooqConfig в методе getDsl. Конфигурация для DSLContext возвращается методом getDslConfig. В этом методе мы указали, что будем использовать диалект SQL СУБД Firebird, провайдер подключений (определяет, как мы получаем подключение через JDBC) и слушатель выполнения SQL запросов.

jOOQ поставляется с собственным DSL (или Domain Specific Language), который эмулирует SQL в Java. Это означает, что вы можете писать SQL-операторы почти так, как если бы Java изначально поддерживал их, примерно так же, как .NET в C# делает это с помощью LINQ к SQL.
jOOQ использует неформальную BNF нотацию, которая моделирует унифицированный SQL диалект, подходящий для большинства СУБД. В отличие от других, более простых фреймворков, которые используют «Fluent API» или «метод цепочек”, иерархия интерфейса BNF на основе jOOQ не позволяет плохой синтаксис запросов.

Давайте рассмотрим простой запрос на языке SQL
SELECT *
  FROM author a
  JOIN book b ON a.id = b.author_id
 WHERE a.year_of_birth > 1920
   AND a.first_name = 'Paulo'
 ORDER BY b.title


В jOOQ он будет выглядеть следующим образом:
Result result =
dsl.select()
   .from(AUTHOR.as("a"))
   .join(BOOK.as("b")).on(a.ID.equal(b.AUTHOR_ID))
   .where(a.YEAR_OF_BIRTH.greaterThan(1920)
   .and(a.FIRST_NAME.equal("Paulo")))
   .orderBy(b.TITLE)
   .fetch();


Классы AUTHOR и BOOK, описывающие соответствующие таблицы должны быть сгенерированы заранее. Процесс генерации классов jOOQ по заданной схеме БД был описан выше.

В данном случае мы задали таблицам AUTHOR и BOOK алиас с помощью конструкции as. Без использования алиасов этот запрос выглядел бы следующим образом
Result result =
dsl.select()
   .from(AUTHOR)
   .join(BOOK).on(AUTHOR.ID.equal(BOOK.AUTHOR_ID))
   .where(AUTHOR.YEAR_OF_BIRTH.greaterThan(1920)
   .and(AUTHOR.FIRST_NAME.equal("Paulo")))
   .orderBy(BOOK.TITLE)
   .fetch();


Теперь посмотрим более сложный запрос с использованием агрегатных функций и группировки.
SELECT AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, COUNT(*)
FROM AUTHOR
  JOIN BOOK ON AUTHOR.ID = BOOK.AUTHOR_ID
WHERE BOOK.LANGUAGE = 'DE'
  AND BOOK.PUBLISHED > '2008-01-01'
GROUP BY AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME
  HAVING COUNT(*) > 5
ORDER BY AUTHOR.LAST_NAME ASC NULLS FIRST
  OFFSET 1 ROWS
  FETCH FIRST 2 ROWS ONLY


В jOOQ он будет выглядеть так:
dsl.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count())
   .from(AUTHOR)
   .join(BOOK).on(BOOK.AUTHOR_ID.equal(AUTHOR.ID))
   .where(BOOK.LANGUAGE.equal("DE"))
   .and(BOOK.PUBLISHED.greaterThan("2008-01-01"))
   .groupBy(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
   .having(count().greaterThan(5))
   .orderBy(AUTHOR.LAST_NAME.asc().nullsFirst())
   .limit(2)
   .offset(1)
   .fetch();


Заметьте ограничение на количество возвращаемых записей, будет сгенерировано в соответствии с указанным диалектом SQL. В примере выше использовался диалект FIREIRD_3_0. Если бы был указан диалект FIREBIRD_2_5 или просто FIREBIRD, то использовалось бы предложение ROWS вместо OFSET … FETCH.

Вы можете собирать запрос по частям. Это позволяет менять его динамически, что можно использовать для изменения порядка сортировки или добавления дополнительных параметров фильтрации.
        SelectFinalStep query = select.getQuery();
        switch (searchOper) {
            case "eq":
                query.addConditions(PRODUCT.NAME.eq(searchString));
                break;
            case "bw":
                query.addConditions(PRODUCT.NAME.startsWith(searchString));
                break;
            case "cn":
                query.addConditions(PRODUCT.NAME.contains(searchString));
                break;
        }
        switch (sOrd) {
            case "asc":
                query.addOrderBy(PRODUCT.NAME.asc());
                break;
            case "desc":
                query.addOrderBy(PRODUCT.NAME.desc());
                break;
        }
        return query.fetchMaps();


Именованные и неименованные параметры



По умолчанию каждый раз, когда вы используете в запросе литера строк, дат и чисел, а также подставляете внешние переменные, jOOQ делает привязку этой переменной или литерала через неименованные параметры. Например, следующее выражение на языке Java
dsl.select()
   .from(BOOK)
   .where(BOOK.ID.equal(5))
   .and(BOOK.TITLE.equal("Animal Farm"))
   .fetch();


Эквивалентно полной форме записи
dsl.select()
   .from(BOOK)
   .where(BOOK.ID.equal(val(5)))
   .and(BOOK.TITLE.equal(val("Animal Farm")))
   .fetch();


и преобразуется в sql запрос
SELECT *
FROM BOOK
WHERE BOOK.ID = ?
  AND BOOK.TITLE = ?


Вам не нужно беспокоиться какой индекс у соответствующего параметра, значения автоматически будут привязаны к нужному параметру. Если нужно изменить значение параметра, то вы можете сделать это, выбрав нужный параметр по номеру индекса (индексация начинается с 1).
Select param = select.getParam("2");
Param.setValue("Animals as Leaders");


Другим способом присвоить параметру новое значение является вызов метода bind.
Query query1 = 
  dsl.select()
     .from(AUTHOR)
     .where(LAST_NAME.equal("Poe"));
query1.bind(1, "Orwell");


Кроме того, jOOQ поддерживает именованные параметры. В этом случае их надо явно создавать, используя org.jooq.Param.
// Create a query with a named parameter. You can then use that name for 
// accessing the parameter again
Query query1 = 
  dsl.select()
     .from(AUTHOR)
     .where(LAST_NAME.equal(param("lastName", "Poe")));
Param query) {
        switch (this.searchOper) {
            case "eq":
                // CUSTOMER.NAME = ?
                query.addConditions(CUSTOMER.NAME.eq(this.searchString));
                break;
            case "bw":
                // CUSTOMER.NAME STARTING WITH ?
                query.addConditions(CUSTOMER.NAME.startsWith(this.searchString));
                break;
            case "cn":
                // CUSTOMER.NAME CONTAINING ?
                query.addConditions(CUSTOMER.NAME.contains(this.searchString));
                break;
        }
    }


    /**
     * Возвращает общее количество записей
     *
     * @return
     */
    @Override
    public int getCountRecord() {
        // запрос, возвращающий количество записей
        SelectFinalStep query = select.getQuery();
        // если мы осуществляем поиск, то добавляем условие поиска
        if (this.searchFlag) {
            makeSearchCondition(query);
        }
        // возарщаем количество
        return (int) query.fetch().getValue(0, 0);
    }

    /**
     * Возвращает записи грида
     * 
     * @return
     */
    @Override
    public List> getRecords() {
        // Базовый запрос на выборку
        SelectFinalStep query = select.getQuery();
        // если мы осуществляем поиск, то добавляем условие поиска
        if (this.searchFlag) {
            makeSearchCondition(query);
        }
        // задаём порядок сортировки
        switch (this.sOrd) {
            case "asc":
                query.addOrderBy(CUSTOMER.NAME.asc());
                break;
            case "desc":
                query.addOrderBy(CUSTOMER.NAME.desc());
                break;
        }
        // ограничиваем количество записей
        if (this.limit != 0) {
            query.addLimit(this.limit);
        }
        // смещение
        if (this.offset != 0) {
            query.addOffset(this.offset);
        }
        // возвращаем массив карт
        return query.fetchMaps();
    }
}



Добавление, редактирование и удаление заказчика мы будем осуществлять через класс CustomerManager, который является своеобразным бизнес слоем между соответствующим контроллером и базой данных. Все операции в этом слое мы будем осуществлять в транзакции с уровнем изолированности Snapshot.
CustomerManager.java
package ru.ibase.fbjavaex.managers;

import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;

import static ru.ibase.fbjavaex.exampledb.Tables.CUSTOMER;
import static ru.ibase.fbjavaex.exampledb.Sequences.GEN_CUSTOMER_ID;

/**
 * Менеджер заказчиков
 *
 * @author Simonov Denis
 */
public class CustomerManager {

    @Autowired(required = true)
    private DSLContext dsl;
    
    /**
     * Добавление заказчика
     * 
     * @param name
     * @param address
     * @param zipcode
     * @param phone 
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)  
    public void create(String name, String address, String zipcode, String phone) {
        if (zipcode != null) {
            if (zipcode.trim().isEmpty()) {
                zipcode = null;
            }
        }        
        
        int customerId = this.dsl.nextval(GEN_CUSTOMER_ID).intValue();
               
        this.dsl
                .insertInto(CUSTOMER,
                        CUSTOMER.CUSTOMER_ID,
                        CUSTOMER.NAME,
                        CUSTOMER.ADDRESS,
                        CUSTOMER.ZIPCODE,
                        CUSTOMER.PHONE)
                .values(
                        customerId,
                        name,
                        address,
                        zipcode,
                        phone
                )
                .execute();
    }

    /**
     * Редактирование заказчика
     * 
     * @param customerId
     * @param name
     * @param address
     * @param zipcode
     * @param phone 
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)      
    public void edit(int customerId, String name, String address, String zipcode, String phone) {

        if (zipcode != null) {
            if (zipcode.trim().isEmpty()) {
                zipcode = null;
            }
        }
               
        this.dsl.update(CUSTOMER)
                .set(CUSTOMER.NAME, name)
                .set(CUSTOMER.ADDRESS, address)
                .set(CUSTOMER.ZIPCODE, zipcode)
                .set(CUSTOMER.PHONE, phone)
                .where(CUSTOMER.CUSTOMER_ID.eq(customerId))
                .execute();
    }

    /**
     * Удаление заказчика
     * 
     * @param customerId 
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)    
    public void delete(int customerId) {
        this.dsl.deleteFrom(CUSTOMER)
                .where(CUSTOMER.CUSTOMER_ID.eq(customerId))
                .execute();
    }
}



Теперь перейдём к написанию контроллера. Классы контроллеров начинаются с аннотации Controller. Для определения действий контроллера необходимо добавить аннотацию @RequestMapping перед методом и указать в ней маршрут, по которому будет вызвано действие контроллера. Маршрут указывается в параметре value. В параметре method можно указать метод HTTP запроса (PUT, GET, POST, DELETE). Входной точкой нашего контроллера будет метод index, он отвечает за отображение JSP страницы (представления). Эта страница содержит разметку для отображения грида, панель инструментов и навигации.

Данные для отображения загружаются асинхронно компонентом jqGrid (маршрут /customer/getdata). С данным маршрутом связан метод getData. Метод содержит дополнительную аннотацию @ResponseBody, которая говорит о том, что наш метод возвращает объект для сериализации в один из форматов. В аннотации @RequestMapping задан параметр produces = MediaType.APPLICATION_JSON, что обозначает, что возвращаемый объект будет сериализован в формат JSON. Именно в этом методе мы работаем с классом JqGridCustomer описанном выше. Аннотация @RequestParam позволяет извлечь значение параметра из HTTP запроса. Данный метод класса работает с GET запросами. Параметр value в аннотации @RequestParam задаёт имя параметра HTTP запроса для извлечения. Параметр required задаёт, является ли параметр HTTP запроса обязательным. Параметр defaultValue задаёт значение по умолчанию, которое будет подставлено в случае отсутствия HHTP параметра.

Метод addCustomer предназначен для добавления нового заказчика. Он связан с маршрутом /customer/create, и в отличие от предыдущего метода работает с POST запросом. Метод возвращает {success: true} в случае успешного добавления, и объект с текстом ошибки в случае ошибки. Данный метод работает с классом бизнес слоя CustomerManager.

Метод editCustomer связан с маршрутом /customer/edit и предназначен для редактирования заказчика. Метод deleteCustomer связан с маршрутом /customer/delete и предназначен для удаления заказчика.

CustomerController.java
package ru.ibase.fbjavaex.controllers;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestParam;
import javax.ws.rs.core.MediaType;

import org.springframework.beans.factory.annotation.Autowired;

import ru.ibase.fbjavaex.managers.CustomerManager;

import ru.ibase.fbjavaex.jqgrid.JqGridCustomer;
import ru.ibase.fbjavaex.jqgrid.JqGridData;


/**
 * Контроллер заказчиков
 *
 * @author Simonov Denis
 */
@Controller
public class CustomerController {


    @Autowired(required = true)
    private JqGridCustomer customerGrid;
    
    @Autowired(required = true)
    private CustomerManager customerManager;


    /**
     * Действие по умолчанию
     * Возвращает имя JSP страницы (представления) для отображения
     *
     * @param map
     * @return имя JSP шаблона
     */
    @RequestMapping(value = "/customer/", method = RequestMethod.GET)
    public String index(ModelMap map) {
        return "customer";
    }

    /**
     * Возвращает данные в формате JSON для jqGrid
     *
     * @param rows количество строк на страницу
     * @param page номер страницы
     * @param sIdx поле для сортировки
     * @param sOrd порядок сортировки
     * @param search должен ли осуществляться поиск
     * @param searchField поле поиска
     * @param searchString значение поиска
     * @param searchOper операция поиска
     * @return JSON для jqGrid
     */
    @RequestMapping(value = "/customer/getdata", 
            method = RequestMethod.GET, 
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public JqGridData getData(
            // количество записей на странице
            @RequestParam(value = "rows", required = false, defaultValue = "20") int rows,
            // номер текущей страницы
            @RequestParam(value = "page", required = false, defaultValue = "1") int page,
            // поле для сортировки
            @RequestParam(value = "sidx", required = false, defaultValue = "") String sIdx,
            // направление сортировки
            @RequestParam(value = "sord", required = false, defaultValue = "asc") String sOrd,
            // осуществляется ли поиск
            @RequestParam(value = "_search", required = false, defaultValue = "false") Boolean search,
            // поле поиска
            @RequestParam(value = "searchField", required = false, defaultValue = "") String searchField,
            // значение поиска
            @RequestParam(value = "searchString", required = false, defaultValue = "") String searchString,
            // операция поиска
            @RequestParam(value = "searchOper", required = false, defaultValue = "") String searchOper,
            // фильтр
            @RequestParam(value="filters", required=false, defaultValue="") String filters) {
        customerGrid.setLimit(rows);
        customerGrid.setPageNo(page);
        customerGrid.setOrderBy(sIdx, sOrd);
        if (search) {
            customerGrid.setSearchCondition(searchField, searchString, searchOper);
        }

        return customerGrid.getJqGridData();
    }

    @RequestMapping(value = "/customer/create", 
            method = RequestMethod.POST, 
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public Map addCustomer(
            @RequestParam(value = "NAME", required = true, defaultValue = "") String name,
            @RequestParam(value = "ADDRESS", required = false, defaultValue = "") String address,
            @RequestParam(value = "ZIPCODE", required = false, defaultValue = "") String zipcode,
            @RequestParam(value = "PHONE", required = false, defaultValue = "") String phone) {
        Map map = new HashMap<>();
        try {
            customerManager.create(name, address, zipcode, phone);
            map.put("success", true);
        } catch (Exception ex) {
            map.put("error", ex.getMessage());
        }
        return map;
    }

    @RequestMapping(value = "/customer/edit", 
            method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public Map editCustomer(
            @RequestParam(value = "CUSTOMER_ID", required = true, defaultValue = "0") int customerId,
            @RequestParam(value = "NAME", required = true, defaultValue = "") String name,
            @RequestParam(value = "ADDRESS", required = false, defaultValue = "") String address,
            @RequestParam(value = "ZIPCODE", required = false, defaultValue = "") String zipcode,
            @RequestParam(value = "PHONE", required = false, defaultValue = "") String phone) {
        Map map = new HashMap<>();
        try {
            customerManager.edit(customerId, name, address, zipcode, phone);
            map.put("success", true);
        } catch (Exception ex) {
            map.put("error", ex.getMessage());
        }
        return map;
    }

    @RequestMapping(value = "/customer/delete", 
            method = RequestMethod.POST, 
            produces = MediaType.APPLICATION_JSON)
    @ResponseBody
    public Map deleteCustomer(
            @RequestParam(value = "CUSTOMER_ID", required = true, defaultValue = "0") int customerId) {
        Map map = new HashMap<>();
        try {
            customerManager.delete(customerId);
            map.put("success", true);
        } catch (Exception ex) {
            map.put("error", ex.getMessage());
        }
        return map;
    }
}



JSP страница для отображения справочника заказчиков не содержит ничего особенного: разметку с основными частями страницы, таблицу для отображения грида и блок для отображения панели навигации. JSP шаблоны не очень продвинутое средство, при желании вы можете заменить их на другие системы шаблонов, которые поддерживают наследование. В файле ../jspf/head.jspf содержатся общие скрипты и стили для всех страниц сайта, а файл ../jspf/menu.jspf главное меню сайта. Мы не будем приводить их код, он довольно простой и при желании вы можете посмотреть его в исходных кодах проекта.
Customers.jsp






    
        
        
        
        
        
           
    
    
        
        

        

Customers


© 2016 - Пример Spring MVC приложения с использованием Firebird и jOOQ




Основная логика на стороне клиента сосредоточена в JavaScript модуле /resources/js/jqGridCustomer.js
jqGridCustomer.js
var JqGridCustomer = (function ($) {

    return function (options) {
        var jqGridCustomer = {
            dbGrid: null,
            // опции
            options: $.extend({
                baseAddress: null,
                showEditorPanel: true
            }, options),
            // возвращает модель
            getColModel: function () {
                return [
                    {
                        label: 'Id', // подпись
                        name: 'CUSTOMER_ID', // имя поля
                        key: true, // признак ключевого поля
                        hidden: true          // скрыт 
                    },
                    {
                        label: 'Name', // подпись поля
                        name: 'NAME', // имя поля
                        width: 240, // ширина
                        sortable: true, // разрешена сортировка
                        editable: true, // разрешено редактирование
                        edittype: "text", // тип поля в редакторе
                        search: true, // разрешён поиск
                        searchoptions: {
                            sopt: ['eq', 'bw', 'cn'] // разрешённые операторы поиска
                        },
                        editoptions: {size: 30, maxlength: 60}, // размер и максимальная длина для поля ввода
                        editrules: {required: true}      // говорит о том, что поле обязательное
                    },
                    {
                        label: 'Address',
                        name: 'ADDRESS',
                        width: 300,
                        sortable: false, // запрещаем сортировку
                        editable: true, // редактируемое
                        search: false, // запрещаем поиск
                        edittype: "textarea", // мемо поле
                        editoptions: {maxlength: 250, cols: 30, rows: 4}
                    },
                    {
                        label: 'Zip Code',
                        name: 'ZIPCODE',
                        width: 30,
                        sortable: false,
                        editable: true,
                        search: false,
                        edittype: "text",
                        editoptions: {size: 30, maxlength: 10}
                    },
                    {
                        label: 'Phone',
                        name: 'PHONE',
                        width: 80,
                        sortable: false,
                        editable: true,
                        search: false,
                        edittype: "text",
                        editoptions: {size: 30, maxlength: 14}
                    }
                ];
            },
            // инициализация грида
            initGrid: function () {
                // url для получения данных
                var url = jqGridCustomer.options.baseAddress + '/customer/getdata';
                jqGridCustomer.dbGrid = $("#jqGridCustomer").jqGrid({
                    url: url,
                    datatype: "json", // формат получения данных 
                    mtype: "GET", // тип http запроса
                    colModel: jqGridCustomer.getColModel(),
                    rowNum: 500, // число отображаемых строк
                    loadonce: false, // загрузка только один раз
                    sortname: 'NAME', // сортировка по умолчанию по столбцу NAME
                    sortorder: "asc", // порядок сортировки
                    width: window.innerWidth - 80, // ширина грида
                    height: 500, // высота грида
                    viewrecords: true, // отображать количество записей
                    guiStyle: "bootstrap",
                    iconSet: "fontAwesome",
                    caption: "Customers", // подпись к гриду
                    // элемент для отображения навигации
                    pager: 'jqPagerCustomer'
                });
            },
            // опции редактирования
            getEditOptions: function () {
                return {
                    url: jqGridCustomer.options.baseAddress + '/customer/edit',
                    reloadAfterSubmit: true,
                    closeOnEscape: true,
                    closeAfterEdit: true,
                    drag: true,
                    width: 400,
                    afterSubmit: jqGridCustomer.afterSubmit,
                    editData: {
                        // дополнительно к значениям из формы передаём ключевое поле
                        CUSTOMER_ID: function () {
                            // получаем текущую строку
                            var selectedRow = jqGridCustomer.dbGrid.getGridParam("selrow");
                            // получаем значение интересующего нас поля
                            var value = jqGridCustomer.dbGrid.getCell(selectedRow, 'CUSTOMER_ID');
                            return value;
                        }
                    }
                };
            },
            // опции добавления
            getAddOptions: function () {
                return {
                    url: jqGridCustomer.options.baseAddress + '/customer/create',
                    reloadAfterSubmit: true,
                    closeOnEscape: true,
                    closeAfterAdd: true,
                    drag: true,
                    width: 400,
                    afterSubmit: jqGridCustomer.afterSubmit
                };
            },
            // опции удаления
            getDeleteOptions: function () {
                return {
                    url: jqGridCustomer.options.baseAddress + '/customer/delete',
                    reloadAfterSubmit: true,
                    closeOnEscape: true,
                    closeAfterDelete: true,
                    drag: true,
                    msg: "Удалить выделенного заказчика?",
                    afterSubmit: jqGridCustomer.afterSubmit,
                    delData: {
                        // передаём ключевое поле
                        CUSTOMER_ID: function () {
                            var selectedRow = jqGridCustomer.dbGrid.getGridParam("selrow");
                            var value = jqGridCustomer.dbGrid.getCell(selectedRow, 'CUSTOMER_ID');
                            return value;
                        }
                    }
                };
            },
            // инициализация панели навигации вместе с диалогами редактирования
            initPagerWithEditors: function () {
                jqGridCustomer.dbGrid.jqGrid('navGrid', '#jqPagerCustomer',
                        {
                            // кнопки
                            search: true, // поиск
                            add: true, // добавление
                            edit: true, // редактирование
                            del: true, // удаление
                            view: true, // просмотр записи
                            refresh: true, // обновление
                            // подписи кнопок
                            searchtext: "Поиск",
                            addtext: "Добавить",
                            edittext: "Изменить",
                            deltext: "Удалить",
                            viewtext: "Смотреть",
                            viewtitle: "Выбранная запись",
                            refreshtext: "Обновить"
                        },
                        jqGridCustomer.getEditOptions(),
                        jqGridCustomer.getAddOptions(),
                        jqGridCustomer.getDeleteOptions()
                        );
            },
            // инициализация панели навигации вместе без диалогов редактирования
            initPagerWithoutEditors: function () {
                jqGridCustomer.dbGrid.jqGrid('navGrid', '#jqPagerCustomer',
                        {
                            // кнопки
                            search: true, // поиск
                            add: false, // добавление
                            edit: false, // редактирование
                            del: false, // удаление
                            view: false, // просмотр записи
                            refresh: true, // обновление
                            // подписи кнопок
                            searchtext: "Поиск",
                            viewtext: "Смотреть",
                            viewtitle: "Выбранная запись",
                            refreshtext: "Обновить"
                        }
                );
            },
            // инициализация панели навигации
            initPager: function () {
                if (jqGridCustomer.options.showEditorPanel) {
                    jqGridCustomer.initPagerWithEditors();
                } else {
                    jqGridCustomer.initPagerWithoutEditors();
                }
            },
            // инициализация
            init: function () {
                jqGridCustomer.initGrid();
                jqGridCustomer.initPager();
            },
            // обработчик результатов обработки форм (операций)
            afterSubmit: function (response, postdata) {
                var responseData = response.responseJSON;
                // проверяем результат на наличие сообщений об ошибках
                if (responseData.hasOwnProperty("error")) {
                    if (responseData.error.length) {
                        return [false, responseData.error];
                    }
                } else {
                    // если не была возвращена ошибка обновляем грид
                    $(this).jqGrid(
                            'setGridParam',
                            {
                                datatype: 'json'
                            }
                    ).trigger('reloadGrid');
                }
                return [true, "", 0];
            }
        };
        jqGridCustomer.init();
        return jqGridCustomer;
    };
})(jQuery);



Сетка jqGrid создаётся в методе initGrid и привязывается к html элементу с идентификатором jqGridCustomer. Описание столбцов (колонок) грида возвращается методом getColModel. Каждый столбец в jqGrid имеет достаточно много возможных свойств. В исходном коде присутствуют комментарии, объясняющие свойства столбцов. Подробнее о конфигурировании модели столбцов jqGrid вы можете прочитать в документации проекта jqGrid в разделе ColModel API.

Панель навигации может быть создана с кнопками редактирования или без них (методы initPagerWithEditors и initPagerWithoutEditors соответственно). Конструктор панели прикрепляет её к элементу с идентификатором jqPagerCustomer. Опции создания панели навигации описаны в разделе Navigator документации jqGrid.

Функции getEditOptions, getAddOptions, getDeleteOptions возвращают опции диалогов редактирования, добавления и удаления соответственно. Свойство url указывает, по какому адресу будут отправлены данные после нажатия кнопки OK в диалоге. Свойство afterSubmit – событие, происходящее после отправки данных на сервер и получения ответа от него. В методе afterSubmit проверяется, не вернул ли наш контроллер ошибку. Если ошибки не было, то производится обновление грида, в противном случае ошибка сообщается пользователю. Обратите внимание на свойство editData. Оно позволяет задать значения дополнительных полей, которые не участвуют в диалоге редактирования. Дело в том, что диалоги редактирования не включают в себя значение скрытых полей, а отображать автоматически генерируемые ключи не сильно хочется.

Создание журналов



В отличие от справочников журналы содержат довольно большое количество записей и являются часто пополняемыми. Большинство журналов содержат поле с датой создания документа. Чтобы уменьшить количество выбираемых данных обычно принято вводить такое понятие как рабочий период для того, чтобы уменьшить объём данных передаваемый на клиента. Рабочий период – это диапазон дат, внутри которого требуются рабочие документы. Рабочий период описывается классом WorkingPeriod. Этот класс создаётся через бин workingPeriod в классе конфигурации ru.ibase.fbjavaex.config.JooqConfig.
WorkingPeriod.java
package ru.ibase.fbjavaex.config;

import java.sql.Timestamp;
import java.time.LocalDateTime;

/**
 * Рабочий период
 *
 * @author Simonov Denis
 */
public class WorkingPeriod {

    private Timestamp beginDate;
    private Timestamp endDate;

    /** 
     * Конструктор
     */
    WorkingPeriod() {
        // в реальных приложениях вычисляется от текущей даты
        this.beginDate = Timestamp.valueOf("2015-06-01 00:00:00");
        this.endDate = Timestamp.valueOf(LocalDateTime.now().plusDays(1));
    }

    /**
     * Возвращает дату начала рабочего периода
     *
     * @return
     */
    public Timestamp getBeginDate() {
        return this.beginDate;
    }

    /**
     * Возвращает дату окончания рабочего периода
     * 
     * @return
     */
    public Timestamp getEndDate() {
        return this.endDate;
    }

    /**
     * Установка даты начала рабочего периода
     * 
     * @param value
     */
    public void setBeginDate(Timestamp value) {
        this.beginDate = value;
    }

    /**
     * Установка даты окончания рабочего периода
     * 
     * @param value
     */
    public void setEndDate(Timestamp value) {
        this.endDate = value;
    }

    /**
     * Установка рабочего периода
     * 
     * @param beginDate
     * @param endDate
     */
    public void setRangeDate(Timestamp beginDate, Timestamp endDate) {
        this.beginDate = beginDate;
        this.endDate = endDate;
    }
}



В нашем приложении будет один журнал «Счёт-фактуры». Счёт-фактура – состоит из заголовка, где описываются общие атрибуты (номер, дата, заказчик …), и позиций счёт-фактуры (наименование товара, количество, стоимостью и т.д.). Шапка счёт-фактуру отображается в основной сетке, а позиции могут быть просмотрены в детализирующей сетке, которая раскрывается по щелчку по значку «+» на нужном документе.

Реализуем класс для просмотра шапок счёт-фактуры через jqGrid, он будет наследоваться от нашего абстрактного класса ru.ibase.fbjavaex.jqgrid.JqGrid, описанного выше. В нём имеется возможность поиска наименованию заказчика и дате счёта. Кроме того данный класс поддерживает сортировку по дате в обоих направлениях.
JqGridInvoice.java
package ru.ibase.fbjavaex.jqgrid;

import java.sql.*;
import org.jooq.*;

import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import ru.ibase.fbjavaex.config.WorkingPeriod;

import static ru.ibase.fbjavaex.exampledb.Tables.INVOICE;
import static ru.ibase.fbjavaex.exampledb.Tables.CUSTOMER;

/**
 * Обработчик грида для журнала счёт-фактур
 *
 * @author Simonov Denis
 */
public class JqGridInvoice extends JqGrid {
    
    @Autowired(required = true)
    private WorkingPeriod workingPeriod;    

    /**
     * Добавление условия поиска
     *
     * @param query
     */
    private void makeSearchCondition(SelectQuery select
                = dsl.selectCount()
                        .from(INVOICE)
                        .where(INVOICE.INVOICE_DATE.between(this.workingPeriod.getBeginDate(), this.workingPeriod.getEndDate()));

        SelectQuery select
                = dsl.select(
                        INVOICE.INVOICE_ID,
                        INVOICE.CUSTOMER_ID,
                        CUSTOMER.NAME.as("CUSTOMER_NAME"),
                        INVOICE.INVOICE_DATE,
                        INVOICE.PAID,
                        INVOICE.TOTAL_SALE)
                        .from(INVOICE)
                        .innerJoin(CUSTOMER).on(CUSTOMER.CUSTOMER_ID.eq(INVOICE.CUSTOMER_ID))
                        .where(INVOICE.INVOICE_DATE.between(this.workingPeriod.getBeginDate(), this.workingPeriod.getEndDate()));

        SelectQuery select
                = dsl.selectCount()
                        .from(INVOICE_LINE)
                        .where(INVOICE_LINE.INVOICE_ID.eq(this.invoiceId));

        SelectQuery select
                = dsl.select(
                        INVOICE_LINE.INVOICE_LINE_ID,
                        INVOICE_LINE.INVOICE_ID,
                        INVOICE_LINE.PRODUCT_ID,
                        PRODUCT.NAME.as("PRODUCT_NAME"),
                        INVOICE_LINE.QUANTITY,
                        INVOICE_LINE.SALE_PRICE,
                        INVOICE_LINE.SALE_PRICE.mul(INVOICE_LINE.QUANTITY).as("TOTAL"))
                        .from(INVOICE_LINE)
                        .innerJoin(PRODUCT).on(PRODUCT.PRODUCT_ID.eq(INVOICE_LINE.PRODUCT_ID))
                        .where(INVOICE_LINE.INVOICE_ID.eq(this.invoiceId));

        SelectQuery> httpMessageConverters) {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        jsonConverter.setObjectMapper(objectMapper);
        httpMessageConverters.add(jsonConverter);
    }
…

}


Метод initBinder контроллера InvoiceController описывает, каким образом текстовое представление даты, присылаемое браузером, преобразуется в значение типа Timestamp.

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

Invoices.jsp






    
        
        

        
        
           
           
           
    
    
        
        

        

Invoices


© 2016 - Пример Spring MVC приложения с использованием Firebird и jOOQ




Основная логика на стороне клиента сосредоточена в JavaScript модуле /resources/js/jqGridInvoice.js
jqGridInvoice.js
var JqGridInvoice = (function ($, jqGridProductFactory, jqGridCustomerFactory) {

    return function (options) {
        var jqGridInvoice = {
            dbGrid: null,
            detailGrid: null,
            // опции
            options: $.extend({
                baseAddress: null
            }, options),
            // возвращает опции колонок (модель) счёт фактуры
            getInvoiceColModel: function () {
                return [
                    {
                        label: 'Id', // подпись
                        name: 'INVOICE_ID', // имя поля
                        key: true, // признак ключевого поля   
                        hidden: true // скрыт 
                    },
                    {
                        label: 'Customer Id', // подпись
                        name: 'CUSTOMER_ID', // имя поля
                        hidden: true, // скрыт 
                        editrules: {edithidden: true, required: true}, // скрытое и требуемое
                        editable: true, // редактируемое
                        edittype: 'custom', // собственный тип
                        editoptions: {
                            custom_element: function (value, options) {
                                // добавляем скрытый input
                                return $("")
                                        .attr('type', 'hidden')
                                        .attr('rowid', options.rowId)
                                        .addClass("FormElement")
                                        .addClass("form-control")
                                        .val(value)
                                        .get(0);
                            }
                        }
                    },
                    {
                        label: 'Date',
                        name: 'INVOICE_DATE',
                        width: 60, // ширина
                        sortable: true, // позволять сортировку
                        editable: true, // редактируемое
                        search: true, // разрешён поиск
                        edittype: "text", // тип поля ввода
            
                                        
Метки:  

 

Добавить комментарий:
Текст комментария: смайлики

Проверка орфографии: (найти ошибки)

Прикрепить картинку:

 Переводить URL в ссылку
 Подписаться на комментарии
 Подписать картинку