Monday, November 14, 2011

Spring and GWT tutorial. Part 4 - MyBatis and MySQL

Recently I've started working in a GWT project. It is a new technology for me, so I've decided to create a simple web application based on Spring and GWT.

This post has a Russian version.

In previous parts we have created an application that has some working functionality. It is a good start. But our app lacks one important thing - it cannot work with database.

Let's make it work with:
  • MyBatis 3.0.5
  • MySQL 5.1

For that add mybatis-spring and mysql-connector-java artifacts into 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>
    <name>hellogwt</name>

    <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>
        <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>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>tomcat-maven-plugin</artifactId>
                <configuration>
                    <url>http://localhost:8080/manager</url>
                    <server>tomcat</server>
                    <path>/hellogwt</path>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Our application will let user to list and edit different greetings. Every greeting has author and text.
We should create a database and a table using MySQL. Execute following SQL commands:
CREATE DATABASE hellogwt;
USE hellogwt;

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

To make MyBatis work we need to add new bean definitions into applicationContext.xml.
If we use MyBatis then every domain object must have a mapper. Mapper allows to execute SQL requests connected with domain object. We can register every mapper as a bean, but also we can use a mapper scanner that knows about the package where all mappers are located. In our case that is com.hellogwt.server.persistence package.
The data that is needed to connect to MySQL database is read from jdbc.properties file:
<?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>

Create jdbc.properties file in WEB-INF directory:
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/hellogwt
jdbc.username=root
jdbc.password=root

Create a domain object that represents a greeting. Class Greeting is used by server and client parts of application, so it should be created in com.hellogwt.shared.domain package:
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;
    }
}

Create a mapper that will allow to perform main operations with our greetings in com.hellogwt.server.persistence package. We could declare all operations in XML, but we will do it using annotations:
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();
}

We need to compile to JavaScript classes from com.hellogwt.client and com.hellogwt.shared packages. So respectively modify HelloGWT.gwt.xml file:
<?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'>
    <!-- Inherit the core Web Toolkit stuff.                        -->
    <inherits name='com.google.gwt.user.User'/>

    <!-- Inherit the default GWT style sheet.  You can change       -->
    <!-- the theme of your GWT application by uncommenting          -->
    <!-- any one of the following lines.                            -->
    <inherits name='com.google.gwt.user.theme.standard.Standard'/>
    <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
    <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>     -->

    <!-- Other module inherits                                      -->

    <!-- Specify the app entry point class.                         -->
    <entry-point class='com.hellogwt.client.HelloGWT'/>

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

Declare all methods to work with greetings in service 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();
}

Also, don't forget to edit the contents of 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);
}

Implement all declared methods in service GreetingServiceImpl using created mapper:
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();
    }
}

Now we can communicate with database in our application. Change class HelloGWT, it should allow to list, add, edit and delete greetings:
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());
        }
    }
}

Build and deploy application by executing command "mvn clean tomcat:deploy":
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for hellogwt:hellogwt:war:1.0
[WARNING] 'build.plugins.plugin.version' for org.codehaus.mojo:tomcat-maven-plugin is missing. @ line 76, column 21
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building hellogwt 1.0
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ hellogwt ---
[INFO] Deleting D:\Work\hellogwt\target
[INFO] 
[INFO] >>> tomcat-maven-plugin:1.1:deploy (default-cli) @ hellogwt >>>
[INFO] 
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ hellogwt ---
[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:\Work\hellogwt\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ hellogwt ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\Work\hellogwt\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.7.2:test (default-test) @ hellogwt ---
[INFO] No tests to run.
[INFO] Surefire report directory: D:\Work\hellogwt\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
There are no tests to run.

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 1...
[INFO]       Process output
[INFO]          Compiling
[INFO]             Compiling permutation 0...
[INFO]       Compiling permutation 2...
[INFO]       Compiling permutation 3...
[INFO]          Compiling
[INFO]             Compiling permutation 4...
[INFO]       Compiling permutation 5...
[INFO]    Compile of permutations succeeded
[INFO] Linking into D:\Work\hellogwt\target\hellogwt-1.0\hellogwt
[INFO]    Link succeeded
[INFO]    Compilation succeeded -- 30.131s
[INFO] 
[INFO] --- maven-war-plugin:2.1.1:war (default-war) @ hellogwt ---
[INFO] Packaging webapp
[INFO] Assembling webapp [hellogwt] in [D:\Work\hellogwt\target\hellogwt-1.0]
[INFO] Processing war project
[INFO] Copying webapp resources [D:\Work\hellogwt\src\main\webapp]
[INFO] Webapp assembled in [308 msecs]
[INFO] Building war: D:\Work\hellogwt\target\hellogwt-1.0.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] <<< tomcat-maven-plugin:1.1:deploy (default-cli) @ hellogwt <<<
[INFO] 
[INFO] --- tomcat-maven-plugin:1.1:deploy (default-cli) @ hellogwt ---
[INFO] Deploying war to http://localhost:8080/hellogwt  
[INFO] OK - Deployed application at context path /hellogwt
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 37.890s
[INFO] Finished at: Fri Nov 18 00:00:40 EET 2011
[INFO] Final Memory: 16M/309M
[INFO] ------------------------------------------------------------------------

Go to http://localhost:8080/hellogwt/. Page with edit form and greetings list should be displayed:















Folder structure:












































Application source code is available at http://code.google.com/p/hellogwt-4/.

We have successfully added a MySQL database support to our application using MyBatis.
Now our app has no lacks :) But we will still impove it in the next part by describing UI in XML instead of Java code.

15 comments:

  1. Really ...
    A good article.

    I have a question Alex, can I use xml files instead of annotation in Mybatis and GWT?
    I have experience of iBatis and Spring but sfrom last 2 years not in touch with both, so just want to know it.

    Once again really a nice post.

    ReplyDelete
    Replies
    1. Thank you, Atul.
      Of course, you can. Look at MyBatis User Guide and JPetStore code example.

      Delete
  2. i should have visited this series earlier.. superb efforts. thank you very much.

    ReplyDelete
  3. Hi Alex, I am new to GWT and need to integrate it with Spring and Hibernate, and I found your blog articles very helpful in explaining step-by step process. Is there any chance you could upload the source code (there is no downloadables at http://code.google.com/p/hellogwt-4/). I have some glitches in my code config and hope it can help me to "find the differences".

    Natalia

    ReplyDelete
    Replies
    1. Natalia, you can get the source code via SVN using link listed at http://code.google.com/p/hellogwt-4/source/checkout page. Also, you can browse any project file at "Browse" tab of the same page.

      Delete
    2. Great - thank you, I somehow missed these links before.

      Delete
  4. Excellent article Thanks Alex.

    Kuhajeyan

    ReplyDelete
  5. Hi Alex,

    Alex did a great job .....
    I have successfully executed the application got the app to work.If anyone find it difficult in configuring without Maven .Please do mail[achris1985@gmail.com] or call me +919600706968.

    Thanks,
    Christopher

    ReplyDelete