GraniteDS Data Management Tutorial

We have setup a simple example project that demonstrates most features of GraniteDS when used in a Spring/Hibernate technology stack.

The project is available on GitHub at http://github.com/wdrai/wineshop-admin and requires Maven 3.x for building.

Just issue the following commands to try it :

git clone git://github.com/wdrai/wineshop-admin.git

cd wineshop-admin

mvn clean install

cd webapp

mvn jetty:run-war

Now you can browse http://localhost:8080/wineshop-admin/wineshop-admin.swf. You can log in with admin/admin or user/user.

It’s a simple CRUD example which allows searching, creating and modifying vineyards and the vines they produce. The application is definitely ugly but its goal is simply to demonstrate the following features :

  • Basic CRUD with a Spring service
  • Support for lazy-loading of JPA x-to-many associations
  • Dirty-checking
  • Client validation
  • Real-time push
  • Reverse lazy-loading

Each step corresponds to a tag on the GitHub project so you can see what has been changed at each step.

Let’s rebuild this project from scratch.

Step 1 : Create the project with the Maven archetype

This is the easiest one :

mvn archetype:generate<br />

    -DarchetypeGroupId=org.graniteds.archetypes<br />

    -DarchetypeArtifactId=org.graniteds-tide-spring-jpa<br />

    -DarchetypeVersion=1.1.0.GA<br />

    -DgroupId=com.wineshop<br />

    -DartifactId=wineshop-admin<br />

    -Dversion=1.0-SNAPSHOT<br />

Then check that the initial project is working :

cd wineshop-admin<br />

mvn clean install<br />

cd webapp<br />

mvn jetty:run-war<br />

And browse http://localhost:8080/wineshop-admin/wineshop-admin.swf. You will get the default hello world application.

Step 2 : Implement basic CRUD functionality

This is the longer step as we are going to build most of the application : the JPA entity model, the Spring service and a basic Flex client.

Here is the entity model, there is nothing special here.


@Entity

public class Vineyard extends AbstractEntity {

    private static final long serialVersionUID = 1L;

    @Basic

    private String name;

    @OneToMany(cascade=CascadeType.ALL, mappedBy="vineyard",

        orphanRemoval=true)

    private Set<Wine> wines;

    public String getName() {

        return name;

    }

    public void setName(String name) {

        this.name = name;<br />

    }

    public Set<Wine> getWines() {

        return wines;<br />

    }

    public void setWines(Set<Wine> wines) {

        this.wines = wines;

    }

}


@Entity

public class Wine extends AbstractEntity {



    private static final long serialVersionUID = 1L;



    public static enum Type {

        RED,

        WHITE,

        ROSE

    }



    @ManyToOne

    private Vineyard vineyard;



    @Basic

    private String name;



    @Basic

    private Integer year;



    @Enumerated(EnumType.STRING)

    private Type type;



    public Vineyard getVineyard() {

        return vineyard;

    }

    public void setVineyard(Vineyard vineyard) {

        this.vineyard = vineyard;<br />

    }

    public Integer getYear() {

        return year;<br />

    }

    public void setYear(Integer annee) {

        this.year = annee;<br />

    }

    public String getName() {

        return name;<br />

    }

    public void setName(String nom) {

        this.name = nom;

    }

    public Type getType() {

        return type;

    }

    public void setType(Type type) {

        this.type = type;

    }

}

The Spring service interface to handle this model :


@RemoteDestination

@DataEnabled(topic="")

public interface WineshopService {

    public void save(Vineyard vineyard);

    public void remove(Long vineyardId);

    public Map<String, Object> list(Vineyard filter,

        int first, int max, String[] sort, boolean[] asc);

}

As you can see, there are two specific annotations on the service. @RemoteDestination indicates that the service is exposed to the Flex application, and that an ActionScript3 proxy will be generated for the service. @DataEnabled indicates that GraniteDS will track the JPA updates on the entities and report them automatically to the relevant clients.

Then the implementation :


