Home >
Getting Free with GraniteDS
A few weeks ago, Justin Shacklette published a very interesting two-part article describing the new features of Adobe LCDS 3 (part 1 and part 2).
The first article described the model-driven development features, and the second article focused on data management and ease of development.
As one of the developers of the GraniteDS free and open-source alternative to LCDS, I found these articles very instructive and in the end I was struck at how the Flex 4 example application that Justin built was similar to what it would have been if using our upcoming GraniteDS 2.1 release.
This example application makes a perfect basis to write a comparison between LCDS 3 and GraniteDS, and is a good way of showing how GraniteDS can be a valid alternative to the full LCDS stack and not only to BlazeDS.
Model-driven development
As Justin noted, LCDS is Flex-centric and can generate a whole application from a Flex data model and UI. As he said in the article :
" In LCDS 3, you write the UI and the model, and LCDS does everything else. "
This has the big advantage of using the same language and tools for all layers, and definitely brings very high productivity. However this comes at the expense of being completely locked to LCDS as a server-side technology and of a high coupling between the UI and server layers.
A different path
GraniteDS takes the completely opposite path and in fact does not even try to compete on this. You write your data model in Java using any supported persistence technology (Hibernate, JPA, JDO) and your services using any supported Java EE technology (EJB3, JEE6/CDI, Spring, JBoss Seam, Grails) and the GraniteDS runtime and tools will handle all the plumbing between the Flex application and the JEE server application.
Supporting all these technologies means that you can easily choose between a whole host of model-driven frameworks and tools (Grails, seam-gen, AppFuse, Spring Roo...) to build the server-side application if you want to. It is also important to note that your backend will have a very minimal dependency on the UI technology.
For the purpose of the comparison, I will just use the very common Spring and JPA/Hibernate combination of Java technologies for the server-side part and build the application manually.
Getting Started
Here are the steps to initiate the project :
- Install the latest versions of Flex Builder 4, Eclipse WTP (or much better an Eclipse for JavaEE with the FB4 plugin), the GraniteDS libraries, the GraniteDS Eclipse plugin and MySQL.
- Install a recent nightly build of the Flex 4 SDK and set it as default in FB4 (for example build 13472). This is needed because there is a bug in Flex SDK 4 beta 2 that breaks GraniteDS.
- Create an empty database :
- Create a new Dynamic Web Project in Eclipse and add Spring, Hibernate and MySQL connector libraries in WebContent/WEB-INF/lib.
CREATE DATABASE blog;
GRANT ALL PRIVILEGES ON blog.* TO blogger@localhost
IDENTIFIED BY 'password';
Now we have to enable GraniteDS for the server application :
- Add granite.jar, granite-spring.jar and granite-hibernate.jar in WEB-INF/lib.
- Add a Spring MVC dispatcher servlet for GraniteDS remoting and the GraniteDS asynchronous server push servlet in web.xml :
- Define an empty dispatcher-servlet.xml in WEB-INF for Spring :
- Define the Spring configuration and enable GraniteDS :
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>GraniteDS Blog</display-name>
<description>GraniteDS Blog Example Application</description>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/conf/applicationContext.xml</param-value>
</context-param>
<!-- Spring listeners -->
<listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener>
<listener><listener-class>org.springframework.web.context.request.RequestContextListener</listener-class></listener>
<!-- Spring MVC dispatcher servlet for GraniteDS remoting -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/graniteamf/*</url-pattern>
</servlet-mapping>
<!-- GraniteDS asynchronous servlet for server push -->
<servlet>
<servlet-name>GravityServlet</servlet-name>
<servlet-class>org.granite.gravity.tomcat.GravityTomcatServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>GravityServlet</servlet-name>
<url-pattern>/gravityamf/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> </beans>
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:graniteds="http://www.graniteds.org/config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.graniteds.org/config http://www.graniteds.org/config/granite-config-2.1.xsd">
<!-- Annotation scan -->
<context:component-scan base-package="services" />
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>com.mysql.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost:3306/blog?autoReconnect=true</value>
</property>
<property name="username">
<value>blogger</value>
</property>
<property name="password">
<value>password</value>
</property>
</bean>
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="persistenceUnitName" value="jpa" />
<property name="jpaVendorAdapter">
<bean
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="false" />
<property name="generateDdl" value="true" />
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
</bean>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
<property name="dataSource" ref="dataSource" />
</bean>
<graniteds:flex-filter url-pattern="/*" tide="true">
<graniteds:tide-annotations>
<graniteds:value>org.granite.messaging.service.annotations.RemoteDestination</graniteds:value>
</graniteds:tide-annotations>
</graniteds:flex-filter>
<graniteds:messaging-destination id="blogTopic" session-selector="true"/>
</beans>
This is a relatively complex configuration but the specific configuration for GraniteDS itself (marked in blue) is very simple.
Data modeling
We're ready to build the data model with JPA. The GraniteDS data framework imposes a few simple requirements on the JPA entities, so we are first going to define a mapped superclass AbstractEntity to centralize all this stuff :
- First it is highly recommended to define a persistent uid field. It will be the common identifier of an entity instance throughout the Flex UI, the Java layer and the database. The GraniteDS data manager can work without it and derive the uid from the @Id field but this is not completely reliable when creating new instances from Flex as they don't have an assigned id yet.
- It is also highly recommended to use JPA optimistic locking (the @Version field). This will help the JPA engine detect concurrent updates on a particular entity instance and is also used by GraniteDS to accept or ignore remote updates.
- Finally, we add the DataPublishListener JPA listener. This is how GraniteDS integrates with the JPA engine and can be informed that entity instances have been added or updated in the database.
package entities;
import java.io.Serializable;
import java.util.UUID;
import javax.persistence.*;
import org.granite.tide.data.DataPublishListener;
@MappedSuperclass
@EntityListeners({AbstractEntity.AbstractEntityListener.class, DataPublishListener.class})
public abstract class AbstractEntity {
@Id @GeneratedValue
private Integer id;
@Column(unique=true, nullable=false, updatable=false, length=36)
private String uid;
@Version
private Integer version;
public Integer getId() {
return id;
}
public Integer getVersion() {
return version;
}
@Override
public boolean equals(Object o) {
return (o == this || (o instanceof AbstractEntity && uid().equals(((AbstractEntity)o).uid())));
}
@Override
public int hashCode() {
return uid().hashCode();
}
public static class AbstractEntityListener {
@PrePersist
public void onPrePersist(AbstractEntity abstractEntity) {
abstractEntity.uid();
}
}
private String uid() {
if (uid == null)
uid = UUID.randomUUID().toString();
return uid;
}
}
Finally we can get to the real thing :
@Entity
public class Author extends AbstractEntity {
@Basic
private String name;
@OneToMany(cascade=CascadeType.ALL, mappedBy="author")
@Cascade({org.hibernate.annotations.CascadeType.ALL, org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
private Set<Post> posts = new HashSet<Post>();
@Formula("(select count(*) from post p where p.author_id = id)")
private Integer numPosts = 0;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Post> getPosts() {
return posts;
}
public void setPosts(Set<Post> posts) {
this.posts = posts;
}
@ExternalizedProperty
public Integer getNumPosts() {
return numPosts;
}
}
@Entity
public class Post extends AbstractEntity {
@ManyToOne(optional=false)
private Author author;
@Basic
private String title;
@Basic
@Length(max=5000)
private String content;
@Temporal(TemporalType.DATE)
private Date pubDate;
@Formula("datediff(now(), pubDate)")
private int daysOld;
@ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
private Set<Tag> tags = new HashSet<Tag>();
public Author getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getPubDate() {
return pubDate;
}
public void setPubDate(Date pubDate) {
this.pubDate = pubDate;
}
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
@ExternalizedProperty
public int getDaysOld() {
return daysOld;
}
}
@Entity
public class Tag extends AbstractEntity {
@Basic
private String name;
@ManyToMany(fetch=FetchType.LAZY, mappedBy="tags")
private Set<Post> posts;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Post> getPosts() {
return posts;
}
public void setPosts(Set<Post> posts) {
this.posts = posts;
}
}
As you can see, there is no drag-and-drop or fancy expression builder, just plain JPA/Hibernate annotations and Java code, but we're already done with the data model.
Besides the always annoying getters/setters, we could easily define all properties and associations through simple annotations. We could even make a small improvement because Hibernate supports orphan delete cascading so we won't have to handle it manually as with LCDS.
Note the use of @Formula and @ExternalizedProperty as an equivalent of LCDS derived properties for Post.daysOld and Author.numPosts. We just ignore Post.wordCount for now, we will implement it later in the ActionScript class. That makes three possible places to define derived properties : the database with @Formula, the Java class and the ActionScript class, just choose depending on the use case.
Building the services
Now that we have a nice data model, we need a service layer to play with it. In the case of this simple application, this is definitely mostly uninteresting boilerplate JPA code, but let's just have a look to one service implementation, the others will be very similar :
@Service("postService")
@RemoteDestination
@DataEnabled(topic="blogTopic", params=ObserveAllPublishAll.class, publish=PublishMode.ON_SUCCESS)
public class PostService {
@PersistenceContext
@IgnoredProperty
private EntityManager entityManager;
@Transactional
public Post createPost(Post post) {
Set<Tag> tags = new HashSet<Tag>();
for (Tag tag : post.getTags())
tags.add((Tag)entityManager.merge(tag));
post.setTags(tags);
entityManager.persist(post);
entityManager.flush();
return post;
}
@Transactional
public Post updatePost(Post post) {
post = (Post)entityManager.merge(post);
entityManager.flush();
return post;
}
@Transactional
public void deletePost(Post post) {
post = (Post)entityManager.find(Post.class, post.getId());
post.getTags().clear();
entityManager.remove(post);
entityManager.flush();
}
@Transactional(readOnly=true)
public List<Post> findAllPosts() {
Query qp = entityManager.createQuery("select p from Post p order by p.pubDate desc");
return (List<Post>)qp.getResultList();
}
@Transactional(readOnly=true)
public List<Post> findPostsByAuthorName(String authorName) {
Query qp = entityManager.createQuery("select p from Post p "
+ "where p.author.name like ('%' || :authorName || '%') order by p.pubDate desc");
qp.setParameter("authorName", authorName);
return (List<Post>)qp.getResultList();
}
@Transactional(readOnly=true)
public List<Post> findPostsByTag(String tagName) {
Query qp = entityManager.createQuery("select p from Post p "
+ "where exists (select t from p.tags t where t.name = :tagName) order by p.pubDate desc");
qp.setParameter("tagName", tagName);
return (List<Post>)qp.getResultList();
}
}
The important things are the GraniteDS annotations @RemoteDestination and @DataEnabled. The former indicates that the Spring service can be accessed remotely from Flex, the latter indicates that all methods of the service will be intercepted by GraniteDS and may dispatch data updates on a particular messaging topic.
DataEnabled has two arguments : the messaging topic that will be used to dispatch the updates that should match the one we set up in the Spring configuration, and a publishing filter class that determine who will receive which updates. In this example we simply use the following ObserveAllPublishAll class that dispatch everything to everyone. It's possible to define more complex rules based on user identity, access rights or anything else by implementing observes and publishes.
public class ObserveAllPublishAll implements DataTopicParams {
public void observes(DataObserveParams params) {
}
public void publishes(DataPublishParams params, Object entity) {
}
}
The goal of this article is not to explain how to build a service layer with Spring, just to show that it's not necessarily more complex than to build it in Flex with LCDS, especially for companies with Java developers. Arguably there is much more configuration than with all Flex Builder 4 wizards and tools, but I voluntarily did not use any of the existing Eclipse or Spring tools that make a lot easier to get started with a new project.
Where is Flex ?
All is good now for the server application but we still don't have any single line of Flex code. At least this proves that our backend does not have much dependency on Flex besides a few GraniteDS annotations, but what we want is a Flex application.
We go back to our Eclipse / Flex Builder 4 project. We now have to add Flex support by right-clicking on the project in the Project Explorer view and choosing 'Add Flex Project type...'. Then put granite.swc and granite-essentials.swc in the libs folder and add the Flex compiler option -include-libraries libs/granite-essentials.swc.
We are now able to compile Flex classes and build a swf file, so let's configure the GraniteDS Gas3 generator to generate ActionScript classes for our project :
And setup generation for the project entities and services.
Once everything is correctly set up, Gas3 will generate a set of ActionScript classes in flex/entities and flex/services. Just like LCDS modeling tools generate a _Super_Post and a Post class, Gas3 generates PostBase and Post. The Base class will be overwritten on each generation, the derived one can be modified as needed. Note that the ActionScript model is automatically kept in sync when a Java class is modified.
The Flex model is almost done. To finish it, just add a computed wordCount property to our Post class :
package entities {
[Bindable]
[RemoteClass(alias="entities.Post")]
public class Post extends PostBase {
public function get wordCount():int {
return content.split(/\s+/).length;
}
}
}
The interesting thing is that for now we have written manually almost no Flex code and still have a complete Flex data model. This is exactly the inverse situation as what happens when using LCDS !
Testing all this
Before deploying the application to a Tomcat server, let's just build a test class :
package tests
{
import entities.Author;
import org.flexunit.Assert;
import org.flexunit.async.Async;
import org.granite.tide.events.TideResultEvent;
import org.granite.tide.service.DefaultServiceInitializer;
import org.granite.tide.spring.Context;
import org.granite.tide.spring.Spring;
import services.AuthorService;
Spring.getInstance().getSpringContext().serviceInitializer = new DefaultServiceInitializer("/blog");
public class TestAuthorService
{
public function TestAuthorService():void {
Spring.getInstance().getSpringContext().testAuthorService = this;
}
[In]
public var authorService:AuthorService;
private var _author:Author;
[Test(async)]
public function testCreateAuthor():void {
_author = new Author();
_author.name = "Victor Hugo";
authorService.createAuthor(_author, Async.asyncHandler(this, createAuthorResult, 5000));
}
private function createAuthorResult(event:TideResultEvent, passThroughData:Object = null):void {
Assert.assertFalse("Author saved", isNaN(_author.id));
authorService.findAllAuthors(Async.asyncHandler(this, findAllAuthorsResult, 5000));
}
private function findAllAuthorsResult(event:TideResultEvent, passThroughData:Object = null):void {
var found:Boolean = false;
for each (var obj:Object in event.result) {
if (obj.uid === _author.uid) {
found = true;
break;
}
}
Assert.assertTrue("Author found", found);
}
}
}
Like in the original article, we just use Flex Builder 4 support for FlexUnit4 to build and run this test. An important thing is the use of the GraniteDS/Tide API by the the generated service proxy :
- It makes use of handler functions instead of responders. It's a bit more readable and saves a few typing when writing asynchronous code.
- We get the typesafe client stub by injection : we first put the test class in the Tide context to make it injectable and then Tide injects the property annotated with [In] with an instance of the service proxy AuthorService.
- We don't have a services-config.xml file. The setup of the endpoint is done by the DefaultServiceInitializer class. We could also implement another service initializer for example to get the endpoint uri from a remote location in the case of an AIR application.
End of first step
We just arrived at the end of the first step, we now have a functional back-end and a set of Flex classes to use it. The result it not that different from what was obtained with LCDS 3, we just got it differently. In particular the source of metadata for the project is the JPA model instead of the LCDS model.
Now go on some more interesting work on the client.
Avoid Async Hell
Justin rightly noted in his article how LCDS helps in handling with the asynchronous nature of remote calls in Flex. Again, GraniteDS brings similar functionality but with a different implementation and usage. As we have seen just before, GraniteDS data management uses the Tide API instead of the standard Flex CallResponder/AsyncToken API. Let's see what this changes :
LCDS version
<s:Group ...
xmlns:dms="dms.*"
creationComplete="complete()">
<fx:Script>
<![CDATA[
public function complete():void {
getPosts.token = postService.getAllSorted();
}
]]>
</fx:Script>
<fx:Declarations>
<s:CallResponder id="getPosts"/>
<dms:PostService id="postService" />
</fx:Declarations>
<s:List
dataProvider="{getPosts.lastResult}"
labelField="title"
itemRenderer="components.PostRenderer"/>
</s:Group>
GraniteDS/Tide version
<s:Group ...
creationComplete="complete()">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import org.granite.tide.TideResponder;
import org.granite.tide.spring.Context;
import services.PostService;
[Bindable]
private var _posts:ArrayCollection = new ArrayCollection();
[In]
public var postService:PostService;
[In]
public var tideContext:Context;
private function complete():void {
tideContext.addContextEventListener("org.granite.tide.data.persist.Post", list);
tideContext.addContextEventListener("org.granite.tide.data.remove.Post", list);
list();
}
private function list(event:Event = null):void {
postService.findAllPosts(new TideResponder(null, null, null, _posts));
}
]]>
</fx:Script>
<s:List
dataProvider="{_posts}"
labelField="title"
itemRenderer="components.PostRenderer"/>
</s:Group>
Now let's have a look at the API differences :
- LCDS style is more declarative : CallResponder and PostService are defined in MXML. The use is a bit less intuitive and requires to wire manually the internal token object.
- GraniteDS style is more programmatic : the remote call is more similar to a 'normal' method call. The result object specified in the responder will serve as a placeholder for the remote data and its content will be replaced by it, that's why it's important to initialize the collections and not let it null.
- GraniteDS style is more typesafe : the result object is a typed ArrayCollection and is not hidden by responder.lastResult.
In fact the main difference is the persist/remove listeners. LCDS will automatically manage updated, new or removed elements and refresh the collection accordingly. On the other hand, GraniteDS will only handle automatically updates on existing elements, but will not refresh the list when elements are added or removed. It dispatches corresponding events though, so it is only required to handle these events manually, here for example we just trigger a refresh of the collection.
Displaying a Post
The code for displaying a post is almost identical between LCDS and GraniteDS. The main difference is that GraniteDS does not throw ItemPendingError to handle lazy loading of collections, it simply dispatches appropriate CollectionEvent as needed. Using lazily loaded collections does not change anything on the client code.
<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/halo">
<fx:Declarations>
<mx:DateFormatter id="df" formatString="MMMM D, YYYY" />
</fx:Declarations>
<s:states>
<s:State name="normal"/>
<s:State name="hovered"/>
<s:State name="selected"/>
</s:states>
<s:Rect left="0" right="0" top="0" bottom="0">
<!--s:fill>
<s:SolidColor color="{contentBackgroundColor}" color.hovered="{rollOverColor}" color.selected="{selectionColor}" />
</s:fill-->
</s:Rect>
<s:VGroup left="8" right="8" top="8" bottom="8" gap="4">
<s:Label id="labelDisplay" width="470" maxDisplayedLines="1" maxDisplayedLines.selected="3" fontSize="18" fontWeight="bold"/>
<s:Label width="470" maxDisplayedLines="1" fontStyle="italic"
text="{[
'by ' + data.author.name,
df.format(data.pubDate),
data.daysOld + (data.daysOld == 1 ? ' day' : ' days') + ' ago',
data.wordCount + ' words'
].join(' | ')}" />
<s:Label width="470" text="{data.content}" maxDisplayedLines="3" maxDisplayedLines.selected="40" />
<s:HGroup>
<s:Label text="Tags:" fontWeight="bold" />
<s:DataGroup id="tagList" dataProvider="{data.tags}">
<s:layout>
<s:HorizontalLayout gap="5"/>
</s:layout>
<s:itemRenderer>
<fx:Component>
<s:DataRenderer>
<s:Label id="tagName" text="{data.name}" fontWeight="bold" />
</s:DataRenderer>
</fx:Component>
</s:itemRenderer>
</s:DataGroup>
</s:HGroup>
</s:VGroup>
</s:ItemRenderer>
Sorting locally
The original article presented a way to sort data locally. We are just going to do the same by defining a result handler in the TideResponder for the remote call :
[Bindable]
private var _authors:ArrayCollection = new ArrayCollection();
[In]
public var authorService:AuthorService;
private function list(event:Event = null):void {
authorService.findAllAuthors(new TideResponder(authorSort, null, null, _authors));
}
private function authorSort(event:TideResultEvent):void {
// sort collection by author name
var sortByName:Sort = new Sort();
sortByName.fields = [new SortField("name", true)];
_authors.sort = sortByName;
_authors.refresh();
}
Forms
I won't follow into details the complete build of the application because really most of it is identical with GraniteDS or LCDS, but the data update part is important and needs more explanation :
private function editHandler(e:AdminEvent):void {
if (currentState == 'author') {
authorForm.author = e.entity as Author;
authorForm.editing = true;
}
...
editBox.visible = true;
}
private function saveHandler():void {
author.name = nameTextInput.text;
dispatchEvent(new AdminEvent(AdminEvent.SAVE, author));
}
private function saveHandler(e:AdminEvent):void {
if (currentState == 'author') {
var author:Author = e.entity as Author;
if (isNaN(author.id)) {
authorService.createAuthor(author);
}
else {
authorService.updateAuthor(author);
}
}
}
As you can see if you compare with the code for LCDS, the only visible difference is the use of NaN instead of 0 to determine an unsaved entity instance. The real difference is very important and is not in the code but in the internal working : LCDS immediately triggers a server update when author.name is set and just commits the result when authorService.updateAuthor is called. GraniteDS works exactly the opposite and never does any data operation by itself, but strictly relies on the application remote services to do something in the database and handle transactions. So the real update happens only when authorService.updateAuthor is called, and GraniteDS listens to JPA events and dispatches the update to all subscribed clients.
Updating the many-to-many associations works just as differently. LCDS sends updates on-the-fly whereas GraniteDS relies on JPA cascading.
In fact, it is almost incredible that the exact same client code gives the same results with such different internal implementations.
Summary
The application is now finished, what did we do :
- Build a JPA data model with complex relationships and derived properties
- Build Spring services to get and update data
- Deploy the backend to a Tomcat server
- Write a FlexUnit4 test and check the backend
- Display data asynchronously from the backend and sort it on the server or client
- Handle lazy loading (by the way we didn't do anything to handle it..)
- Perform CRUD operations
- Build the complete multiuser blog application with realtime server push
Conclusion
This is no question that Adobe LCDS 3 is an extremely powerful and integrated solution for building Flex RIAs, but it implies that the whole application is built with it and ties the UI with the backend.
On the contrary, GraniteDS does not provide much services by itself but greatly helps in building your RIA by gluing together existing Java technologies and letting you choose the frameworks that better suit your needs and habits. It is completely open-source under the LGPL3 license, is very extensible and supports a wide choice of platforms. It is for example possible to deploy a GraniteDS application on a simple Tomcat 6 with Spring and Hibernate or on WebLogic with EJB3 and EclipseLink, and in most cases GraniteDS will be able to take full advantage of the platform to get better scalability or more features.
References
- GraniteDS Web site
- Justin's article part 1 and part 2
Code and Demo
The code is available as a working Eclipse project for download here. A war file that can be deployed in Tomcat 6.0.18+ is also available here. Once deployed, browse http://localhost:8080/blog/Blog.html.




Facebook Application Development
Interesting article! thx
Never played with GDS because I thought I had to use Seam for it and I don't have any knowledge about it...but as it seams...euhm seems, I can just use me standard Spring based server...nice!
Looking forward checking our the lazy loading implementation, because I still do eager fetching on the application I'm working on.
Kind regards,
Jochen
I have been using BlazeDS till now for all my projects. Will start using GraniteDS from now and make use all of its features. Thanks for the framework.
thx for the GDS framework
i love it
Great article. I tried to implement the code using Google App Engine, but I am having problems using @ExternalizedProperty. DataNucleus is giving the following error.
SEVERE: DataNucleus Enhancer completed with an error. Please review the enhancer log for full details. Some classes may have been enhanced but some caused errors
Class entities.Post has property daysOld declared in MetaData, but its setter method doesnt exist in the class!
org.datanucleus.metadata.InvalidMetaDataException: Class com.example.granite.blog.entities.Post has property daysOld declared in MetaData, but its setter method doesnt exist in the class!
I can provide the full stack trace if you want.
I am using IntelliJ 9 to develop Flex-GraniteDS-Spring-Hibernate-Maven based application. While following your sample application and the ones given on http://graniteds.blogspot.com, I am getting unresolved function or method on Spring.getInstance().initApplication()
org.granite.tide.service.DefaultServiceInitializer and org.granite.tide.data.DataObserver are also unrecognizable.
I have tried adding maven dependency of every single swc library from org.graniteds, have also updated the version of all dependencies to the latest available. But all in vain, the issue is still there.
Any help in this regard will be highly appreciated.
A great article - GDS is excellent - Thanks William.
@Vega - I think you will need to make the daysOld property a type Integer and not type int (as described in the article) as int is a primative and therefore not Serializable.
Thanks a lot for this tutorial. GDS is realy a nice way to access the data.
The code doesnot have a build file associated..can anyone let me know where to find the pom.xml/ant.xml?
The code doesnot have a build file associated..can anyone let me know where to find the pom.xml/ant.xml?
Hi, Its a good article and helps the beginners.
For the sake of completeness, I am developing this example using Hibernate, instead of JPA. I am facing a problem, for lazyfetching.
Like in this example (in the author tab), when I click on one entry of author, all his posts are fetched from db at that time and transfered to flex side to be displayed. But in my example, posts are fetched from DB but it is not brought to Flex side. i.e. data.posts does cause the request to fetch the data from db but doesn't come flex side.
I have compare the code alot of time, but i am unable to find any difference and expected results. Please, can some one help me out.