ExtJS, Spring MVC 3 e Hibernate 3.5: Exemplo de um CRUD Grid

2 de September de 2010 | By | 34 Comments

Este tutorial demonstra como implementar um CRUD Grid (Create, Read, Update, Delete) usando ExtJS, Spring MVC 3 e Hibernate 3.5

O que geralmente queremos fazer com os dados

  • Create (Criar) – (Insert)
  • Read (Ler/Visualizar) – (Select)
  • Update (Atualizar) – (Update)
  • Delete (Deletar) – (Delete)

Até a versão 3.0 do ExtJS, podíamos apenas LER dados utilizando o componente dataGrid. Se você quisesse fazer um update, insert ou delete, você tinha que codificar funções específicas para essas ações no lado ExtJS. Com a versão 3.0 (e versões mais recentes) do ExtJS, a biblioteca javascript introduziu o ext.data.writer, e você não tem todo aquele trabalho de criar as funções específicas, pode utilizar o Writer para ter um CRUD Grid.

Mas o que é preciso para ter todas as funcionalidades funcionando apenas com o uso desse writer?

No exemplo desse tutorial, estou usando JSON como formato de dados para troca de informações entre brwoser e servidor.

Código ExtJS

Primeiro, é preciso criar um Ext.data.JsonWriter:

 // The new DataWriter component.
    var writer = new Ext.data.JsonWriter({
        encode: true,
        writeAllFields: true
    });

Onde writeAllFields significa que queremos enviar todos os campos do registro para o banco de dados. identifies that we want to write all the fields from the record to the database. Se você tem uma estrutura de dados um pouco complicada ou o usuário irá fazer muitas iterações de update, é melhor deixar setado como false.

Por exemplo, Essa é a declaração da minha estrutura de dados no ExtJS:

    var Contact = Ext.data.Record.create([
	{name: 'id'},
    {
        name: 'name',
        type: 'string'
    }, {
        name: 'phone',
        type: 'string'
    }, {
        name: 'email',
        type: 'string'
    }]);

Se eu apenas atualizar o nome do contato, a aplicação irá apenas enviar o nome do contato e a id do mesmo para o servidor dizendo que foi atualizado (se o campo writeallfields estiver como false). Se tiver setado como true, irá enviar todos os campos, e o trabalho para descobrir o que sofreu alteração ficará para o server. Como o hibernate possui o método saveOrUpdate, precisamos enviar o objeto completo para o server, pois o hibernate descobre sozinho o que é preciso fazer update ou não.

Agora, é necessário configurar o proxy, como esse:

    var proxy = new Ext.data.HttpProxy({
        api: {
            read : 'contact/view.action',
            create : 'contact/create.action',
            update: 'contact/update.action',
            destroy: 'contact/delete.action'
        }
    });

E só para constar, é assim que meu reader se parece:

    var reader = new Ext.data.JsonReader({
        totalProperty: 'total',
        successProperty: 'success',
        idProperty: 'id',
        root: 'data',
        messageProperty: 'message'  // <-- New "messageProperty" meta-data
    },
    Contact);

O próximo passo é juntat tudo (writer, proxy e reader) no objeto store:

 // Typical Store collecting the Proxy, Reader and Writer together.
    var store = new Ext.data.Store({
        id: 'user',
        proxy: proxy,
        reader: reader,
        writer: writer,  // <-- plug a DataWriter into the store just as you would a Reader
        autoSave: false // <-- false would delay executing create, update, destroy requests until specifically told to do so with some [save] buton.
    });

O autosave significa que deseja salvar as alterações automaticamente no servidor (não precisa de um botão salvar na tela, assim que o usuário atualizar, deleter ou criar um novo dado, será enviado automaticamente para o servidor). Para este exemplo, implementei um botão salvar, assim, qualquer registro ou dado que for adicionado ou alterado terá uma marcação vermelha (no canto superior esquerdo da célula), assim quando o evento (ou botão) salvar for disparado, serão enviados para o servidor os dados que sofreram alteração (marcados com o flag vermelho). Você pode fazer múltiplos updates e enviar todos para o servidor em apenas uma vez (Observe como isso foi tratado no código da classe de serviço no código fonte desse projeto).