@Service

public class WineshopServiceImpl implements WineshopService {



    @PersistenceContext

    private EntityManager entityManager;



    @Transactional

    public void save(Vineyard vineyard) {

        entityManager.merge(vineyard);

        entityManager.flush();

    }



    @Transactional

    public void remove(Long vineyardId) {

    	Vineyard vineyard = entityManager.find(Vineyard.class, vineyardId);

    	entityManager.remove(vineyard);

        entityManager.flush();

    }



    @Transactional(readOnly=true)

    public Map<String, Object> list(Vineyard filter,

            int first, int max, String[] sort, boolean[] asc) {



    	StringBuilder sb = new StringBuilder("from Vineyard vy ");

    	if (filter.getName() != null)

    		sb.append("where vy.name like '%' || :name || '%'");

    	if (sort != null && sort.length > 0) {

    		sb.append("order by ");

    	        for (int i = 0; i < sort.length; i++)

    		      sb.append(sort[i]).append(" ").append(asc[i] ? " asc" : " desc");

        }

    	Query qcount = entityManager.createQuery("select count(vy) "

             + sb.toString());

    	Query qlist = entityManager.createQuery("select vy "

             + sb.toString()).setFirstResult(first).setMaxResults(max);

    	if (filter.getName() != null) {

    		qcount.setParameter("name", filter.getName());

    		qlist.setParameter("name", filter.getName());

    	}

    	Map<String, Object> result = new HashMap<String, Object>(4);

    	result.put("resultCount", (Long)qcount.getSingleResult());

    	result.put("resultList", qlist.getResultList());

    	result.put("firstResult", first);

    	result.put("maxResults", max);

    	return result;

    }

}

This is a classic Spring JPA service. There are two particularities however :

  • It uses merge to update the entities. This is important as objects that are transferred between Flex and Java are considered as detached objects.
  • The method list has the specific signature (filter, first, max, sort[], asc[]) so it can be easily used from the GraniteDS paged collection implementation.

Note however that there is no particular dependency on GraniteDS, this service can be used by any other client.

Finally the Flex client application in Home.mxml :


<?xml version="1.0" encoding="utf-8"?>



<s:VGroup

    xmlns:fx="http://ns.adobe.com/mxml/2009"

    xmlns:s="library://ns.adobe.com/flex/spark"

    xmlns:mx="library://ns.adobe.com/flex/mx"

    xmlns:e="com.wineshop.entities.*"

    xmlns="*"

    width="100%" height="100%">



    <fx:Metadata>[Name]</fx:Metadata>

   	

    <fx:Script>

        <![CDATA[

            import mx.collections.ArrayCollection;

            

            import org.granite.tide.spring.Spring;

            import org.granite.tide.collections.PagedQuery;

            import org.granite.tide.events.TideResultEvent;

            import org.granite.tide.events.TideFaultEvent;

            

            import com.wineshop.entities.Vineyard;

            import com.wineshop.entities.Wine;

            import com.wineshop.entities.Wine$Type;

            import com.wineshop.services.WineshopService;



            Spring.getInstance().addComponentWithFactory("vineyards", PagedQuery, 

                { filterClass: Vineyard, elementClass: Vineyard, remoteComponentClass: WineshopService, methodName: "list", maxResults: 12 }

            );

			

            [In] [Bindable]

            public var vineyards:PagedQuery;

			

            [Inject]

            public var wineshopService:WineshopService;

			

            private function save():void {

                 wineshopService.save(vineyard);

            }

			

            private function remove():void {

                wineshopService.remove(vineyard.id, function(event:TideResultEvent):void {

                    selectVineyard(null);

                });

            }

			

            private function selectVineyard(vineyard:Vineyard):void {

                this.vineyard = vineyard;

                vineyardsList.selectedItem = vineyard;

            }

        ]]>

    </fx:Script>

	

	<fx:Declarations>		

		<e:Vineyard id="vineyard"/>

	</fx:Declarations>

    

	<s:VGroup paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10" width="800">

		<s:HGroup id="filter">

			<s:TextInput id="filterName" text="@{vineyards.filter.name}"/>

			<s:Button id="search" label="Search" click="vineyards.refresh()"/>

		</s:HGroup>

		

		<s:List id="vineyardsList" labelField="name" width="100%" height="200"

				change="selectVineyard(vineyardsList.selectedItem)">

			<s:dataProvider><s:AsyncListView list="{vineyards}"/></s:dataProvider>

		</s:List>

		

		<s:Button id="newVineyard" label="New" click="selectVineyard(new Vineyard())"/>

	</s:VGroup>

	

	<s:VGroup paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10" width="800">

		<mx:Form id="formVineyard">

			<mx:FormHeading label="{isNaN(vineyard.id) ? 'Create vineyard' : 'Edit vineyard'}"/>			

			<mx:FormItem label="Name">

				<s:Label text="{vineyard.id}"/>

				<s:TextInput id="formName" text="@{vineyard.name}"/>

			</mx:FormItem>

			<mx:FormItem>

				<s:HGroup>

					<s:Button id="saveVineyard" label="Save"

							  click="save()"/>					

					<s:Button id="removeVineyard" label="Remove"

							  enabled="{!isNaN(vineyard.id)}" click="remove()"/>

				</s:HGroup>

			</mx:FormItem>

		</mx:Form>

	</s:VGroup>



