Sunday, November 13, 2011

Создаем приложение с помощью Spring и GWT. Часть 4 - MyBatis и MySQL

Недавно я начал работу в проекте, в котором используется новая для меня технология - GWT. Решил написать небольшое приложение, использующее этот фреймворк и Spring.

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

Восполним этот пробел с помощью:
  • MyBatis 3.0.5
  • MySQL 5.1

Для этого нужно добавить артефакты mybatis-spring и mysql-connector-java в pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>hellogwt</groupId>
    <artifactId>hellogwt</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <properties>
        <spring.version>3.0.5.RELEASE</spring.version>
        <gwt.version>2.4.0</gwt.version>
        <log4j.version>1.2.16</log4j.version>
        <mybatis-spring.version>1.0.1</mybatis-spring.version>
        <mysql-connector-java.version>5.1.18</mysql-connector-java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.gwt</groupId>
            <artifactId>gwt-servlet</artifactId>
            <version>${gwt.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.gwt</groupId>
            <artifactId>gwt-user</artifactId>
            <version>${gwt.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>${mybatis-spring.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector-java.version}</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>hellogwt</finalName>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>gwt-maven-plugin</artifactId>
                <version>${gwt.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>test</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

По задумке наше приложение позволит пользователю просматривать и редактировать различные приветствия. Каждое приветствие имеет автора и, собственно, текст приветствия.
Создадим с помощью MySQL базу данных и таблицу, в которой будут храниться наши приветствия.
Выполним следующие SQL-команды:
CREATE DATABASE hellogwt;
USE hellogwt;

CREATE TABLE greetings (
id INT NOT NULL AUTO_INCREMENT,
author VARCHAR(30),
text VARCHAR(50),
PRIMARY KEY (id)
);

Для работы MyBatis необходимо добавить новые объявления бинов в applicationContext.xml.
В MyBatis каждому объекту доменной модели соответствует определенный мэппер (mapper). С помощью него можно выполнять SQL-запросы, связанные с данным объектом. Чтобы не объявлять каждый мэппер как отдельный бин, мы объявляем сканер мэпперов и указываем ему пакет с мэпперами.
В нашем случае это - com.hellogwt.server.persistence.
Данные для работы с MySQL считываются из файла jdbc.properties:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="com.hellogwt"/>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="propertyConfigurer"
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
          p:location="/WEB-INF/jdbc.properties"/>

    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          p:driverClassName="${jdbc.driverClassName}"
          p:url="${jdbc.url}"
          p:username="${jdbc.username}"
          p:password="${jdbc.password}"/>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.hellogwt.server.persistence"/>
    </bean>

</beans>

Создадим в директории WEB-INF файл jdbc.properties, который будет содержать настройки для работы с MySQL:
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/hellogwt
jdbc.username=root
jdbc.password=root

Создадим объект доменной модели, представляющий наше приветствие. Так как класс Greeting будет использоваться и серверной, и клиентской частями приложения, его необходимо создать в пакете com.hellogwt.shared.domain:
package com.hellogwt.shared.domain;

import java.io.Serializable;

public class Greeting implements Serializable {

    private Integer id;
    private String author;
    private String text;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

Создадим в пакете com.hellogwt.server.persistence мэппер, который позволит производить основные операции с нашими приветствиями. Можно было бы описать операции в XML, но мы сделаем это с помощью аннотаций:
package com.hellogwt.server.persistence;

import com.hellogwt.shared.domain.Greeting;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface GreetingMapper {

    @Select("SELECT * FROM greetings WHERE text = #{text}")
    Greeting getGreeting(@Param("text") String text);

    @Select("INSERT INTO greetings (author, text) VALUES (#{author}, #{text})")
    void addGreeting(@Param("author") String author, @Param("text") String text);

    @Select("UPDATE greetings SET author = #{author} WHERE text = #{text}")
    void updateGreeting(@Param("author") String author, @Param("text") String text);

    @Select("DELETE FROM greetings WHERE text = #{text}")
    void deleteGreeting(@Param("text") String text);

    @Select("SELECT * FROM greetings")
    List<Greeting> getGreetings();
}

Укажем в файле HelloGWT.gwt.xml, что необходимо компилировать в JavaScript не только классы из пакета com.hellogwt.client, но и com.hellogwt.shared:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.6.2//EN"
        "http://google-web-toolkit.googlecode.com/svn/tags/1.6.2/distro-source/core/src/gwt-module.dtd">
<module rename-to='hellogwt'>
    <inherits name='com.google.gwt.user.User'/>
    <inherits name='com.google.gwt.user.theme.standard.Standard'/>
    <entry-point class='com.hellogwt.client.HelloGWT'/>

    <source path='client'/>
    <source path='shared'/>
</module>

Добавим все методы для работы с приветствиями в сервис GreetingService:
package com.hellogwt.client.service;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
import com.hellogwt.shared.domain.Greeting;

import java.util.List;

@RemoteServiceRelativePath("springGwtServices/greetingService")
public interface GreetingService extends RemoteService {

    Greeting getGreeting(String text);

    void addGreeting(String author, String text);

    void updateGreeting(String author, String text);

    void deleteGreeting(String text);

    List<Greeting> getGreetings();
}

Соответственно, не забудем обновить содержимое GreetingServiceAsync:
package com.hellogwt.client.service;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.hellogwt.shared.domain.Greeting;

import java.util.List;

public interface GreetingServiceAsync {

    void getGreeting(String text, AsyncCallback<Greeting> async);

    void addGreeting(String author, String text, AsyncCallback<Void> async);

    void updateGreeting(String author, String text, AsyncCallback<Void> async);

    void deleteGreeting(String text, AsyncCallback<Void> async);

    void getGreetings(AsyncCallback<List<Greeting>> async);
}

Далее, реализуем все методы для работы с приветствиями в реализации сервиса GreetingServiceImpl с помощью мэппера:
package com.hellogwt.server.service;

import com.hellogwt.client.service.GreetingService;
import com.hellogwt.server.persistence.GreetingMapper;
import com.hellogwt.shared.domain.Greeting;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("greetingService")
public class GreetingServiceImpl implements GreetingService {

    @Autowired
    private GreetingMapper greetingMapper;

    @Override
    public Greeting getGreeting(String text) {
        return greetingMapper.getGreeting(text);
    }

    @Override
    public void addGreeting(String author, String text) {
        greetingMapper.addGreeting(author, text);
    }

    @Override
    public void updateGreeting(String author, String text) {
        greetingMapper.updateGreeting(author, text);
    }

    @Override
    public void deleteGreeting(String text) {
        greetingMapper.deleteGreeting(text);
    }

    @Override
    public List<Greeting> getGreetings() {
        return greetingMapper.getGreetings();
    }
}

Теперь мы можем взаимодействовать с базой данных в нашем приложении. Изменим класс HelloGWT так, чтобы он отображал все приветствия и позволял добавлять, редактировать и удалять их:
package com.hellogwt.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.hellogwt.client.service.GreetingService;
import com.hellogwt.client.service.GreetingServiceAsync;
import com.hellogwt.shared.domain.Greeting;

import java.util.List;

public class HelloGWT implements EntryPoint {
    private GreetingServiceAsync greetingService = GWT.create(GreetingService.class);

    private HorizontalPanel authorPanel = new HorizontalPanel();
    private HorizontalPanel textPanel = new HorizontalPanel();
    private HorizontalPanel editPanel = new HorizontalPanel();

    private Label authorLabel = new Label("Author:");
    private Label textLabel = new Label("Text:");
    private TextBox authorTextBox = new TextBox();
    private TextBox textTextBox = new TextBox();

    private Button addButton = new Button("Add");
    private Button updateButton = new Button("Update");
    private Button deleteButton = new Button("Delete");

    private FlexTable greetingsFlexTable = new FlexTable();

    @Override
    public void onModuleLoad() {
        initWidgets();
        initHandlers();

        refreshGreetingsTable();
    }

    private void initWidgets() {
        authorLabel.setWidth("50");
        authorTextBox.setWidth("100");
        authorPanel.add(authorLabel);
        authorPanel.add(authorTextBox);

        textLabel.setWidth("50");
        textTextBox.setWidth("100");
        textPanel.add(textLabel);
        textPanel.add(textTextBox);

        addButton.setWidth("50");
        updateButton.setWidth("50");
        deleteButton.setWidth("50");
        editPanel.add(addButton);
        editPanel.add(updateButton);
        editPanel.add(deleteButton);

        RootPanel.get().add(authorPanel);
        RootPanel.get().add(textPanel);
        RootPanel.get().add(editPanel);
        RootPanel.get().add(greetingsFlexTable);
    }

    private void initHandlers() {
        final AsyncCallback<Void> callback = new AsyncCallback<Void>() {
            @Override            
            public void onFailure(Throwable caught) {
                Window.alert("ERROR: Cannot edit greetings!");
            }

            @Override
            public void onSuccess(Void result) {
                refreshGreetingsTable();
            }
        };

        addButton.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent clickEvent) {
                if (!authorTextBox.getText().isEmpty() &&                                
                        !textTextBox.getText().isEmpty()) {
                    greetingService.getGreeting(textTextBox.getText(), 
                            new AsyncCallback<Greeting>() {
                        @Override
                        public void onFailure(Throwable caught) {
                            Window.alert("ERROR: Cannot find greeting!");
                        }

                        @Override
                        public void onSuccess(Greeting result) {
                            if (result == null) {
                                greetingService.addGreeting(authorTextBox.getText(), 
                                        textTextBox.getText(), callback);
                            } else {
                                Window.alert("Greeting already exists!");
                            }
                        }
                    });
                } else {
                    Window.alert("\"Author\" and \"Text\" fields cannot be empty!");
                }
            }
        });

        updateButton.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                if (!authorTextBox.getText().isEmpty() &&  
                        !textTextBox.getText().isEmpty()) {
                    greetingService.updateGreeting(authorTextBox.getText(), 
                            textTextBox.getText(), callback);
                } else {
                    Window.alert("\"Author\" and \"Text\" fields cannot be empty!");
                }
            }
        });

        deleteButton.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                greetingService.deleteGreeting(textTextBox.getText(), callback);
            }
        });
    }

    private void refreshGreetingsTable() {
        greetingService.getGreetings(new AsyncCallback<List<Greeting>>() {
            @Override
            public void onFailure(Throwable throwable) {
                Window.alert("ERROR: Cannot load greetings!");
            }

            @Override
            public void onSuccess(List<Greeting> greetings) {
                fillGreetingsTable(greetings);
            }
        });
    }

    private void fillGreetingsTable(List<Greeting> greetings) {
        greetingsFlexTable.removeAllRows();

        greetingsFlexTable.setText(0, 0, "Author");
        greetingsFlexTable.setText(0, 1, "Text");

        for (Greeting greeting : greetings) {
            int row = greetingsFlexTable.getRowCount();

            greetingsFlexTable.setText(row, 0, greeting.getAuthor());
            greetingsFlexTable.setText(row, 1, greeting.getText());
        }
    }
}

