lördag 2 februari 2013

GAE - Google App Engine, JPA2, Maven and Eclipse

When I started to build applications on the Google App Engine platform I found it quite hard to get started. I am used to use Maven and develop in Eclipse, and it took a while to get all running. Most of the guides and tutorial I found was either old or did miss some parts in the setup.

In this post I will try to write down how you get starting with GAE, JPA2, Maven and Eclipse. If you have not used GAE before, have a look at my previous post about getting started with GAE and Maven, http://loop81.blogspot.se/2013/01/gae-getting-started-with-google-app.html.

If you just like to see the source code of the project with JPA configured follow this link: http://files.loop81.com/rest/download/7bbf263b-abf2-4703-93c2-75b0eb2f49c2.

So let us get started. I assume that you have a Eclipse installation with Maven configured, otherwise get it first. When you have a Eclipse you will need the Google App Engine plugin. This plugin simplify launching and development for you. However we will still setup our project so we can use command-line any time. Easiest way to install the Eclipse plugin it is to select it from the Eclipse Market or follow these instructions, https://developers.google.com/appengine/docs/java/tools/eclipse.

To begin we need to have a GAE application. If you do not know how to setup a GAE project please see http://loop81.blogspot.se/2013/01/gae-getting-started-with-google-app.html, or download the project created in the previous post from: http://files.loop81.com/files/rest/download/92a771ad-7aef-43bc-9438-ffc70a358874

First of we will need some new dependencies to be able to start using JPA2. When reading the Google documentation https://developers.google.com/appengine/docs/java/datastore/jpa/overview-dn2 it specifies that we need the binaries found within appengine-java-sdk/lib/opt/user/datanucleus/v2/. Using that information gives us the following Mavens dependencies:
...
  <datanucleus.jpa.version>3.1.1</datanucleus.jpa.version>
...

<dependency>
  <groupId>org.datanucleus</groupId>
  <artifactId>datanucleus-api-jpa</artifactId>
  <version>${datanucleus.jpa.version}</version>
</dependency>

<dependency>
  <groupId>org.datanucleus</groupId>
  <artifactId>datanucleus-core</artifactId>
  <version>${datanucleus.jpa.version}</version>
</dependency>

<dependency>
  <groupId>com.google.appengine.orm</groupId>
  <artifactId>datanucleus-appengine</artifactId>
  <version>2.1.1</version>

  <!-- Need to exclude the enhancer since it interfere with the enhancer plugin. -->
  <exclusions>
    <exclusion>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-enhancer</artifactId>
    </exclusion>
  </exclusions>
</dependency>

<dependency>
  <groupId>javax.jdo</groupId>
  <artifactId>jdo-api</artifactId>
  <version>3.0.1</version>
</dependency>

<dependency>
  <groupId>org.apache.geronimo.specs</groupId>
  <artifactId>geronimo-jpa_2.0_spec</artifactId>
  <version>1.1</version>
</dependency>
The dependencies are quite straight forward. However note the comment on line 22. We will get back to this later on, but the reason for exclude the enhancer is because it will interfere with the maven plugin for entity enhancement, which we will be using later on.

