Cocomonio


Blog


GraniteDS – Data Management

By ffrizzo February 19th, 2012 Uncategorized No Comments

Vamos configurar um projeto simples, que aborde os principais recursos do GraniteDS usando Spring/Hibernate.

Vamos utilizar o projeto do Willian Draï, ele está disponível em http://github.com/wdrai/wineshop-admin é necessário ter o Maven 3.x.

Para baixar o projeto e executar de forma rapida, basta seguir os passos a baixo.

git clone git://github.com/wdrai/wineshop-admin.git
cd wineshop-admin
mvn clean package
cd webapp
mvn jetty:run-war

Agora basta acessar o seguinte endereço  http://localhost:8080/wineshop-admin/wineshop-admin.swf, os usuários disponíveis são admin/admin e user/user.

Este exemplo é um crud simples que permite criar, editar e pesquisar. O Layout da app é feia, mas seu objetivo é simplesmente demonstrar as seguintes características:

  • Crud básico com Services do Spring
  • Suporte a lazy-loading de associações x-to-many
  • Dirty-checking
  • Validação no Cliente
  • Real-Time Data Push
  • Lazy loading reverso

Cada tópico é correspondente a uma tag no GitHub, isso vai servir para que você saiba exatamente o que foi alterado em cada tópico.

Vamos reconstruir o projeto do zero.

Passo 1. Criando o Projeto a partir de um artefato maven.

Esta é a parte mais facil

mvn archetype:generate
-DarchetypeGroupId=org.graniteds.archetypes
-DarchetypeArtifactId=org.graniteds-tide-spring-jpa
-DarchetypeVersion=1.1.0.GA
-DgroupId=com.wineshop
-DartifactId=wineshop-admin
-Dversion=1.0-SNAPSHOT

Em seguida verifique se o projeto está funcional

cd wineshop-admin
mvn clean package
cd webapp
mvn jetty:run-war

Acesse o seguinte endereço http://localhost:8080/wineshop-admin/wineshop-admin.swf. Você deve ser capaz de ver o aplicativo Hello World.

Passo 2. Implementando um Crud básico e funcional

Este passo será maior, nele iremos construir a maior parte da aplicação. Entidade JPA, Serviço do Spring e um Cliente Flex básico.

Está é nossa entidade, não há nada de especial.

@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 wines;

    //Get's e Set's omitido
}
@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;

    //Get's e Set's omitidos
}

Interface do serviço Spring para trabalhar com este modelo.

@RemoteDestination
@DataEnabled(topic="")
public interface WineshopService {
    public void save(Vineyard vineyard);
    public void remove(Long vineyardId);
    public Map list(Vineyard filter,
        int first, int max, String[] sort, boolean[] asc);
}

Como você pode observar temos duas Anotações @RemoteDestination indica que o serviço deve ser exposto ao Flex e um proxy ActionScript3 será gerado para o serviço.  @DataEnable indica que o GraniteDS irá acompanhar as atualizações das entidades JPA e envia-las automaticamente para os clientes.

Implementação do Serviço

@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 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.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 result = new HashMap(4);
        result.put("resultCount", (Long)qcount.getSingleResult());
        result.put("resultList", qlist.getResultList());
        result.put("firstResult", first);
        result.put("maxResults", max);
        return result;
    }
}

Este é um serviço classico do Spring com JPA. Porém temos algumas particularidades.

  • Ele sempre faz merge das entidades. Isto é importante porque os objetos transferidos entre flex e java são considerados objetos detacheds.
  • O método list tem uma assinatura especifica (filter, first, max, sort[], asc[]), isto é usado pela implementação de Paged Collection do GraniteDS.

Mas isto não traz nenhuma dependência com GraniteDS, sendo assim este serviço pode ser usado por qualquer cliente.

Vamos ao cliente Flex:

<?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>

