Tuesday, May 26, 2009

Cloud to RIA: Accessing Google App Engine (Java) Data Through Flex AMF - Part III

[Part I] [Part II] [Part III]

JPA Entity

Now that we have the client side set up, let's prepare the server side, starting with the JPA entity that will store the data. For this example, I'm going to use just two fields: "key" (primary key) and "value." For those of you relational folks new to GAE/JPA, there is no "table" per se: you declare a JPA "entity," which is a Java class annotated to be persistent. Then, to "insert" a "row," you create a class instance and "persist" it. GAE/J will associate all the class instances together as if they were rows in a table.

Create the below Java class DataItem.java in package com.acme.data (or change as necessary).

package com.acme.data;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity public class DataItem {
@Id String key;
String value;
public DataItem(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

As you can see, this is ridiculously simple. All you had to do to make persistent was to add the @Entity annotation for the class and an @Id to mark "key" as the primary key. You do need to add getters/setters however: making the fields public and directly manipulating them will NOT work! Eclipse can automatically generate getters/setters for you - use Right Click->Source on the Java source.

EntityManagerFactory

Next, we need to create a POJO to do create/read/update/delete on the JPA entity we just declared. The basic steps are as follows:
  1. Get an EntityManagerFactory instance
  2. Create an EntityManager
  3. Get an EntityTransaction (if doing transactional work)
  4. Do your thing
The first step (get EntityManagerFactory) you want to do through a singleton because the operation is VERY expensive (10-20 seconds is pretty typical). The way AMF works is that it has servlets running on the server listening for requests from Flex RemoteObject (see web.xml in Part II). It is VERY important that you define the scope for these servlets as "application," meaning the servlets start once and will continue to run till the next update (see services-config.xml in Part II). If you set the scope as "session" (or worse, "request"), the servlets will keep restarting, creating EntityManagerFactory afresh, severely reducing the responsiveness.

Create a Java class EMF.java in package com.acme.data with the below code.

package com.acme.data;

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class EMF {
private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("transactions-optional");
public static EntityManagerFactory get() {
return emf;
}
private EMF() {
}
}

POJO Interface

The final step is to create POJO that GraniteDS will use to access the JPA entities. It's the methods in this POJO that your RemoteObject uses (see createData, readValue, updateValue, and deleteData in the Data.mxml RemoteObject tag in Part II).

package com.acme.data;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

public class Data {
public void createData(String key, String value) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction tx = em.getTransaction();
DataItem dataItem = new DataItem(key, value);
try {
tx.begin();
em.persist(dataItem);
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
em.close();
}
}
public String readValue(String key) {
EntityManager em = EMF.get().createEntityManager();
return em.find(DataItem.class, key).getValue();
}
public void updateValue(String key, String value) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction tx = em.getTransaction();
DataItem dataItem = em.find(DataItem.class, key);
dataItem.setValue(value);
try {
tx.begin();
em.merge(dataItem);
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
em.close();
}
}
public void deleteData(String key) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction tx = em.getTransaction();
DataItem dataItem = em.find(DataItem.class, key);
try {
tx.begin();
em.remove(dataItem);
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
em.close();
}
}
}

There's really nothing to this code, it's quite self-explanatory. I've simplified it to the extent possible, and as you get familiar with the process, you can explore other APIs (like em.createQuery).

BUILD/PUBLISH

The final step is to build and publish to GAE. To build the release version of the Flex app, select the project name in the Eclipse left pane and Right Click->Export->Flex Builder->Release Build. Point "Export to folder" to your war/ directory.

