piątek, 29 czerwca 2012

JavaEE materiały

Mam chciwy plan ogarnąć (cały) internet z materiałami dla programistów, głównie JavyEE, ale wszystkie wspólne mianowniki w programowaniu, będą równie mile widziane. Po co to robić skoro jest google? Ano chociażby po to, że googla trzeba wiedzieć o co spytać, a ja czasami mam po prostu ochotę przeczytać/obejrzeć coś nowego... Po drugie często coś ciekawego znajdę na vimeo, a potem totalnie nie mam pojęcia jak to odnaleźć.

Proponuje następujący układ, za każdy link dzięki, któremu dowiedziałeś/aś się czegoś nowego/przydatnego, proszę o jakiś nowy link, żeby powstał samo napędzający się zbiór materiałów. Myślę, że nie muszę dodawać, że zależy mi na linkach, które jednak coś za sobą niosą. Docelowo chciałbym, aby powstał przesiany kanon materiałów dla programisty, do którego można w każdej chwili wrócić, czy też zweryfikować, że już się dane źródło przetrawiło.

Blogi: 

Video:

Craftsmanship:
DDD:
TDD:

Agile:

Spring:

Java:

Inne:

Książki:

niedziela, 24 czerwca 2012

Mapowanie opakowywacza kolekcji

Opakowywacze kolekcji, które zostały opisane przez Koziołka, uważam za bardzo dobre i przydatne podejście/wzorzec. Jednak w pewnym momencie natrafiłem na problem podczas mapowania takich tworów. Mamy w systemie encje BankAccount, wielu różnych aktorów (Client, Manager, itd) może mieć zbiór BankAccount. Warto opakować taki zbiór komponentem BankAccounts, który miałby wspólne metody dla wszystkich aktorów, takie jak np. znajdź konto z zadanego banku, itd.

Wyglądałyby to mniej więcej tak:
@Embeddable
public class BankAccounts {

    @OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
    private Set<BankAccount> bankAccounts = new HashSet<>();
    //...
}
I w jednym z aktorów:
@Entity
public class Client{
    //...

    @Embedded
    @AssociationOverride(name = "bankAccounts", 
        joinColumns = @JoinColumn(name = "client_id"))
    private BankAccounts bankAccounts = new BankAccounts();
    //...
}
I tu powstaje problem, w postaci wyjątku:
javax.persistence.PersistenceException: [PersistenceUnit: testPU] Unable to configure EntityManagerFactory
    at org.hibernate.ejb.Ejb3Configuration.configure(Ejb3Configuration.java:378)
    at org.hibernate.ejb.HibernatePersistence.createEntityManagerFactory(HibernatePersistence.java:56)
    at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:63)
    at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:47)
    at pl.cqbroker.utils.DaoTest.setUp(DaoTest.java:39)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
    at ...
Caused by: org.hibernate.AnnotationException: Illegal attempt to define a @JoinColumn with a mappedBy association: bankAccounts.bankAccounts
    at org.hibernate.cfg.Ejb3JoinColumn.buildJoinColumn(Ejb3JoinColumn.java:253)
    at org.hibernate.cfg.Ejb3JoinColumn.buildJoinColumnsWithDefaultColumnSuffix(Ejb3JoinColumn.java:227)
    at org.hibernate.cfg.Ejb3JoinColumn.buildJoinColumns(Ejb3JoinColumn.java:194)
    at org.hibernate.cfg.ColumnsBuilder.extractMetadata(ColumnsBuilder.java:124)
    at org.hibernate.cfg.AnnotationBinder.processElementAnnotations(AnnotationBinder.java:1495)
    at org.hibernate.cfg.AnnotationBinder.fillComponent(AnnotationBinder.java:2433)
    at org.hibernate.cfg.AnnotationBinder.fillComponent(AnnotationBinder.java:2336)
    at org.hibernate.cfg.AnnotationBinder.bindComponent(AnnotationBinder.java:2285)
    at org.hibernate.cfg.AnnotationBinder.processElementAnnotations(AnnotationBinder.java:2021)
    at org.hibernate.cfg.AnnotationBinder.processIdPropertiesIfNotAlready(AnnotationBinder.java:796)
    at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:707)
    at org.hibernate.cfg.Configuration$MetadataSourceQueue.processAnnotatedClassesQueue(Configuration.java:4035)
    at org.hibernate.cfg.Configuration$MetadataSourceQueue.processMetadata(Configuration.java:3989)
    at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1398)
    at org.hibernate.cfg.Configuration.buildMappings(Configuration.java:1375)
    at org.hibernate.ejb.Ejb3Configuration.buildMappings(Ejb3Configuration.java:1519)
    at org.hibernate.ejb.EventListenerConfigurator.configure(EventListenerConfigurator.java:193)
    at org.hibernate.ejb.Ejb3Configuration.configure(Ejb3Configuration.java:1100)
    at org.hibernate.ejb.Ejb3Configuration.configure(Ejb3Configuration.java:282)
    at org.hibernate.ejb.Ejb3Configuration.configure(Ejb3Configuration.java:366)
    ... 28 more