Está não é uma parte complicada, mas temos alguns pontos que devem ser observados.

  • Aqui usamos o componente PagedQuery para exibirmos a lista de vinhas. A configuração no inico ( addComponentWithFactory …) liga o componete de PagedQuery com o serviço Spring, por isso cada vez que o componente precisar buscar dados, ele irá chamar o serviço remoto de forma transparente. Este componente também lida com a paginação, ele irá buscar os dados do servidor conforme o usuário percorere a lista.
  • PagedQuery também trata de forma transparente filtros e ordenação. Basta simplesmente ligar um TextInput ao compoenente, que o dado informado será enviado ao servidor. Quando o usuário clicar em “Search” nós simplesmente precisariamos atualizar a coleção, como fariamos com qualquer outro filtro no cliente. A ordenação é ainda mais fácil e é tratado de forma totalmente transparente quando a coleção estiver ligado a um componete de interface, que permite a ordenação, como o DataGrid.
  • As operações de CRUD funcionam de forma “fire e forget”. Você chama o método do servidor e o GraniteDS vai tratar das atualizações automaticamente, você não precisa fazer nada. Não há nem um handler para o resultado e como pode ver o método do Serviço não retorna nada. Na verdade o GraniteDS escuta todos os eventos da JPA e dispara eles de forma transparente para os clientes Flex, incluindo o cliente que originou a chamada.

Agora você pode fazer um novo build com mvn package, reniciar o jetty e ver as mudanças.

Passo 3. Suporte ao Lazy Loading

Aqui não há muito a fazer, basta adicionar um itemrender com um componente de Formulário para permitir a edição dos vinhos da vinha selecionada.

<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>

Deixamos passar duas coisas: Precisamos mudar o construtor da classe Wine para aceitar como argumento a classe Vineyard. Isto será utilizado pela adição de um novo “Wine”

public function Wine(vineyard:Vineyard = null):void {
    this.vineyard = vineyard;
}

E inicializar a coleção de Wines para a nova Vineyard:

public function Vineyard():void {
    this.wines = new ArrayCollection();
}

Recrie a aplicação com mvn package e reinicie o jetty.

Como você pode observar isto é puramente código Flex. Deixamos para o “Cascade” salvar nossas alterações no banco de dados.

Mas quando as entidades Vineyards são buscadas as listas de Wines ainda não são carregadas. Quando o usuário seleciona uma Vineyard automaticamente ele irá carrega a lista de wines. Isto é completamente transparente para que você não precise pensar nisso.

Passo 4. Dirty Checking/Undo

Se você ja rodou a aplicação pode ter notado que o uso de bindings bidirecionais leva a um comportamento estranho. Mesmo sem salvar as alterações, os objetos locais são modificados. GrantieDS é capaz de rastrear as todas as modificações feitas nas entidades Gerenciadas e é capaz de restaurar o estado estável dos objetos (normalmente a ultima busca feita no servidor).

Tambem é possivel de forma facil habilitar ou desabilitar o botão “Save”, dependendo do estado do objeto.

Para conseguir isso, precisamos apenas de algumas linhas, quando o usuário seleciona outro elemento na lista principal para restaurar o elemento anterior:

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;
}

Então podemos usar a propriedade meta_dirty do contexto Tide para habilitar/desabilitar o botão “Save”

<s:Button id="saveVineyard" label="Save"
     enabled="{tideContext.meta_dirty}" click="save()"/>

mvn clean package, jetty, …

Passo 5. Validação

Agora ja podemos criar, editar e pesquisar no nosso banco de dados. Agora gostariamos de assegurar que os dados são consistentes. Em vez de definirmos validadores Flex para cada campo, vamos usar a api Bean Validation no servidor e a implementação de GraniteDS no cliente.

Primeiro vamos adicionar algumas anotações de Bean Validation ao nosso modeo.

@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 wines;

Isto vai garantir que não podemos salvar entidades inválidas. No entanto gostariamos de informar ao usuário que a operação falhou. Uma maneira feia de fazer isso seria usar um fault Handler com um Alert. Em vez disso vamos usar o componente FormValidator que irá validar a entidade localmente e interpretar as exceçoes do servidor e propagar as mensagens para o campo correto.