Next, create a GAE app (if you haven't already) and deploy your code by clicking the little GAE logo on the top left in Eclipse (or click on the project name in the left pane and Right Click->Google->Deploy to App Engine). If you don't have GAE Java access (see Part I), this operation will fail with a somewhat cryptic message.

That's it. Open your app with http://yourapp.appspot.com/Data.html and see the magic. If there's no magic, may be I missed something, so please post back here so I can fix it. If there IS magic, please do post comments/opinions. Happy coding!

37 comments:

Flex developer said...

Very interesting post, thanks.

I tried to follow all the tutorial, but get lost at the last point.

Which is the name of the class and the package of POJO Interface?

Aparently JPA Entity and POJO Interface have the same name.

Sekhar Ravinutala said...

My bad, forgot to change the Entity name. I've renamed it to DataItem and changed the references. Thanks for pointing this out! And please do post back after you've tried it out.

Anil Kommareddi said...

Sekhar - couple of issues:
There is no data.html in the project as referenced in the URL. I just changed it to invoke Data.swf directly. The UI comes up, but when trying to create a record, get the following error:

[RPC Fault faultString="[MessagingError message='Destination 'Data' either does not exist or the destination has no channels defined (and the application does not define any default channels.)']" faultCode="InvokeFailed" faultDetail="Couldn't establish a connection to 'Data'"]

Sekhar Ravinutala said...

Anil, it's Data.html (not data.html). Also, there's an issue with HTML template that Flex uses to create the HTML. I've added explicit steps to handle this, please re-read the installation section.

WRT the connection error, JPA needs a persistence.xml file, which GAE doesn't create automatically in the current release. So, you'll need to create this manually and tell the Flex compiler about it. I've added these steps as well.

Anyway, I've installed the entire thing from scratch on a fresh laptop and new GAE app, and I've verified that the steps in this tutorial will create a working app. So, everything should work. Do go over the tutorial again so you pick up the changes.

Anil Kommareddi said...

Thanks Sekhar - works now.

Andrew said...

Thank you very much for this excellent tutorial. I almost have it working now, but I have one more problem to get past. When I try to create a record, I get the following error:

[RPC Fault faultString="Could not initialize class com.acme.gae.data.EMF" faultCode="DefaultServiceExceptionHandler.Call.Failed" faultDetail="
- destination: Data
- method: public java.lang.String com.acme.gae.data.Data.readValue(java.lang.String)
- exception: java.lang.NoClassDefFoundError: Could not initialize class com.acme.gae.data.EMF

For some reason, it can't see my EMF class. Any ideas?

Thanks!

Sekhar Ravinutala said...

Andrew, glad you like the tut. Looks like you added a "gae" in the package name over what's in the tut. I'd double-check to make sure you propagated this change everywhere.

Andrew said...

Thanks Sekhar. Actually, I just figured it out. My Eclipse IDE was making several copies of persistence.xml in different locations. I deleted all of them and directly added it to war/WEB-INF/classes/META-INF rather than relying on Eclipse to copy it over from src/.

It works great now. I'm looking forward to writing my own apps now that I have a working framework. I greatly appreciate you sharing this with us.

Anil said...

Very good article..
cant wait to get home and try this out!

Ben said...

Sekhar,
Great post. I was to able to get up and running with AMF and GAE very easily with this post. However, I did come across a bug of sorts when I needed to save a text object. Apparently, above your property declaration you need to add:
@Persistent(defaultFetchGroup="true")

I've posted a blog entry about this if anyone else comes across it:

http://www.themorphicgroup.com/blog/2009/07/02/google-app-engine-text-datastore-object-not-saving-with-amf/

Jason said...

Great post. I was able to get up and running with little snags. Thanks for putting this together!

I am stuck on one issue and was hoping that someone could clear it up. Whenever I try and create a POJO that is more complex I get a java.lang.NoSuchMethodException exception.

Does GraniteDS with GAE only support simple key:value pairs?

Is it even possible to persist on object like this:

public void createData(String key, List< MyCustomObject1> myList1, List< String> myList2) {
EntityManager em = EMF.get().createEntityManager();
EntityTransaction tx = em.getTransaction();

DataItem dataItem = new DataItem(key, myList1, myList2);
try {
tx.begin();
em.persist(stratItem);
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
em.close();
}
}

Tom Martwick said...

Thanks! Works like a charm.

newin flex-gae said...

hi, i follow each step (i believe) but when i test the result in the gae (http://conexionpanama.appspot.com/Data.html)

i receive this message


[RPC Fault faultString="" faultCode="DefaultServiceExceptionHandler.Call.Failed" faultDetail="
- destination: Data
- method: public void com.acme.data.Data.createData(java.lang.String,java.lang.String)
- exception: java.lang.ExceptionInInitializerError.......
.............


please help me, i am new in this gae technology what is bad?

thank

newin flex-gae said...

i not sure if the step for the add "flex compiler" is done rith for me

i added the following line in the
"Additional compilers argument:"

-locale en_US -services ../war/WEB-INF/flex/services-config.xml

Sekhar Ravinutala said...

Is it working for you now, Newin?

Nehul said...

Thanks for the blog. Everything works.

Anonymous said...

Hi, i've followed the tutorial and im getting this error

[RPC Fault faultString="Could not initialize class com.acme.data.EMF" faultCode="DefaultServiceExceptionHandler.Call.Failed" faultDetail="
- destination: Data
- method: public void com.acme.data.Data.createData(java.lang.String,java.lang.String)
- exception: java.lang.NoClassDefFoundError: Could not initialize class com.acme.data.EMF
org.granite.messaging.service.ServiceException: Could not initialize class com.acme.data.EMF
at

Anonymous said...

Thanks you for the nice tut. I've followed it step by step and able to finish with success. I'm having problems while getting KEY from datastore. I'm querying like;

EntityManager em = EMF.get().createEntityManager();
Query query = em.createQuery("select from MenuItem");
return (List) query.getResultList();

Everything works fine except the KEY field, I couldn't get it but all other fields are retrieved. Is there and special way to get the key fields? I've been searching for 3 hours but couldn't find an answer about it. BTW I've declared my key field as;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Extension(vendorName="datanucleus", key="gae.pk-id", value="true")
private Long KEY;

Thanks for the nice tut again.

Jaideep Adhvaryu said...

great post, thanks!

do you have an example of Flex charts with GAE-J.

Am also keen to know if you have any insights of using Flex client v/s GWT in the context of GAE-J

Many thanks!
Jaideep

Swap's Cafeteria said...
This comment has been removed by the author.
Rakesh Chintha said...

Hi Shekar.

I was trying to configure BlazeDS with GAE since few days and I was getting many runtime errors.

I just tried the same with GraniteDS and followed your amazing tutorial and it worked for me.

Thanks for the wonderful tutorial !!

Rakesh Chintha.

Ramón Múgica said...

Good Job, but with Granite about how can we store the data in a httpsession with remote object. In blazeds exist the FlexSession but in Granite...

Thanks a lot

Anonymous said...

Salut! Steve Martinez . payday loans

Paul Georges said...

I'm getting endless class not found errors... the latest that I don't quite understand is this one when trying to run locally...


Caused by: java.lang.ClassNotFoundException: org.granite.messaging.amf.io.convert.impl.GAEKeyConverter

and this one on GAE...

[RPC Fault faultString="Send failed" faultCode="Client.Error.MessageSend" faultDetail="Channel.Connect.Failed error NetConnection.Call.Failed: HTTP: Status 500: url: 'http://granitedsgae.appspot.com/graniteamf/amf'"]

any suggestions?

Cedric said...

Works perfectly for me. Thanks a lot!

Ravi Ramesh Bisht said...

Thanks sir :-)
Exactly what i needed Thanku

Dan said...

It's alive!
great tutorial.

what about following up with the publisher/subscriber service?

many thanks!

illyrian said...

Great job on the tutorial, I have been able to follow the steps and get it to work.
I am trying to build an application using these technologies.
My problem is that I can not figure out how to pass user defined objects from Flex to the server.

Basically, I would like to have the ability to pass Objects back and forth as bellow:

public void createDataItem(Data data) {
....
...
}

I would appreciate some help with this.

Thanks

Angela Han said...

Great article. Thanks for sharing.

Not sure which part is not right. After I followed everything, I kept on getting
[RPC Fault faultString="[MessagingError message='Destination 'Data' either does not exist or the destination has no channels defined (and the application does not define any default channels.)']" faultCode="InvokeFailed" faultDetail="Couldn't establish a connection to 'Data'"]

Any ideas? Thanks!

Angela Han said...

Hey, it actually worked! I forgot to put -services to Flex compiler settings. Everything was written clearly in your article.

Thanks a lot!

Raad Yacu said...

After following the parts, I still get the following:

[RPC Fault faultString="[MessagingError message='Destination 'Data' either does not exist or the destination has no channels defined (and the application does not define any default channels.)']" faultCode="InvokeFailed" faultDetail="Couldn't establish a connection to 'Data'"]

One blog says something about adding -services to Flex Compiler settings but not sure what this all about.

Can you include the source code as zip? Just so that I can compare

Raad Yacu said...

O.k. - I followed it this time to the teeth and it works.

Sekhar, this is an awesome example.

Anonymous said...

I have this:

[WARN] You should configure a deserializer securizer in your granite-config.xml file in order to prevent potential security exploits!

Marcel said...

I have followed your tut and liked it a lot.

At my appspot I get:

[RPC Fault faultString="[MessagingError message='Destination 'Data' either does not exist or the destination has no channels defined (and the application does not define any default channels.)']" faultCode="InvokeFailed" faultDetail="Couldn't establish a connection to 'Data'"]

Also, locally I can not run the server. I get:

9-okt-2011 13:06:56 com.google.apphosting.utils.jetty.JettyLogger info
INFO: Logging to JettyLogger(null) via com.google.apphosting.utils.jetty.JettyLogger
9-okt-2011 13:06:56 com.google.apphosting.utils.config.AppEngineWebXmlReader readAppEngineWebXml
INFO: Successfully processed C:\Users\Rossie\EclipsWorkspace\Flex\GraniteDS\war\WEB-INF/appengine-web.xml
9-okt-2011 13:06:56 com.google.apphosting.utils.config.AbstractConfigXmlReader readConfigXml
INFO: Successfully processed C:\Users\Rossie\EclipsWorkspace\Flex\GraniteDS\war\WEB-INF/web.xml
[Fatal Error] :2:1: Content is not allowed in prolog.
9-okt-2011 13:06:58 com.google.apphosting.utils.jetty.JettyLogger warn
WARNING: Failed startup of context com.google.apphosting.utils.jetty.DevAppEngineWebAppContext@178460d{/,C:\Users\Rossie\EclipsWorkspace\Flex\GraniteDS\war}
java.lang.RuntimeException: Could not initialize Granite environment
at org.granite.config.GraniteConfigListener.contextInitialized(GraniteConfigListener.java:93)
at org.mortbay.jetty.handler.ContextHandler.startContext(ContextHandler.java:548)
at org.mortbay.jetty.servlet.Context.startContext(Context.java:136)
at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1250)
at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:517)

etc.

granite.jar is in de war/WEB-INF/lib folder

Any clues?

Marcel said...

I fixed the missing Data problem by altering the mxml file:



Adding the endpoint fixed it.

Thanks. You're star for making this tutorial

NotengoID said...

Hi I just do all in orden, and get this error:



[RPC Fault faultString="Could not initialize class com.acme.data.EMF" faultCode="DefaultServiceExceptionHandler.Call.Failed" faultDetail="
- destination: Data
- method: public void com.acme.data.Data.createData(java.lang.String,java.lang.String)
- exception: java.lang.NoClassDefFoundError: Could not initialize class com.acme.data.EMF
org.granite.messaging.service.ServiceException: Could not initialize class com.acme.data.EMF

Anonymous said...

I just put the persisntence.xml in
war/WEB-INF/classesMETA-INF/
and work