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

02 Sep 2010
8 mins read

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

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:

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
// The new DataWriter component.
var writer = new Ext.data.JsonWriter({
encode: true,
writeAllFields: true
});
[/code]

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:

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
var Contact = Ext.data.Record.create([
{name: 'id'},
{
name: 'name',
type: 'string'
}, {
name: 'phone',
type: 'string'
}, {
name: 'email',
type: 'string'
}]);
[/code]

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:

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
var proxy = new Ext.data.HttpProxy({
api: {
read : 'contact/view.action',
create : 'contact/create.action',
update: 'contact/update.action',
destroy: 'contact/delete.action'
}
});
[/code]

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

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
var reader = new Ext.data.JsonReader({
totalProperty: 'total',
successProperty: 'success',
idProperty: 'id',
root: 'data',
messageProperty: 'message'// <-- New "messageProperty" meta-data
},
Contact);
[/code]

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

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
// 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.
});
[/code]

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!):

[code lang="html" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<!-- 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>
[/code]

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

[code lang="js" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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();
}
}]
});
[/code]

Código Java

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

Controller:

[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}

}
[/code]

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:

[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]

Classe Contato - POJO:

[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]

Classe DAO:

[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]

Classe Util:

[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
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;
}
}
[/code]

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!