</s:VGroup>

This is no major complexity here, but there are some things that can be noted :

  • It uses the PagedQuery component to display the list of existing vineyards. The configuration section at the beginning (addComponentWithFactory…) links the client component to the service method we have define in our Spring service, so each time the component needs to fetch data, it will call the remote service transparently. This component also handles paging, so it will fetch next elements on demand when there are lots of data on the server and the user scrolls the list. This is also why it is wrapped in an AsyncListView collection view.
  • The PagedQuery also handles transparently filtering and sorting. Here we simply bind a TextInput to the filter object that is passed to the server method. When the user clicks on ‘Search’, we simply have to refresh the collection, as we would have done for a client filter. Sorting is even easier and is handled completely transparently when the collection is bound to a UI component that allows sorting, for example DataGrid.
  • The remote service invocation is completely typesafe thanks to the ActionScript 3 generator and the [Inject] annotation. If you refactor the service, you will instantly detect the inconsistency between the client and the server at build time. You can also benefit from code completion in the IDE.
  • The CRUD operations are completely ‘fire and forget’. You just call the server method, and GraniteDS will automatically handle the updates, you don’t have to do anything yourself. There is not even a result handler and the server method do not return anything. In fact GraniteDS listens to all JPA events though the DataPublishListener in AbstractEntity and dispatches them transparently to the Flex clients, including the client from which the call originates. The local caching does everything else and GraniteDS is able to match and merge the result with the initial object on the client. The remove operation also works transparently because the PagedQuery is able to handle the remove events and update itself. It we were using simple collections, we would have to handle manually the persist and remove events.

Now you can build the application with mvn clean install, restart jetty and check your changes.

Step 3 : Support for JPA lazy associations

There is nothing much to do, simply add a form item allowing to edit the list of wines for the selected vineyard. We can for example use a list with an item renderer containing editors for the properties of the Wine entity :


<s:FormItem label="Wines">

    <s:HGroup gap="10">

        <s:List id="formWines" dataProvider="{vineyard.wines}">

            <s:itemRenderer>

                <fx:Component>

                    <s:ItemRenderer>

                        <s:states><s:State name="normal"/></s:states>                   

                        <s:HGroup id="wineEdit">

                            <s:TextInput text="@{data.name}"/>

                            <s:TextInput text="@{data.year}"/>

                            <s:DropDownList 

                                selectedItem="@{data.type}" 

                                requireSelection="true" 

                                dataProvider="{outerDocument.wineTypes}" 

                                labelField="name"/>

                        </s:HGroup>

                    </s:ItemRenderer>							

                </fx:Component>

            </s:itemRenderer>					

        </s:List>				

		

        <s:VGroup gap="10">

            <s:Button label="+" 

                click="vineyard.wines.addItem(new Wine(vineyard))"/>

            <s:Button label="-" 

                 enabled="{Boolean(formWines.selectedItem)}" 

                 click="vineyard.wines.removeItemAt(formWines.selectedIndex)"/>

        </s:VGroup>

    </s:HGroup>