Enhancing entities
GAE is using the DataNucleus implementation of JPA2 and that implementation requires the classes to be enhanced with various functionality to work. There are a couple of different methods to use like runtime-enhancement where entities get enhanced when loaded into the JVM or as DataNucleus use post-compilation enhancement. With this method entities are enhanced after compilation. To get the enhancement up and running we will be using the maven-datanucleus-plugin, http://www.datanucleus.org/products/accessplatform/enhancer.html.
<plugin>
  <groupId>org.datanucleus</groupId>
  <artifactId>maven-datanucleus-plugin</artifactId>
  <version>${datanucleus.jpa.version}</version>
  
  <configuration>
    <api>JPA</api>
    <mappingIncludes>**/entities/*.class</mappingIncludes>
    <verbose>true</verbose>
  </configuration>
  
  <dependencies>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-core</artifactId>
      <version>${datanucleus.jpa.version}</version>
    </dependency>
  </dependencies>
  
  <executions>
    <execution>   
      <phase>compile</phase>
      <goals>
        <goal>enhance</goal>
      </goals>
    </execution>
  </executions>
</plugin>
Line 7: Tells the plugin to use the JPA API. JDO is also supported, but I will not discuss that in this post.
Line 8: Specifies which classes we should enhance. I will place all my entities in a package called entities. Make sure that you do not enhance any other classes, since that can create some weird errors.
Line 21-26: Tell the plugin in which stage of the Maven build it should enhance the classes, "in the phase compile run the goal enhance of this plugin".

If you would run mvn compile in a comand-line everything would work, but in Eclipse you will see a error like: "Plugin execution not covered by lifecycle configuration: org.datanucleus:maven-datanucleus-plugin:3.1.1:enhance (execution: default, phase: compile)". The reason for this to not working as expected is called the "m2eclipse dance". Basically it is all about different plugins being configured differently. Some at workspace level, some at project level and so on. To solve this problem one is required to tell m2e (the Eclipse Maven plugin) what to do.
<pluginManagement>
  <plugins>
    <plugin>
      <groupId>org.eclipse.m2e</groupId>
      <artifactId>lifecycle-mapping</artifactId>
      <version>1.0.0</version>
      
      <configuration>
        <lifecycleMappingMetadata>
          <pluginExecutions>
            <pluginExecution>
              <pluginExecutionFilter>
                <groupId>org.datanucleus</groupId>
                <artifactId>maven-datanucleus-plugin</artifactId>
                <versionRange>${datanucleus.jpa.version}</versionRange>
              
                <goals>
                  <goal>enhance</goal>
                </goals>
              </pluginExecutionFilter>

              <action>
                <execute >
                  <runOnIncremental>false</runOnIncremental>
                </execute >
              </action>
            </pluginExecution>
          </pluginExecutions>
        </lifecycleMappingMetadata>
      </configuration>
    </plugin>
  </plugins>
</pluginManagement>
By adding this plugin under plugin management we tell m2e that it is okay to run the plugin. You might have to "update the project" from the Maven menu to get the error to disappear.

A simple servlet
Now we are ready to start implementing our code which should use JPA. The idea here is to create something super simple: A servlet which for each request logs the request time and then list all historical request. Lets start with create a servlet. We will need to add the servlet-API, create the servlet and configure it into the web.xml. I know boring... but we need something which can use JPA.

pom.xml
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.0.1</version>
</dependency>
web.xml
<servlet>
  <servlet-name>The servlet</servlet-name>
  <servlet-class>com.loop81.gae.jpa2_test.TheServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>The servlet</servlet-name>
  <url-pattern>jpa</url-pattern>
</servlet-mapping>
And last the servlet. I named my serlvet com.loop81.gae.jpa2_test.TheServlet.
public class TheServlet extends HttpServlet {

 private static final long serialVersionUID = 1L;

 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
   throws ServletException, IOException {
  resp.setContentType("text/html");
  
  PrintWriter writer = resp.getWriter();
  writer.write("Servlet is so 1997");
 }
}
To try the servlet you can start the application using the gae-plugin in Eclipse or command-line mvn gae:start and browse to the page. From my Eclipse: http://localhost:8888/jpa.

Time for JPA!
Finnaly. To enable JPA we need to add a percistance.xml which describes how DataNucleus should connect to the datastore whin GAE. We will also need to update the servlet to actually use JPA. Let us start with the percistance.xml (some part of the file is omitted, see the complete project for a complete file).
<persistence-unit name="gae-test">
  <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
  <properties>
    <property name="datanucleus.NontransactionalRead" value="true" />
    <property name="datanucleus.NontransactionalWrite" value="true" />
    <property name="datanucleus.ConnectionURL" value="appengine" />
    <property name="datanucleus.singletonEMFForName" value="true" />
  </properties>
</persistence-unit>
Nothing special in the percistance.xml then the name on line 1, gae-test. This name will be used in the code for getting an EntityManager. Next is a simple entity. I will not go into how JPA works. A good starting point JPA is to check out: http://www.vogella.com/articles/JavaPersistenceAPI/article.html.
@Entity
public class RequestLog {
 
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Key id;
 
 private Date requestTime;
 
 // Getters
}
As with the percistance.xml nothing fancy. On line 1 we say that this is a entity, and on line 5 we tell the implementation to give us a generated unique identifier to the entity. I suggest you check out the JavaDoc of the Key and KeyFactory classes. Last but not least, let us update the servlet to actually use JPA.
public class TheServlet extends HttpServlet {

 private static final long serialVersionUID = 1L;
 
 private static final EntityManagerFactory emf =
         Persistence.createEntityManagerFactory("gae-test");

 @SuppressWarnings("unchecked")
 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
   throws ServletException, IOException {
  resp.setContentType("text/html");
  
  EntityManager entityManager = emf.createEntityManager();
  entityManager.getTransaction().begin();
  entityManager.persist(new RequestLog(new Date()));
  entityManager.getTransaction().commit();
  
  PrintWriter writer = resp.getWriter();
  
  writer.write("Servlet is so 1997.. but here are some requests made:\n");
  List resultList =  entityManager.createQuery("select r from RequestLog r")
    .getResultList();
  for (RequestLog number : resultList) {
   writer.write(number.getId().toString() + " " + number.getRequestTime() + "\n");
  }
  entityManager.close();
 }
}
Line 5: Here we uses the name defined in the percistance.xml to get a EntityManagerFactory. The reason for making it a static variable is that it can take a while to initialize the factory, and we would not like to do that each request. Also one should note that in a real application you probably would have the EntityManagerFactory in a provider class or use some other framework like Spring.

Line 14-17: Here we simple create a EntityManager and a transaction which we use to save a new RequestLog.

Line 22: Fetches all RequestLog entries from the datastore which we later on prints to the output.

That is all. If you have mange to get all parts correctly you should have a running GAE-application which uses JPA2. However I have gotten a error within Eclipse sometimes saying: " Plugin (Bundle) "org.datanucleus" is already registered. Ensure you dont have multiple JAR versions of the same plugin in the classpath." I have not manage to find out exactly way the enhancing fails, but I believe it has to do with the enhancement plugin and the GAE-plugin within Eclipse. To solve the problem do a clean on the project and things should work.

For the complete project follow this link: http://files.loop81.com/rest/download/7bbf263b-abf2-4703-93c2-75b0eb2f49c2.

6 kommentarer:

  1. Thanks for posting, this was quite helpful.

    SvaraRadera
  2. [ERROR] Failed to execute goal org.datanucleus:maven-datanucleus-plugin:3.1.3:enhance (default) on project experiment: Error executing DataNucleus tool org.datanucleus.enhancer.DataNucleusEnhancer: I
    vocationTargetException: Plugin (Bundle) "org.datanucleus" is already registered. Ensure you dont have multiple JAR versions of the same plugin in the classpath. The URL "file:/e:/java/maven_reposito
    y/org/datanucleus/datanucleus-core/3.1.3/datanucleus-core-3.1.3.jar" is already registered, and you are trying to register an identical plugin located at URL "file:/E:/java/maven_repository/org/datan
    cleus/datanucleus-core/3.1.5/datanucleus-core-3.1.5.jar." -> [Help 1]

    SvaraRadera
  3. sorry but all the download links are dead. Can you please fix this?

    This is THE best single source of information on the issue! Thank you so much!!!

    SvaraRadera
  4. Can you please let me know the source code of http://www.loop81.com/2013/02/gae-google-app-engine-jpa2-maven-and.html program
    Thanks

    SvaraRadera