Primeiro precisa registrar o ValidatorExceptionHandler que irá processar os erros de validação provenientes do servidor. Neste exemplo isto não é necessário, pois todas as restrições podem ser processadas no cliente. Mas é util, caso o serviço tenha restrições adicionais. Basta adicionar está linha no método init do main.mxml.

Spring.getInstance().addExceptionHandler(ValidatorExceptionHandler);

Em Home.xml defina o namespace “v” e defina um FormValidator ligado ao formulário de edição e a entidade.

<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>

Também podemos definir um FormValidator para o item render:

<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>

Estás duas declarações permitirá exibir mensagens de erro no campo durante a edição. O FormValidator utiliza as anotações de validação do bean ActionScript3 para saber quais validações precisam ser aplicadas.

E por fim podemos impedir o usuário de salvar adicionando uma linha.

private function save():void {
    if (formValidator.validateEntity())
        wineshopService.save(vineyard);
}

mvn package, jetty, …

Passo 6. Real-Time Data Push

Habilitar o Data Push é apenas uma questão de configuração. Temos 4 itens a verificar.

  • Declare um tópico de mensagens no arquivo de configuração do spring(app-config.xml)
<graniteds:messaging-destination id="wineshopTopic"
    no-local="true" session-selector="true"/>
  • Defina o tópico e o modo de publicar na anotação @DataEnabled nos serviços expostos.
@RemoteDestination
@DataEnabled(topic="wineshopTopic", publish=PublishMode.ON_SUCCESS)
public interface WineshopService {
  • Declare o DataObserver para este tópico no Main.xml
Spring.getInstance().addComponent("wineshopTopic", DataObserver);
Spring.getInstance().addEventObserver("org.granite.tide.login",
    "wineshopTopic", "subscribe");
Spring.getInstance().addEventObserver("org.granite.tide.logout",
    "wineshopTopic", "unsubscribe");

Claro que as três declarações devem usar o mesmo nome do tópico. Mas isto é tudo o que você precisa para permitir o envio de dados.

mvn package, jetty, …

Agora basta abrir vários browsers, e todas as alterações feitas em um navegador devem ser enviadas a todos os outros.

Passo 7. Conflict Handling

Em qualquer aplicação multiusuário, pode haver vários usuários fazendo alterações simultaneamente na mesma entidade. Usar optimistic look é a forma commun de lidar com estes casos e evitar inconssistência dos dados. O GraniteDS é capaz de lidar com este problema tanto em chamadas normais quanto com chamadas em real-time data push.

Isso é muito simples de configurar, você só precisar registrar um manipuladaro para exceções JPA OptimistickLockException e um event listener no contexto do Tide, que será chamado quando um conflito por modificação concorrente acontecer.

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 time\n. "
        + "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();
    });
}

A parte mais difícil realmente é obter um conflito. Após mvn package, jetty… Abra dois navegadores, crie uma vinha no primeiro, ele irá aparecer no segundo. Edite o registro no segundo navegador altere o nome mas não salve. No primeiro navegador mude para um nome diferente e salve. No segundo navegador um alerta deve aparecer.

Passo 8. Lazy Loading Reverso

Este ultimo passo não é visual mas pode melhorar muito o desempenho da sua aplicação. O Suporte a lazy loading servidor-cliente garante que a quantidade de dados transferidos neste sentido é limitado, mas pode ocorrer um problema no sentido contrário(cliente-servidor). Uma vez que todo o grafo do seu Objeto é carregado para o cliente, o grafo todo será enviado ao servidor, mesmo que você tenha alterado sómente uma propriedade raiz do seu objeto. Com grafos mais profundos e complexos isto pode prejudicar o desenpenho de operações de salvar.

Para resolver isso GraniteDS oferece agora um novo recurso chamado de “reverse lazy loading”.

Isto pode ser configurado da seguinte forma

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

E então basta adicionar no método de update a anotação @Lazy para os argumentos de entrada

public void save(@Lazy Vineyard vineyard);

Conclusão

Agora você é capaz de ver o que o GraniteDS pode oferecer e como ele pode simplificador o seu desenvolvimento, e até mesmo trazer novas possibilidades para sua aplicação



Post Comment


Your email address will not be published. Required fields are marked *