</s:FormItem>

We just miss two minor things : add an argument to the constructor of Wine to be able to associate it to a Vineyard (here used for the add operation) :


public function Wine(vineyard:Vineyard = null):void {

    this.vineyard = vineyard;

}

And initialize the collection of wines for a new vineyard :


public function Vineyard():void {

    this.wines = new ArrayCollection();

}

Again, build the application with mvn install, restart jetty and check your changes.

As you can see, this is purely client code. We rely on cascading to persist the changes in the database, and GraniteDS is able to cleanly transfer lazy associations without much hassle.

More, when entities are fetched in the list of vineyards, their collections of wines are still not loaded. When the user selects a vineyard, the binding {vineyard.wines} on the list automatically triggers the loading of the collection from the server. This is completely transparent so you don’t even have to think about it !!

Step 4 : Dirty checking / Undo

If you have played with the application you may have noticed that using bidirectional bindings leads to strange behaviour. Even without saving your changes, the local objects are still modified. GraniteDS tracks all updates made on the managed entities and is able to easily restore the last known stable state of the objects (usually the last fetch from the server).

It’s also easily possible to enable or disable the ‘Save’ button depending on the fact that the user has modified something or not.

To achieve this, we have to ensure that the entity bound to the form is managed by GraniteDS (in particular for newly created entities because entities retrieved from the server are always managed). We have just to add a few lines when the user selects another element in the main list to restore the state of the previously edited element :

import org.granite.tide.spring.Context;

[Inject] [Bindable]

public var tideContext:Context;

private function selectVineyard(vineyard:Vineyard):void {

Managed.resetEntity(this.vineyard);

tideContext.vineyard = this.vineyard = vineyard;

vineyardsList.selectedItem = vineyard;

}

[/js]

Then we can use the meta_dirty property of the Tide context to enable/disable the ‘Save’ button :


<s:Button id="saveVineyard" label="Save"

     enabled="{tideContext.meta_dirty}" click="save()"/>					

mvn install, jetty, …

Step 5 : Validation

Great, we can now create, edit and search in our database. Now we would like to ensure that the data is consistent. Instead of manually defining Flex validators on each field, we are going to use the Bean Validation API on the server and its GraniteDS implementation on the client.

First let’s add a few Bean Validation annotations on the model :

	

@Basic

@Size(min=5, max=100, 

    message="The name must be between {min} and {max} characters")

private String name;



@Basic

@Min(value=1900, 

    message="The year must be greater than {value}")

@Max(value=2050, 

    message="The year must be less than {value}")

private Integer year;

    

@Enumerated(EnumType.STRING)

@NotNull

private Type type;


@Basic

@Size(min=5, max=100, 

    message="The name must be between {min} and {max} characters")

private String name;

    

@OneToMany(cascade=CascadeType.ALL, 

    mappedBy="vineyard", orphanRemoval=true)

@Valid

private Set<Wine> wines;

This will at least ensure that we cannot save invalid entities. However we would like that our user is informed that the operation has failed. One ugly way would be to add a fault handler on the save operation call with an alert. Instead we are simply going to use the FormValidator component that will validate the entity locally and interpret server exceptions to propagate the error messages to the correct input field.

First you have to register the validation exception handler that will process the validation errors coming from the server. This is not required in this example because all the constraints can be processed locally on the client, but it’s always useful in case the server has additional constraints. Just add this in the init method of Main.mxml.


Spring.getInstance().addExceptionHandler(ValidatorExceptionHandler);