E para deixar a vida ainda mais fácil (afinal, pra isso que usamos bibliotecas como ExtJS :D ), vamos usar o plugin RowEditor, que permite a edição dos dados de forma muito simples. Tudo o que precisa fazer para usar esse plugin é primeiro adicionar os arquivos necessários na sua página HTML (ou JSP, ou outra extensão!):

<!-- Row Editor plugin css -->
	<link rel="stylesheet" type="text/css" href="/extjs-crud-grid/ext-3.1.1/examples/ux/css/rowEditorCustom.css" />
	<link rel="stylesheet" type="text/css" href="/extjs-crud-grid/ext-3.1.1/examples/shared/examples.css" />
	<link rel="stylesheet" type="text/css" href="/extjs-crud-grid/ext-3.1.1/examples/ux/css/RowEditor.css" />

<!-- Row Editor plugin js -->
	<script src="/extjs-crud-grid/ext-3.1.1/examples/ux/RowEditor.js"></script>

E adicionar o plugin na declaração do grid:

    var editor = new Ext.ux.grid.RowEditor({
        saveText: 'Update'
    });

    // create grid
    var grid = new Ext.grid.GridPanel({
        store: store,
        columns: [
            {header: "NAME",
             width: 170,
             sortable: true,
             dataIndex: 'name',
             editor: {
                xtype: 'textfield',
                allowBlank: false
            }},
            {header: "PHONE #",
             width: 150,
             sortable: true,
             dataIndex: 'phone',
             editor: {
                 xtype: 'textfield',
                 allowBlank: false
            }},
            {header: "EMAIL",
             width: 150,
             sortable: true,
             dataIndex: 'email',
             editor: {
                xtype: 'textfield',
                allowBlank: false
            }})}
        ],
        plugins: [editor],
        title: 'My Contacts',
        height: 300,
        width:610,
		frame:true,
		tbar: [{
            iconCls: 'icon-user-add',
            text: 'Add Contact',
            handler: function(){
                var e = new Contact({
                    name: 'New Guy',
                    phone: '(000) 000-0000',
                    email: 'new@loianetest.com'
                });
                editor.stopEditing();
                store.insert(0, e);
                grid.getView().refresh();
                grid.getSelectionModel().selectRow(0);
                editor.startEditing(0);
            }
        },{
            iconCls: 'icon-user-delete',
            text: 'Remove Contact',
            handler: function(){
                editor.stopEditing();
                var s = grid.getSelectionModel().getSelections();
                for(var i = 0, r; r = s[i]; i++){
                    store.remove(r);
                }
            }
        },{
            iconCls: 'icon-user-save',
            text: 'Save All Modifications',
            handler: function(){
                store.save();
            }
        }]
    });

Código Java

E Finalmente, precisamos de código no lado servidor.

Controller:

package com.loiane.web;

@Controller
public class ContactController  {

	private ContactService contactService;

	@RequestMapping(value="/contact/view.action")
	public @ResponseBody Map<String,? extends Object> view() throws Exception {

		try{

			List<Contact> contacts = contactService.getContactList();

			return getMap(contacts);

		} catch (Exception e) {

			return getModelMapError("Error retrieving Contacts from database.");
		}
	}

	@RequestMapping(value="/contact/create.action")
	public @ResponseBody Map<String,? extends Object> create(@RequestParam Object data) throws Exception {

		try{

			List<Contact> contacts = contactService.create(data);

			return getMap(contacts);

		} catch (Exception e) {

			return getModelMapError("Error trying to create contact.");
		}
	}

	@RequestMapping(value="/contact/update.action")
	public @ResponseBody Map<String,? extends Object> update(@RequestParam Object data) throws Exception {
		try{

			List<Contact> contacts = contactService.update(data);

			return getMap(contacts);

		} catch (Exception e) {

			return getModelMapError("Error trying to update contact.");
		}
	}

	@RequestMapping(value="/contact/delete.action")
	public @ResponseBody Map<String,? extends Object> delete(@RequestParam Object data) throws Exception {

		try{

			contactService.delete(data);

			Map<String,Object> modelMap = new HashMap<String,Object>(3);
			modelMap.put("success", true);

			return modelMap;

		} catch (Exception e) {

			return getModelMapError("Error trying to delete contact.");
		}
	}