Соберем проект, выполнив команду "mvn clean install":
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building hellogwt 1.0
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ hellogwt ---
[INFO] Deleting d:\Dev\blog\hellogwt-4\target
[INFO] 
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ hellogwt ---
[debug] execute contextualize
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ hellogwt ---
[WARNING] File encoding has not been set, using platform encoding Cp1252, i.e. build is platform dependent!
[INFO] Compiling 6 source files to d:\Dev\blog\hellogwt-4\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:2.5:testResources (default-testResources) @ hellogwt ---
[debug] execute contextualize
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory d:\Dev\blog\hellogwt-4\src\test\resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ hellogwt ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.10:test (default-test) @ hellogwt ---
[INFO] No tests to run.
[INFO] Surefire report directory: d:\Dev\blog\hellogwt-4\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------

Results :

Tests run: 0, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- gwt-maven-plugin:2.4.0:compile (default) @ hellogwt ---
[INFO] auto discovered modules [com.hellogwt.HelloGWT]
[INFO] Compiling module com.hellogwt.HelloGWT
[INFO]    Compiling 6 permutations
[INFO]       Compiling permutation 0...
[INFO]       Process output
[INFO]          Compiling
[INFO]             Compiling permutation 1...
[INFO]       Process output
[INFO]          Compiling
[INFO]             Compiling permutation 3...
[INFO]       Process output
[INFO]          Compiling
[INFO]             Compiling permutation 2...
[INFO]       Compiling permutation 4...
[INFO]          Compiling
[INFO]             Compiling permutation 5...
[INFO]    Compile of permutations succeeded
[INFO] Linking into d:\Dev\blog\hellogwt-4\target\hellogwt\hellogwt
[INFO]    Link succeeded
[INFO]    Compilation succeeded -- 38.586s
[INFO] 
[INFO] --- maven-war-plugin:2.1.1:war (default-war) @ hellogwt ---
[INFO] Packaging webapp
[INFO] Assembling webapp [hellogwt] in [d:\Dev\blog\hellogwt-4\target\hellogwt]
[INFO] Processing war project
[INFO] Copying webapp resources [d:\Dev\blog\hellogwt-4\src\main\webapp]
[INFO] Webapp assembled in [201 msecs]
[INFO] Building war: d:\Dev\blog\hellogwt-4\target\hellogwt.war
[WARNING] Warning: selected war files include a WEB-INF/web.xml which will be ignored 
(webxml attribute is missing from war task, or ignoreWebxml attribute is specified as 'true')
[INFO] 
[INFO] --- gwt-maven-plugin:2.4.0:test (default) @ hellogwt ---
[INFO] 
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ hellogwt ---
[INFO] Installing d:\Dev\blog\hellogwt-4\target\hellogwt.war to d:\Dev\.m2\repository\hellogwt\hellogwt\1.0\hellogwt-1.0.war
[INFO] Installing d:\Dev\blog\hellogwt-4\pom.xml to d:\Dev\.m2\repository\hellogwt\hellogwt\1.0\hellogwt-1.0.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 45.005s
[INFO] Finished at: Mon Apr 21 19:48:43 EEST 2014
[INFO] Final Memory: 15M/153M
[INFO] ------------------------------------------------------------------------

Скопируем полученный war-файл в директорию Tomcat'а webapps, запустим сервер и перейдем по ссылке http://localhost:8080/hellogwt/. Должна отобразиться страница с формой редактирования и списком всех приветствий:















Структура проекта:












































Исходный код созданного приложения доступен по ссылке http://code.google.com/p/hellogwt-4/.

В этой части мы смогли добавить в наше приложение поддержку работы с MySQL с помощью фреймворка MyBatis, и оно лишилось своего основного, но не единственного недостатка :) В следующей части мы устраним еще один - перенесем описание UI из кода в XML.


Рекомендуемые посты:

1 comment:

  1. Так как класс Greeting будет использоваться и серверной, и клиентской частями приложения, его необходимо создать в пакете com.hellogwt.shared.domain


    А почему именно shared? И за что отвечает данный пакет?

    ReplyDelete