In Home.mxml, add the v namespace and define a FormValidation attached to the edit form and the bound entity :


<s:VGroup

    xmlns:fx="http://ns.adobe.com/mxml/2009"

    xmlns:s="library://ns.adobe.com/flex/spark"

    xmlns:mx="library://ns.adobe.com/flex/mx"

    xmlns:v="org.granite.validation.*"

    xmlns:e="com.wineshop.entities.*"

    xmlns="*"

    width="100%" height="100%"

    initialize="selectVineyard(new Vineyard())">

    ...


<fx:Declarations>		

    <e:Vineyard id="vineyard"/>		

    <s:ArrayCollection id="wineTypes" source="{Wine$Type.constants}"/>		

    <v:FormValidator id="formValidator" 

        entity="{vineyard}" 

        form="{formVineyard}"/>

</fx:Declarations>

We can also define a FormValidator for the item renderer :


<s:itemRenderer>

    <fx:Component>

        <s:ItemRenderer>

            <fx:Declarations>

                <v:FormValidator id="wineValidator" 

                    form="{wineEdit}" entity="{data}"/>

            </fx:Declarations>

			

            <s:states><s:State name="normal"/></s:states>                   

            <s:HGroup id="wineEdit">

                <s:TextInput text="@{data.name}"/>

                <s:TextInput text="@{data.year}"/>

                <s:DropDownList 

                    selectedItem="@{data.type}" 

                    requireSelection="true" 

                    dataProvider="{outerDocument.wineTypes}" 

                    labelField="name"/>

            </s:HGroup>

        </s:ItemRenderer>							

    </fx:Component>

</s:itemRenderer>

These two declarations will allow to display error messages on the field during editing. The FormValidator takes advantage of the replication of the Bean Validation annotations to ActionScript 3 to know what validations have to be applied on the client data.

Finally we can keep the user from trying to save an invalid object by adding the following line :

private function save():void {

if (formValidator.validateEntity())

wineshopService.save(vineyard);

}

[/js]

mvn install, jetty, …

Step 6 : Real-time data push