	private Map<String,Object> getMap(List<Contact> contacts){

		Map<String,Object> modelMap = new HashMap<String,Object>(3);
		modelMap.put("total", contacts.size());
		modelMap.put("data", contacts);
		modelMap.put("success", true);

		return modelMap;
	}

	private Map<String,Object> getModelMapError(String msg){

		Map<String,Object> modelMap = new HashMap<String,Object>(2);
		modelMap.put("message", msg);
		modelMap.put("success", false);

		return modelMap;
	}

	@Autowired
	public void setContactService(ContactService contactService) {
		this.contactService = contactService;
	}

}

Algumas observações:

No Spring 3, é possível obter os objetos do request diretamente nos parâmetros do método utilizando a anotação @RequestParam. Não sei porque, mas não funcionou com o CRUD do ExtJS. Tive que deixar como Object e fazer o parser de JSON-Objeto “na mão”. Por isso que utilizei uma classe Util – para fazer o parser do Objeto do request para a minha classe POJO. Se souber de um jeito para consertar isso, por favor, deixe um comentário ou entre em contato. Realmente quero saber a solução! :)

Classe de Serviço:

package com.loiane.service;

@Service
public class ContactService {

	private ContactDAO contactDAO;
	private Util util;

	@Transactional(readOnly=true)
	public List<Contact> getContactList(){

		return contactDAO.getContacts();
	}

	@Transactional
	public List<Contact> create(Object data){

        List<Contact> newContacts = new ArrayList<Contact>();

		List<Contact> list = util.getContactsFromRequest(data);

		for (Contact contact : list){
			newContacts.add(contactDAO.saveContact(contact));
		}

		return newContacts;
	}

	@Transactional
	public List<Contact> update(Object data){

		List<Contact> returnContacts = new ArrayList<Contact>();

		List<Contact> updatedContacts = util.getContactsFromRequest(data);

		for (Contact contact : updatedContacts){
			returnContacts.add(contactDAO.saveContact(contact));
		}

		return returnContacts;
	}

	@Transactional
	public void delete(Object data){

		//it is an array - have to cast to array object
		if (data.toString().indexOf('[') > -1){

			List<Integer> deleteContacts = util.getListIdFromJSON(data);

			for (Integer id : deleteContacts){
				contactDAO.deleteContact(id);
			}

		} else { //it is only one object - cast to object/bean

			Integer id = Integer.parseInt(data.toString());

			contactDAO.deleteContact(id);
		}
	}

	@Autowired
	public void setContactDAO(ContactDAO contactDAO) {
		this.contactDAO = contactDAO;
	}

	@Autowired
	public void setUtil(Util util) {
		this.util = util;
	}
}

Classe Contato – POJO:

package com.loiane.model;

@JsonAutoDetect
@Entity
@Table(name="CONTACT")
public class Contact {

	private int id;
	private String name;
	private String phone;
	private String email;

	@Id
	@GeneratedValue
	@Column(name="CONTACT_ID")
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	@Column(name="CONTACT_NAME", nullable=false)
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Column(name="CONTACT_PHONE", nullable=false)
	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

	@Column(name="CONTACT_EMAIL", nullable=false)
	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}
}

Classe DAO:

package com.loiane.dao;

@Repository
public class ContactDAO implements IContactDAO{

	private HibernateTemplate hibernateTemplate;

	@Autowired
	public void setSessionFactory(SessionFactory sessionFactory) {
		hibernateTemplate = new HibernateTemplate(sessionFactory);
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Contact> getContacts() {
		return hibernateTemplate.find("from Contact");
	}

	@Override
	public void deleteContact(int id){
		Object record = hibernateTemplate.load(Contact.class, id);
		hibernateTemplate.delete(record);
	}

	@Override
	public Contact saveContact(Contact contact){
		hibernateTemplate.saveOrUpdate(contact);
		return contact;
	}
}

Classe Util:

package com.loiane.util;

@Component
public class Util {