Rozwiązanie jest w generalnie proste, ale trochę czasu mi zajęło zanim zrozumiałem co jest nie tak, więc gdyby ktoś miał podobny problem, wystarczy dodać adnotację @JoinColumn(). Może być wypełniona jakimiś parametrami, bądź nie - nie ma to znaczenia bo i tak zostanie nadpisana przez Clienta lub inną encję.
@Embeddable
public class BankAccounts {

    @OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
    @JoinColumn
    private Set<BankAccount> bankAccounts = new HashSet<>();
    //...
}

wtorek, 19 czerwca 2012

Value Object a prawidłowa implementacja


Z racji wszechobecnej mody na DDD, obiekty typu Value Object (VO) powstają jak grzyby po deszczu. Zaimplementowanie takiego obiektu wydaje się być trywialne. Jednak bazując na własnych doświadczeniach widziałem/zrobiłem wiele różnej jakości VO i dopiero teraz chyba mam pełny pogląd w tej sprawie. Przykładem niech będzie VO Email, służący do opakowania stringowej reprezentacji adresu email. Implementacja będzie bazowała na Hibernate, ale zaprezentowane rozwiazania są wspólne do większości frameworków od persystencji.
@ValueObject
@Access(AccessType.FIELD)
public class Email implements Serializable{
 private static final long serialVersionUID = -6632187560263057672L;
 @Basic
 private String value;


 /**
  * na potrzeby hibenrate prywatny konstruktor
  */
 @SuppressWarnings("unused")
 private Email() {}

 /**
  * @param email - poprawy adres email
  * @throws IllegalArgumentException jeśli adres email jest niepoprawny
  */
 public Email(String email) {
  if (!EmailValidator.getInstance().isValid(email)) {
   throw new IllegalArgumentException("podałeś nieprawidłowy format adresu email");
  }
  this.value = email;
 }

 @Override
 public boolean equals(Object obj) {
  if (obj == this)
   return true;
  if (!(obj instanceof Email))
   return true;
  Email email = (Email) obj;
  return new EqualsBuilder().append(value, email.value).isEquals();
 }

 @Override
 public int hashCode() {
  return new HashCodeBuilder().append(value).toHashCode();
 }

 @Override
 public String toString() {
  return value;
 }

 public String getValue() {
  return value;
 }
}
Na pierwszy rzut oka nic wielce okrywczego w tym kodzie nie ma. Mimo wszystko warto opisać kilka kluczowych aspektów. Po pierwsze warto zrobić sobie adnotację „marker” do oznaczania wszystkich VO. Łatwiej je wtedy chociażby wyszukiwać w projekcie.
/**
 * stereotyp do oznacznia klass o charakterze ValueObject, 
 *  - ich identyczność bazuje na nie na Id jak w przypadku encji, tylko na ich stanie
 *  - najczęśniej powinny być niemodyfikowalne
 *  - enkapsulują pola o podobnej semantyce,
 * @author aludwiko
 */
public @interface ValueObject {}
Kolejna adnotacja @Access służy do nadpisania po czym mapowane są pola. Niezależnie od tego czy klasa bazowa np. User, mapowana jest z wykorzystaniem getterów czy bezpośrednio pól, Email zawsze będzie mapowany bezpośrednio do pola. Dzięki temu nie musimy tworzyć settera i gettera, co poprawia nam enkapsulację. W moim przypadku getter został, ale jest on po prostu przydatny.

Z racji tego, że encje przeważnie implementują interfejs Serializable, od razu dodajemy go do VO, na pewno nic złego z tego nie wyniknie.

Konstruktor domyślny (wymagany przez Hibernata) został zaimplementowany jako prywatny, aby nikogo nie kusiło tworzenie „pustych” Emaili. Publiczny konstruktor gwarantuje nam, że nigdy utworzymy „błędnego” obiektu Email, co jest jedną z cech dobrego VO, tzn jego stan nie powinien być nieprawidłowy.

Taki konstruktor i wspomniany wcześniej brak settera powodują że Email spełnia kolejną ważna cechę VO, jest niemodyfikowalny. Jest to bardzo ważna cecha w kontekście VO. Equals i hashCode dopełniają minimalną implementację.

