wtorek, 16 października 2012

Zasięg @ViewScoped w CDI/Weld

Szybki przepis jak w CDI/Weld stworzyć nowy zasięg @ViewScoped, ponieważ niestety nie został on domyślnie uwzględniony w specyfikacji. Weld oferuje nam 4 domyślne zasięgi:
  • @RequestScoped,
  • @ConversationScoped,
  • @SessionScoped,
  • @ApplicationScoped.
Po przejrzeniu tej listy od razu nasuwa się pytanie - gdzie jest @ViewScoped? Co prawda pojawił się nowy zasięg @ConversationScoped, który znany jest wszystkim tym, którzy mieli styczność z Seam'em, ale to chyba jeszcze nie powód, aby usuwać @ViweScoped (w Seam'ie - @Scope(ScopeType.PAGE)). Niestety z jakiś względów tak się właśnie stało. Na szczęście specyfikacja CDI pozwala nam tworzyć nowe zasięgi, co skłoniło mnie do tego, aby trochę poszperać i stworzyć zapomniany zasięg @ViewScoped. Swoją drogą autorzy dokumentacji Weld nie pomagają w tego typu czynnościach.

Potrzebny kod
Potrzebujemy dodać następujące klasy do naszego projektu.
package com.blogspot.mkorwel.context;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.enterprise.context.NormalScope;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

@Target(value = { METHOD, TYPE, FIELD })
@Retention(value = RUNTIME)
@NormalScope
@Inherited
public @interface ViewScoped {

}
package com.blogspot.mkorwel.context.spi;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;

public class ViewContextExtension implements Extension {

 public void afterBeanDiscovery(@Observes AfterBeanDiscovery event,
   BeanManager manager) {
  event.addContext(new ViewContext());
 }
}
package com.blogspot.mkorwel.context.spi;

import java.util.Map;

import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.faces.context.FacesContext;

import com.blogspot.mkorwel.context.ViewScoped;

public class ViewContext implements Context {

 public Class<ViewScoped> getScope() {
  return ViewScoped.class;
 }

 public <T> T get(Contextual<T> contextual,
   CreationalContext<T> creationalContext) {
  Bean<T> bean = (Bean<T>) contextual;
  Map<String, Object> viewMap =FacesContext.getCurrentInstance()
    .getViewRoot().getViewMap(true);
  
  if (viewMap.containsKey(bean.getName())) {
   return (T) viewMap.get(bean.getName());
  } else {
   T t = bean.create(creationalContext);
   viewMap.put(bean.getName(), t);
   return t;
  }
 }

 public <T> T get(Contextual<T> contextual) {
  Bean<T> bean = (Bean<T>) contextual;
  Map<String, Object> viewMap = FacesContext.getCurrentInstance()
    .getViewRoot().getViewMap(true);
  
  if (viewMap.containsKey(bean.getName())) {
   return (T) viewMap.get(bean.getName());
  } else {
   return null;
  }
 }

 public boolean isActive() {
  return true;
 }
}
W META-INF tworzymy plik: services/javax.enterprise.inject.spi.Extension, w którym zadeklarujemy użycie naszego zasięgu
com.blogspot.mkorwel.context.spi.ViewContextExtension
Użycie
Nasza "Managed Bean" o zasięgu @ViewScoped może wyglądać mniej więcej tak:
@ViewScoped
public class SomeControler implements Serializable {

 //some field and method
}

niedziela, 14 października 2012

Hibernate - Problem z listami przy relacji ManyToMany

Hibernate mimo swoich wielu zalet, ma również wiele wad :). Jedną z nich jest sposób obsługi java.util.List przy próbie mapowania @ManyToMany. W sumie przy pierwszym kontakcie wszystko działa bez zarzutu, ale w momencie gdy chcemy dodać coś do listy, w której są już jakieś inne elementy, to okazuje się, że jednak nie wszystko działa w 100% dobrze. W naszej konsoli widzimy mniej więcej taki log:
Hibernate: insert into Item (name, id) values (?, ?)
Hibernate: delete from OrderTab_Item where OrderTab_id=?
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Czyli w momencie wstawienia jakiegoś wiersza do listy, Hibenrate usuwa wszystkie relacje a następnie z powrotem je dodaje. W moim przykładzie dodajemy 1 element do listy już 3 elementowej. W związku z tym Hibenrate postanowił usunąć wszystkie relacji a następnie po kolei dodał całkiem nowe 4 wiersze. Przykład encji oraz kodu który który dodaje do encji widzimy poniżej.
Encje w relacji many-to-many.
@Entity
@Table(name="OrderTab")
public class Order implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String name;

 @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
 private List items;

 // ... GET and SET
}
@Entity
public class Item implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String name;

 // ... GET and SET

}
Prosta metoda dodająca Item do Orderu.
public void addItem(long orderId, Item item) {

 Order o = em.find(Order.class, orderId);
 o.getItems().add(item);

 em.merge(o);

}
Rozwiązanie problemu
Wystarczy zmienić typ kolekcji w której przechowywujemy elementy z java.util.List na java.util.Set i wszystko będzie działać zgodnie z oczekiwaniami. Encja Order po zmianie wygląda tak:
@Entity
@Table(name="OrderTab")
public class Order implements Serializable {

 @Id
 @GeneratedValue
 private Long id;

 private String name;

 @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER)
 private Set items;

 // ... GET and SET
}
a log w konsoli tak:
Hibernate: insert into Item (name, id) values (?, ?)
Hibernate: insert into OrderTab_Item (OrderTab_id, items_id) values (?, ?)
Pamiętajcie tylko że jak korzystacie ze zbiorów zamiast list, to należy doimplementować metody equals i hashCode.