	public List<Contact> getContactsFromRequest(Object data){

		List<Contact> list;

		//it is an array - have to cast to array object
		if (data.toString().indexOf('[') > -1){

			list = getListContactsFromJSON(data);

		} else { //it is only one object - cast to object/bean

			Contact contact = getContactFromJSON(data);

			list = new ArrayList<Contact>();
			list.add(contact);
		}

		return list;
	}

	private Contact getContactFromJSON(Object data){
		JSONObject jsonObject = JSONObject.fromObject(data);
		Contact newContact = (Contact) JSONObject.toBean(jsonObject, Contact.class);
		return newContact;
	}
)
	private List<Contact> getListContactsFromJSON(Object data){
		JSONArray jsonArray = JSONArray.fromObject(data);
		List<Contact> newContacts = (List<Contact>) JSONArray.toCollection(jsonArray,Contact.class);
		return newContacts;
	}

	public List<Integer> getListIdFromJSON(Object data){
		JSONArray jsonArray = JSONArray.fromObject(data);
		List<Integer> idContacts = (List<Integer>) JSONArray.toCollection(jsonArray,Integer.class);
		return idContacts;
	}
}

Se quiser visualizar o código inteiro dessa app de exemplo (ou fazer o donwload do código completo), visite o meu repositório do GitHub: http://github.com/loiane/extjs-crud-grid-spring-hibernate

Este post foi um pedido de alguns leitores do post anterior de CRUD Grid (e do blog em inglês), além de ter recebido alguns emails. Fiz alguns ajustes em relação ao post anterior, mas a idéia ainda é a mesma. Espero ter respondido a todas as perguntas em relação a esse assunto. :)

Bons códigos!

Posts Similares

Filed in: Ext JS 3, Hibernate, Spring

Comments (34)