Ok, mamy VO, pytanie co dalej z nim zrobić. Jeśli chodzi o warstwę widoku, to należy stworzyć odpowiedni Converter w JSF, PropertyEditor w starym Springu, lub jakikolwiek inny mechanizm, który z wartości inputa utworzy nam obiekt Email. Z racji tego że nie mamy settera nie bindujemy na widoku wartości bezpośrednio do value
<h:inputtext id="email" value="#{user.email.value}">
tylko do samego email'a
<h:inputtext id="email" value="#{user.email}">
Przykładowa implementacji w springu:
public class EmailPropertyEditor extends PropertyEditorSupport {

 @Override
 public void setAsText(String text) throws IllegalArgumentException {
  if (StringUtils.isNotBlank(text)) {
   Email email = new Email(text);
   setValue(email);
  }
  else {
   setValue(null);
  }
 }

 @Override
 public String getAsText() {
  Email email = (Email) getValue();
  if (email != null) {
   return email.getValue();
  }
  else {
   return "";
  }
 }
}
W tym momencie mamy już obiekt z wypełnionym Email'em, teraz warto powiedzieć Hibernatowi jak ma zapisywać taki obiekt z Email'em do bazy. Są dwa główne podejścia, albo idziemy w kierunku @Embeddable i @Embedded albo Hibernate UserType.

Pierwsze podejście jest już proste i prawdopodobnie każdy miał z nim styczność. Chociaż np. dyskusja czy każdy obiekt @Embeddable jest automatycznie VO, nadal trwa i właściwie jestem po obu stronach w tym konflikcie.

Drugie podejście, choć trochę bardziej skomplikowane, daje ciekawy efekt w postaci możliwości tworzenia bardziej obiektowych zapytań, takich jak np:
Email email = new Email("[email protected]");
Criteria criteria = session.createCriteria(User.class).add(eq("email", email));
lub jako HQL
"from User WHERE email = :someEmail"
W tym przypadku musimy napisać własną implementację typu pola w klasie, tak aby hibernate wiedział jak zapisywać Emaila. W większości wypadków wystarczy zaimplementowanie interfejsu UserType. Jednak w tym przypadku chciałbym mieć możliwość tworzenia zapytań z wykorzystaniem LIKE:
Criteria criteria = session.createCriteria(User.class).add(ilike("email.value", "op.pl"));
Dostęp do pól VO w zapytaniach jest możliwy, ale musimy zaimplementować CompositeUserType, przykład implementacji:
/**
 * implementacja UserType'a dla VO [email protected] Email}
 * @author aludwiko
 */
public class EmailCompositeType implements CompositeUserType {

 /**
  * ORDER IS IMPORTANT! it must match the order the columns are defined in the property mapping
  */
 @Override
 public String[] getPropertyNames() {
  return new String[] { "value" };
 }
 @Override
 public Type[] getPropertyTypes() {
  return new Type[] { Hibernate.STRING };
 }
 @Override
 public Object getPropertyValue(Object component, int property) throws HibernateException {
  if (component == null) {
   return null;
  }
  Email email = (Email) component;
  switch (property) {
   case 0: {
    return email.getValue();
   }
   default: {
    throw new HibernateException("Invalid property index [" + property + "] for Email");
   }
  }
 }
 @Override
 public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
  throw new UnsupportedOperationException();
 }
 @Override
 public Class returnedClass() {
  return Email.class;
 }
 @Override
 public boolean equals(Object x, Object y) throws HibernateException {
  if (x == y) {
   return true;
  }
  if (x == null || y == null) {
   return false;
  }
  Email emailX = (Email) x;
  Email emailY = (Email) y;
  return emailX.equals(emailY);
 }
 @Override
 public int hashCode(Object x) throws HibernateException {
  return x.hashCode();
 }
 @Override
 public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
  String name = names[0];
  Object emailString = Hibernate.STRING.nullSafeGet(rs, name);
  if (emailString == null) {
   return null;
  }
  return new Email((String) emailString);
 }
 @Override
 public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
  if (value == null) {
   Hibernate.STRING.nullSafeSet(st, null, index);
  }
  else {
   Hibernate.STRING.nullSafeSet(st, ((Email) value).toString(), index);
  }
 }
 @Override
 public Object deepCopy(Object value) throws HibernateException {
  return value;
 }
 @Override
 public boolean isMutable() {
  return true;
 }
 @Override
 public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException {
  return (Serializable) value;
 }
 @Override
 public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException {
  return cached;
 }
 @Override
 public Object replace(Object original, Object target, SessionImplementor session, Object owner) throws HibernateException {
  return original;
 }
}
I samo wykorzystanie:
@Entity
public class User {
        //...
 @Column
 @Type(type = "package.EmailCompositeType")
 public Email getEmail() {
  return email;
 }
 public void setEmail(Email email) {
  this.email = email;
 }
        //...
}
Mam nadzieję, że temat został opisany kompleksowo i da się to przeczytać w skończonym czasie. Gdyby ktoś miał coś ciekawego do dodania, to zachęcam do komentowania.