Enabling data push is just a question of configuration. There are 4 things to check :

  • Declare a topic messaging destination in the Spring configuration app-config.xml
  • 
    <graniteds:messaging-destination id="wineshopTopic" 
    
        no-local="true" session-selector="true"/>
    
    
  • Declare the topic and publish mode on the @DataEnabled annotation on the exposed services
  • 
    @RemoteDestination
    
    @DataEnabled(topic="wineshopTopic", publish=PublishMode.ON_SUCCESS)
    
    public interface WineshopService {
    
    
  • Declare the DataObserver for this topic in Main.mxml
  • 
    Spring.getInstance().addComponent("wineshopTopic", DataObserver);
    
    Spring.getInstance().addEventObserver("org.granite.tide.login", 
    
        "wineshopTopic", "subscribe");
    
    Spring.getInstance().addEventObserver("org.granite.tide.logout", 
    
        "wineshopTopic", "unsubscribe");
    
    

Of course the three declarations should use the same topic name, but this is all you need to enable data push.

mvn install, jetty, …

Now if you open many browsers, all the changes made in one browser should be dispatched to all other browsers.

Step 7 : Conflict handling

In any multiuser application, there may be cases where different users do changes on the same entity concurrently. Using optimistic locking is a common technique to handle these cases and avoid database inconsistencies. GraniteDS is able to handle these errors cleanly in either normal remoting operations or with real-time data push.

This is quite simple to configure, you just need to register an exception handler for the JPA OptimistickLockException and an event listener on the Tide context that will be called when a concurrent modification conflict occurs :


private function init():void {

    ...

    Spring.getInstance().addExceptionHandler(OptimisticLockExceptionHandler);



    Spring.getInstance().getSpringContext().addEventListener(

        TideDataConflictsEvent.DATA_CONFLICTS, conflictsHandler);

}

			

private function conflictsHandler(event:TideDataConflictsEvent):void {

    Alert.show("Someone has modified this vineyard at the same timen. "

        + "Keep your changes ?", 

        "Conflict", Alert.YES | Alert.NO, null, function(ce:CloseEvent):void {

        if (ce.detail == Alert.YES)

            event.conflicts.acceptAllClient();

        else

            event.conflicts.acceptAllServer();

    });

}

The most difficult part is to actually obtain a conflict. After your rebuild and restart jetty, open two browsers. Create a vineyard in one of the browsers, it will appear in the second one. Edit it in the second browser and change something, for example its name, without saving. Then in the first browser, change the name to a different value and save. An alert should appear in the second browser.

Step 8 : Reverse lazy loading

This final feature is not very visual, but it can improve a lot the performance of your application. The support for server-to-client lazy loading ensures that the amount of data transferred in this direction is limited, but a problem can arise in the other direction (client-to-server). Once all your object graph is loaded on the client by transparent loading or manual operations, the complete loaded object graph will be sent back to the server even when only a property of the root object has been changed. With very deep and complex object graphs, this can really kill the performance of write operations.

GraniteDS now provides a new feature called reverse lazy loading that allows to fold the object graph before transmitting it to the server. It will take in account the changes made locally by the user to fold all parts of the graph that have not been updated, and still send the parts of the graph containing the changes.

This can be setup as follows : in the initialization method of the application, register an argument preprocessor :


Spring.getInstance().addComponents([UninitializeArgumentPreprocessor]);

And then in the service methods that update entities, simply add the @Lazy annotation to the incoming arguments :


public void save(@Lazy Vineyard vineyard);

To really see what happens, you have to run mvn jetty:run-war in debug mode from the Eclipse M2E plugin, and put a breakpoint in the method save(). Then create a vineyard and some wines, and save it. Close and reopen the browser to restart from a clean state, edit and change the name of the vineyard you just created and click on Save. By inspecting the incoming Vineyard object on the debugger, you can see that the collection of wines is marked uninitialized. Now change the name of one of the wine and click on Save. This time the collection will be initialized and contain the Wine object you just updated.

Or you can just trust that it works and that it’s better to use it than not…

Conclusion

We’re done with this tutorial on the data management features. You are now be able to see what you can do with GraniteDS and how it can simplify your developments, and even bring new possibilities for your applications.

Tags:

Author: Franck

This post has 5 Comments

  1. Jeff Plummer on December 22, 2011 at 8:01 pm Reply

    Good article. I can’t wait to try the reverse lazy loading.

    I hope you guys keep posting tutorials and the like. It shows graniteDS is still active and with the recent announcements from Adobe there are a lot of people nervous about Blaze and LCDS.

  2. Michael Montoya on February 5, 2012 at 4:47 am Reply

    I could not get this example to compile running maven 3.0.3. Also, your mvn archetype is incorrect. To download the archetype you will need to remove “org” from the artifactId tag and add “-hibernate” to the same tag like so: “graniteds-tide-jpa-hibernate”

  3. Michael Montoya on February 5, 2012 at 6:06 am Reply

    I was able bringing the project into Eclipse and compile each module separately and reassemble to load in Tomcat 7.0 – unfortunately, the application does not exhibit any data push. When I have to instances running, the only way to get my new vineyard to appear in the other client is to manually enter a search for the new vineyard. Any ideas of what might be the issue? My guess is that the Gravity servlet is failing.

  4. Michael Montoya on February 6, 2012 at 3:47 am Reply

    Interestingly, data push works for me when I deploy the resulting wineshop-admin.war in Jetty 7.5.0 but not in Tomcat7. Any ideas why this might be?

  5. eddie on March 21, 2012 at 7:01 pm Reply

    I’m receiving a couple maven errors that say “Plugin execution not covered by lifecycle configuration: …” I tried looking this issue up and configuring the maven lifecycle mapping; however, I was unable to get it working. Any thoughts?

Leave a Comment