Links to this Post

  1. Retrospectiva 2010 | Loiane Groner | 23 de December de 2010
  1. Pra que ter esse DAO? Porque não jogou a responsa toda pro POJO?

    Parabéns pelo compartilhamento da informação.

    Dalton Camargo

  2. @Dalton Estou usando MVC – model view controller.

    Obrigada!

  3. Ivan Salvadori

    Parabéns Loiane, ótimo material.

    Acredito eu, ser possível usar o MVC e ainda sim deixar a responsabilidade para o POJO, oque eu não costumo fazer tb.
    Acredito ter mais relação com objetos de domínio anêmico., que o uso do padrão MVC.

    Ao fazer isso, tira-se toda parte comportamental da entidade, colocando em oque seria um controle. ai a Pergunta….. A logica fica no controle(serviço, afins) ou no modelo???

    Eis uma questão que persegue muitos programadores.
    Já vi muita discussão sobre isso.
    Não acredito muito em certo ou errado, apenas são diferentes…

    Sempre acompanho seus materiais, são ótimos.

  4. @Ivan
    Bem, eu nunca deixei a parte de BD para o POJO, sempre usei DAO por ser mais fácil de dar manutenção e ser mais legível. MVC + DAO sempre foi a combinação perfeita (na minha opinião). Nas empresas que trabalhei, sempre fizeram dessa forma.

    Lógica no Controller acho um absurdo! Controller foi feito apenas para redirecionar as chamadas e retornar o resultado da operação. O Serviço é quem deve ficar encarregado da lógica. Esse é o MVC que conheço. ;)

    Isso não indica que eu esteja certa, é apenas minha opinião! :)

  5. Ivan Salvadori

    Partilho da mesma opiniao.
    Obrigado pelas informacoes Loiane.

  6. CARLOS

    Olá, estou tendo o seguinte problema….
    quando vai tentar carregar os contatos dah o seguinte erro:
    “CONTACT is not mapped [FROM CONTACT]; nested exception is org.hibernate.hql.ast.QuerySyntaxException: CONTACT is not mapped [FROM CONTACT]”

    isso acontece na classe ContactDAo no método getContacts .
    valeuuu

  7. Junior

    Poderia postar um exemplo usando outro Framework MVC, nada contra o Spring =)
    fica a sugestão

    obrigado

  8. Hola loaine excelente tu tutorial gracias, tengo una consulta? en la parte de Hibernate tu usas el hibernate template con lo que las anotaciones las colocas en la capa servicio, ahora he visto tambien que se usa el hibernateDaoSuport el cual se hereda en los daos segun tu experiencia cual de las dos maneras es mas recomendable trabajar con hibernate template? o con los hibernateDaoSuport? si pudieras ayudar con esta duda te lo agradeceria muchisimo.

    Saludos

  9. Hola loiane una consulta en el ejemplo estas usando las anotacioens @transactional , en la parte de la configuracion xml de spring no veo esta linea

    segun la documentacion de spring esta linea se usaba para las anotaciones @transactional mi pregunta es
    Se te olvido poner esta linea en el xml configuración de spring? o lo remplazaste con otro comando?

    Gracias

  10. Douglas

    Loiane, Otimo tutorial, diga-se de passagem é dificil conseguir algum que realmente funcione, Parabens.

  11. João Paulo

    Olá Loiane,

    Muito bom seu tutorial… eu estava tentando resolver o problema que você levantou durante o tutorial, eu acho que consegui algo que pode ajudar… vou terminar um exemplo e mando para vc pra vc ver o que acha

    vlws

    t+

  12. @JoaoPaulo
    Muito obrigada, se quiser pode enviar sim.
    Até publico aqui no blog com seus créditos! :)

  13. Olá Loiane, estou com uma pequena dúvida gostaria de saber se vc pode me dar uma dica (não sei se esse eh o melhor canal para isso, se tiver outro melhor me avise…) Na mesma idéia deste tutorial supondo que tenho uma entidade java
    Bairro {
    Long id;
    String descricao;
    Cidade cidade;
    }

    Como monto o objeto JSON de bairro para enviar para o Controlador Java de modo que utilizando JSONObject.toBean(jsonObject, Bairro.class); eu consiga esse Cast de forma perfeita. Tem como fazer ou tem que tratar manualmente no controlador?
    Espero ter me expressado de forma correta.
    Parabéns pelos artigos do seu blog e muito obrigado.

  14. Ei Guilherme,

    O Ext.Ajax request faz isso pra vc.

  15. Marcio

    Loiane tenho uma dúvida sobre o HibernateTemplate. Nunca o utilizei, sempre usei o objeto Session e dele os métodos necessários para persistência.
    Você recomenda o uso do HibernateTemplate para o desenvolvimento de aplicações? 

  16. Ei Marcio,

    Pra ser sincera nunca trabalhei com HibernateTemplate num projeto, então não sou a melhor pessoa para te responder. :)

    []‘s

  17. Marcio

    Ok Loiane, obrigado.

  18. Alejandro Mesias

    Estou pensando em trabalhar com colunas dinâmicas, à principio o usuario poderia especificar colunas da tabela dinamicamente. Me parece que talvez não seja tão complicado pois em todos os tratamentos de dados que o ExtJs recebe sempre são Mapas. Então imagino que com isso ele dê flexibilidade o suficiente para isso, só vou ter um pouco de trabalho na classe de serviço para “ajeitar” esses dados, fazendo um mix entre os dados que definem a tabela e os dados propriamente ditos.

    Não conhecia isso de por anotação nos parametros do metodo e no meio do metodo:
    … public @ResponseBody Map delete(@RequestParam ….
    Tenho escrito algumas anotações nos Pojos para alguns processamentos meus, por isso que estranhei.

  19. @Alejandro,
    Sim, trabalhar com grid dinâmico dá na mesma que grid estático, tudo é um objeto. Você só precisa deixar o backend dinâmico também, para efetuar as queries no banco, que também serão dinâmicas.
    Escrevi um tutorial sobre isso tempos atrás, mas só a parte do Exté que está dinâmica, o java deixei hadrcoded mesmo: http://www.loiane.com/2009/07/como-construir-um-grid-dinamico-utilizando-ext-js/

  20. Maycon Belfort

    Olá Loiane, procurei em seus blogs o exemplo seu postado no forum http://www.extjs.com.br/forum/index.php?topic=5005.0 onde esse CRUD era feito com ExtJS 4, mas não achei… então posto aqui mesmo =)

    Minhas dúvidas são as seguintes:

    - Quando o usuário clicar em Add ou editar um registro, você chama a função de edit e quando ele clicar em salvar, aciona a função update. Nesta função vc cria o registro, add no grid e depois manda sincronizar, o que eu encaro como se fosse salvar o registro. O que eu quero fazer é: Mandar para o servidor, salvar no banco e depois dá um reload no grid, mas não faço a mínima ideia de como mandar o objeto (no seu caso Contact) preenchido com as informações do form.

    - A segunda dúvida é a seguinte: nesse mesmo arquivo Controller dos contatos, você utiliza uma função (getContatcsStore e getContactList) dá onde vem essas funções? Para que elas servem?

    Bom Loiane, não sei se esse era o melhor lugar para este post, mas não achei o artigo… =[ Espero que possa me ajudar… 

    Obrigado!

  21. Olá Maycon,
    O método sync da store já faz o que vc quer. Ele salva no servidor e já fica com o valor atualizado.

    Os métodos são gerados pelo Ext JS. Tem um padrão de nomenclatura. Tente usar o firebug ou a ferramenta de desenvolvedor do chrome para visualizar o prototype da classe. O get Store vai obter a Store e o get List vai obter o grid. Repare que também dei um apelido para a grid na parte de refs do Controller.

    []‘s

  22. Raphael

    Loiane, parabéns pelo tutorial, show de bola!

    Segue abaixo algumas dúvidas:
    1. Nesse exemplo do CRUD eu poderia utilizar os recursos nativos do container (JEE 6 + CDI) sem o spring?
    2. Ao invés de utilizar as anotações que o spring oferece para controlar as requisições, seria possível utilizar àquelas existente no jsf 2?

    []‘s

  23. Olá Raphael,
    Você pode usar qualquer tecnologia server side no lugar do spring, basta que o framework ou linguagem suportem troca de dados com json ou xml.
    As 2 tecnologias que mencionou possuem esse suporte.
    Dê uma olhada nesse link em relação ao JSF: http://www.srikanthtechnologies.com/blog/java/jquerywithjsf.aspx
    []‘s

  24. Dyener

    Olá Loiane, imagino que você já deve estar cansada de tantos elogios, mesmo assim deixo aqui minha gratidão por poder ler um post de tamanha qualidade como este.

    Ainda não conhecia seu trabalho, más a partir de agora com certeza nos veremos outras vezes por aqui.

    Saudações.

  25. Sandro Mueller

    Muito bom, é o que eu procurava, “aconcelhado” por vc mesmo… :)
    amanha mesmo vou por em produção…

  26. Ederson Schmeing

    Olá Loiane! Venho acompanhando seu trabalho desde que descobri o blog. Conteúdo de com muita qualidade.
    Estou tendo problemas com collection no spring, quando vai serializar para json, ele retorna erro dizendo que a sessão esta fechada.
    Como posso resolver? Pesquisei e testei algumas coisas que encontrei, mas nada funcionou.

  27. Olá Ederson,
    Como está sua configuração?

  28. Jonas

    Olá Loiane!
    Primeiramente parabéns! sou novo por aqui estou começando a acompanhando o curso Extjs4..
    Dai achei este tópico, ele foi feito em cima do Extjs 3.2, muda mta coisa se for ExtJs 4?
    Axei legal deste tópico pq ele tem a parte do servidor em java, que é o que eu queria usar.. aproveitando a oportunidade seria legal que o curso do ExtJs4 tivesse Java além do PHP.. :P
    E parabéns de novo! to achando fantástico o ExtJS, tinha começado a estudar o flex mas dai veio a Adobe e matou ele, renovei minhas esperanças com ExtJs.

  29. Comentei no Post errado desculpe,
    sou iniciante e não consegui compilar este projeto no eclipse uso o indigo e necessario algum plugin? tem algum tutorial de como instalar.

    Obrigado

  30. Oi Everton,
    Vc precisa ter o tomcat instalado.
    Talvez a versão do Java que vc esteja usando seja diferente da minha, ajeite o build path da sua aplicação no eclipse.
    []‘s

  31. Obrigada Jonas,
    VOu postar alguns exemplos que fiz no curso em Java aqui no blog tb.
    []‘s

  32. Everson

    Loiane, show de bola!

    Obrigado pelo post.

Leave a Reply

Trackback URL | RSS Feed for This Entry