JSON serialisointi: Pois Circular Reference – manalasta

En tiedä onko tuttu tilanne, mutta itselleni harmillisen usein tavattu. Otetaan oliorakenne, esim. Order -> OrderItem, eli tilaus ja tilausrivejä. Rakenne menisi näin:

class Order {

  List<OrderItem> orderItems;

}

class OrderItem {}

Tähän asti kaikki loistavasti. Nyt kuitenkin on useita syitä miksi haluaisimme myös linkin OrderItemistä Order-luokkaan, esim. jos item-riveissä on assosiaatioita muuallekin ja niistä pitäisi näppärästi päästä header-tietoihin kiinni. Tai jos käyttää serialisointiin JPA-tekniikkaa eikä halua tehdä turhia välitauluja (One-to-Many assosiaatiossa tieto assosiaatiosta on many-päässä eli OrderItem luokassa)

Pysyitkö mukana? Hyvä, muutamme siis rakenteen tällaiseksi:

class Order {

  long id;
  List<OrderItem> orderItems;
}

class OrderItem {
  long id;
  Order order;
}

Ja tästä päästääkin syklisten referenssien helvettiin. Tämä on oliorakenteena ihan kelvollinen ja mahdollistaa juuri edellämainitun navigoinnin molempiin suuntiin (bidirectional one-to-many association). Tähän voisi iloisesti läpsäyttää JPA annotaatiot ja antaa sen valua kantaan ja kannasta triviaalilla koodilla.

Ongelmia tulee siinä vaiheessa kun haluttaisiin serialisoida tätä rakennetta johonkin hierarkiseen puurakenteeseen, esim. XML tai JSON. Ongelma johtuu siitä että dynaaminen sarjallistaja, esim. JAXB tai Jackson, käy läpi olion ominaisuudet yksi kerrallaan, ja kutsuu gettereitä, kerää tiedot, ja muuttaa ne tekstimuotoiseksi siirtokelpoiseksi dataksi. Siinä käy siis näin:

  1. Tallennetaan order, hienoa. Order on oliorakenne, joka sisältää orderItems listan, käydään se läpi
  2. Käsitellään jokainen orderItem vuorollaan. OrderItem on oliorakenne, joka sisältää viittauksen Order olioon
  3. Käsitellään jokainen viitattu Order vuorollaan. Order on oliorakenne joka sisältää OrderItems listan

Ja niin edelleen. Ikiliikkuja on keksitty. Tästähän saa palkakseen yleensä jonkun hienon kaatumisen ja cyclic/circlar reference errorin. Tai jos hauskasti käy, kone puuskuttaa hetken ja antaa stack overflow errorin tai out of memory errorin.

Mitä sitten on tehtävissä? Tämä artikkeli koskee JSON vaihtoehtoa, jos olet vielä XML parissa, olet pysyvästi helvetissä vailla poispääsyä, pahoittelen.

Jos käytät tätä esim. Jacksonin puitteissa, vaikkapa REST-rajapinnassa, ratkaisutapoja on muutama (tosiasiassa osa näistä sopii XML hommiinkin, jos edellisestä kohdasta tuli paha mieli):

  1. Katkaise syklinen referenssiketju merkkaamalla jommassakummassa päässä referenssi ei-serialisoitavaksi. Tapoja tähän on monia, Jackson taitaa tukea esim. Javan transient avainsanaa, @JsonIgnore annotaatiota, ja luokkatasolla voi myös listata ohitettavat kentät @JsonIgnoreProperties-annotaatiolla
  2. On myös mahdollista merkitä master-dependant suhde Jackson annotaatioilla @JsonManagedReference ja @JsonBackReference
  3. Tehdään aina value/transfer object johon normalisoidaan kulloinkin tarvittavat tiedot
  4. Myös voi merkitä identity-kentät Jackson annotaatioilla, @JsonIdentityInfo kertoo mikä kenttä on uniikki avain, jonka jälkeen serialisoinnissa voidaan viitata vain id arvoon, ei käydä läpi koko sisältöä.
  5. @JsonView annotaation käyttö näkymien muodostamiseen

Kahdessa ensimmäisessä kohdassa on yksi ongelma: Ne eivät salli talsimista edestakaisin, vaan vain yhteen suuntaan. Mutta ne ratkaisevat syklisen referenssipulman katkaisemalla rekursioketjun, eli sopivat moneen tilanteeseen. Kolmas kohta sisältää potentiaalisesti hurjan paljon virhealtista käsityötä ja myöhemmin ylläptoa ja en ole ollut koskaan kummankaan suuri fani. Neljäs kohta generoi kauheaa huttua serialisoinnista, ja en ole vielä löytänyt sille hyötykäyttöä. Neljäs kohta on näistä oma suosikkini. Se voisi olla vielä parempikin mutta sillä ainakin pääsee alkuun. Ja uusin Spring, Spring Boot, ja JAX-RS yhdistelmä tukee näitä ihanasti.

Homma toimi näin: Merkataan @JsonView annotaatiolla ne kentät, joita halutaan ehdollisesti serialisoida tai olla serialisoitamatta. Parametrina tulee tyypin nimi, joka on yleensä Java rajapinta. Esim. näin:

class View {

interface GimmeOrderRows {}

interface GimmeOrderHeader {}

}

Nyt voidaan muokata aiempia koodeja näin:

class Order {
  
  long id;
  
  @JsonView(View.GimmeOrderRows.class)
  List<OrderItem> orderItems;

}

class OrderItem {
  long id;

  @JsonView(View.GimmeOrderHeader.class)
  Order order;

}

Nyt pystyt hakemaan assosiaatiot on-demand periaatteisesti, eli voit navigoida kummasta päästä vaan. Jos et anna JsonView-annotaatiota, oletuksena saat kaiken. Heti jos annat yhdenkin @JsonView annotaation, saat kaikki kentät joihin se täsmää tai joita ei ole millään JsonView annotaatiolla varustettu. Eli jos meillä olisi tämän näköinen jax-rs palvelu…

@GET
@Path("order")
@JsonView(View.GimmeOrderRows.class)
public Order fetchOrderWithItems(long id) {
  return orderRepository.getOne(id);
}

… niin syklisen referenssin peikko pysyisi piilossa. Koska Jackson serialisoisi Orderin, ja sen sisältämät OrderItemit id-arvoineen, mutta ei seuraisi enää polkua niiden sisältämiin Order-instansseihin.

Vastaavasti nyt voisi huoletta hakea vaikkapa yhden OrderItem instanssin OrderHeadereineen ilman syklisiä referenssejä:

@GET
@Path("orderitem")
@JsonView(View.GimmeOrderHeader.class)
public OrderItem fetchOrderItemWithOrder(long id) {
  return orderItemRepository.getOne(id);
}

Samalla tekniikalla voi noutaa ehdollisesti esim. salasanatietoja, tai binäärisisältöä, tai muuten vain pitkiä kenttiä. Yhdessä kohtaa voi olla vain yksi JsonView-parametri, mutta koska niillä voi olla perintähierarkioita jotka tunnistetaan, rajoitus ei ole paha. On myös mahdollista säätää sellainen oletus, että mitään kenttää ei palauteta elleivät jsonviewt täsmää – ei myöskään niitä joista annotaatio puuttuu kokonaan.

Nyt kun vielä saisi Javaan luontevan suorastaan sisäänrakennetun JSON rajapinnan….

Mainokset

Equals ja Hashcode JPA/Hibernate sovelluksissa

Olen tästä kirjoitellut joskus aiemminkin. Mutta kyseessä siis Java-onjektien equals, hashCode, ja toString metodien toteuttaminen oikein. Periaatteessa ihan perusjuttu, jo ensimmäisissä oppitunneissa opittu, mutta muuttuukin jännittävämmäksi kun otetaan persistentit tietovarastot mukaan. Relaatiotietokantojen kanssa voisi sanoa että suurin osa on toteutettu väärin.

Mitä tällä on väliä? Nämä metodit (no ei sentään toString) astuvat peliin kun olioita puljataan kokoelmissa, tai uusissa Java 8 streameissä. Esim. set ei salli duplikaatteja. Se tarkistaa duplikaatit ja joissain toteutuksissa myös asemoi ’ämpärit’ hashCode ja equals metodien avulla. Jos ne on toteutettu väärin, setit eivät toimi oikein. Sama juttu jos yrität hakea listan metodeilla löytyykö sieltä tietty olio.

Miten tämä voidaan toteuttaa? No tapoja on muutama.

– Jätä toteuttamatta, tässä mallissa pelataan Object luokan oletustoteutuksilla, jotka katsovat equals ja hashCode sisäisen pointterin mukaan, eli pelaavat instanssin perusteella. Näin jos on vaikkapa sama tietokannan rivi ladattu kahteen eri instanssiin, ne ovat eri.

– Toteuta primary keyn perusteella, eli jos id on sama, objektit ovat samat, samoin hashCode lasketaan id:stä. Tässä mallissa heikkoutena on jos on kaksi objektia joiden id on null. Mekanismin mielestä ne olisivat samat. Tämä tulee esiin jos yrität esim. tunkea useampaa instanssia settiin.

– Toteuta business keyn perusteella, eli datasta löytyvän luonnollisen avaimen, esim. sosiaaliturvatunnus, etc. Tämän heikkous on että monella datalla voi olla vaikeaa löytää yksilöivää tietoa ilman kannassa generoitua primary key id:tä.

Lopulta ei ole oikein tai väärin, kunhan dokumentoituja pelisääntöjä noudatetaan (Object-luokan API dokumentaatio) – tärkeintä on tiedostaa mitä valinnasta seuraa koodille. Oletus eli ei equals/hashcodea ollenkaan on huonoin vaihtoehto kannan kanssa pelaavalle koodille. Sitä huonompi on ainoastaan rikkinäinen oma toteutus.

toString metodissa on vähemmän sudenkuoppia, mutta yhden kanssa on oltava tarkkana. Kun metodi tulostaa jäseniensä arvoja, ja jäsenet ovat custom olioita – syntyy helposti nullpointer exceptioneitä tai syklisiä referenssejä jotka johtavat stack overflow exceptioniin (ironista ;). Näissä olisi siis viisainta olla menemättä objektien toString metodeita pitkin navigoimaan, ja tyytyä tulostamaan vain jotain niiden sisältä, esim. id, ja sekin vain kun null arvot on tarkistettu.

Stackoverflow foorumin ihastuttavissa keskusteluissa oli linkki tästä tehtyyn hyvään taulukkoon:

To primary key or not to primary key?

No, tähän on myös toinen hyvä vaihtoehto. Tiedostetaan tämän metodin toiminnan puutteellisuus ja ei nojata siihen liikaa. Onko kaksi oliota sama – sehän riippuu tilanteesta ja tarpeista, eli on enemmänkin dynaaminen algoritmi kuin staattinen totuus. Streamien filter-operaatiot ovat oivallisia korvikkeita tälle. Mutta ellei parempaa pysty keksimään, se perinteinen id kenttä on ihan ok kunhan vain itse pitää rajoitukset mielessään.

Lisäksi pienenä lisä-sudenkuoppana: huomaa että joissain JPA-toteutuksissa liittyy automaagista weavingiä jota suoritetaan ajonaikana. Käytännössä siis kun kutsut get/set metodeita, niihin punotaan automaattisesti tietokantakutsuja mukaan. Näin ollen on eroa viittaatko muuttujaan suoraan, vai get metodi kautta. Lähtökohtana, jos get/set metodit on toteutettu oikein (pelkkiä gettereitä/settereitä, ei sivuvaikutuksia), on parempi mennä metodien läpi kun JPA on pelissä mukana.

Ja täällä erinomaista keskustelua aiheesta. Ei ole helppo kysymys, eikä ole yhtä autoritatiivista vastausta:

http://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma

Ei ole isoisäsi Spring myöskään enää!

Jep, Spring Framework on tosiaan uudistunut rajusti. Ajattelin jatkaa sepustusta siitä mitä Spring Boot projektilla voi tehdä. Tein jo aiemmin perus web projektin jossa on RESTful web service. Tällä kertaa sukelletaan Spring data pakettiin.

Lisää projektiisi riippuvuudet:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>org.hsqldb</groupId>
  <artifactId>hsqldb</artifactId>
  <scope>runtime</scope>
</dependency>

Jep, tässä tuli siis Hibernate, Spring Data JPA, ja Spring ORMS paketit mukana. Ja testikannaksi embedded hsqldb – voi tietysti olla myös derby, tai ihan tuotantokantoja tms. Mutta jostan hyvä aloitella.

Lisätään Entity Object. Spring ei vaadi persistence.xml tiedostoa, vaan oletuksena kaikki paketit pääkonfiguraatiotiedostos alla skannaillaan (Se jossa on @EnableAutoCofiguration) – huomaa tässä sudenkuoppa – jos pistät entityt esim. tuon paketin rinnalle tai yläpuolelle, niitä ei huomioida. Tästä koodista on jätetty paketti pois:

 

@Entity
public class City implements Serializable {

  @Id
  @GeneratedValue
  private Long id;

  @Column(nullable = false)
  private String name;

  @Column(nullable = false)
  private String country;

  protected City() {
    // no-args constructor required by JPA spec
    // this one is protected since it shouldn't be used directly
  }
  public City(String name, String country) {
    this.name = name;
    this.country = country;
  }
  public String getName() {
    return this.name;
  }
  public String getCountry() {
    return this.country;
  }
}

Jep, tätä ei tarvitse rekisteröidä mihinkään. Jos siinä on @MappedSuperclass, @Entity, tai @Embeddable, se huomioidaan automaattisesti. Tätähän pitäisi pystyä vähän manipuloimaan, tallettelemaan kantaan jne. Tehdään rajapinta joka perii CrudRepository kantaluokan:

public interface CityRepository extends CrudRepository<City, Long> {
}

Jep, tosiaan rajapinta. Spring taikoo sen sisään crud metodit automaattisesti. Näin ollen muutetaan meidän Example-luokan REST service esim. tällaiseksi:

@RestController
public class Example {
  @Autowired CityRepository cityRepo;
  @RequestMapping("/")
  String home() {
    City c = new City("Helsinki","Finland");
    cityRepo.save(c);
    return "Hello World2! " + cityRepo.findAll();
  }
}

 

Tuossa kutsutaan cityRepo-luokan (dynaamisesti generoitu rajapinnan pohjalta) save() ja findAll() funktioita jotka ovat automaattisesti olemassa.

No niin, nyt pitäisi vielä luoda tietokanta, datasource, tunnukset, salasanat, sun muut. Paitsi että ei. Yllätyitkö? Spring Boot olettaa että näihin muistinvaraisiin temp kantoihin ei tarvitse luoda datasourceja, tai määritellä sen enempiä tietoja. Riittää että määrittelit riippuvuuden aiemmin. Toisin sanoen voit heti testata sovellusta kunhan olet pudottanut sen sopivaan tomcat serveriin. REST palvelun pitäisi nyt joka kerralla luoda uusi City-olio ja listata se ruudulla.

Aika ketterää. Paljon olisi taas mitä voisi nyt säätää kun alkuun päästiin:

Spring Data antaa mahdollisuuden määritellä lisää kyselyitä suoraan rajapintaan, näin:

public interface CityRepository extends Repository<City, Long> {

    Page<City> findAll(Pageable pageable);

    City findByNameAndCountryAllIgnoringCase(String name, String country);

}

Kun haluat siirtyä testikannasta pysyvämpään testikantaan tai tuotantoon, voit määritellä sen datasource-määritykset application.properties tiedostossa, esim. projektin juuressa:

spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=dbuser
spring.datasource.password=dbpass
spring.datasource.driverClassName=com.mysql.jdbc.Driver

Muista toki myös tuoda se dependencynä pom.xml tiedostossa – jotta saat ajuritkin mukaan.

Kanta luodaan automaattisesti vain H2, HSQL, ja Derby kannalle. Jos haluat sen muillekin, aseta em application.properties tiedostossa ominaisuus päälle:

spring.jpa.hibernate.ddl-auto=create-drop

Muutenkin voit säätää näitä jpa-määrityksiä samaan tapaan:

spring.jpa.database: H2
spring.jpa.show-sql: true

Lisäksi jos pistät luokkapolun juureen tiedoston nimeltä import.sql, sen sisältämät lauseet suoritetaan sovelluksen käynnistyessä – loistava tapa ladata init/testidata sisään. Sen ohella Spring imaisee tiedostot schema.sql, data.sql, ja esim. schema-${platform}.sql ja data-${platform}.sql, ottaen arvonsa spring.datasource.platform muuttujasta.

Äläpä käytä Hibernate connection poolia tai cachea!

Jepp, hibernatea kun pyörii vielä sellaisenaan tuotannossa reippaasti ja JPA:n mukana kylkiäisenä tullutta hibernatea lisääntyvissä määrin, hyvä muistutella että hibernaten oma dokumentaatiokin kehottaa olemaan käyttämättä hibernaten mukana toimitettavia cache ja connection pool kilkkeitä. Syynä se että ne ovat suorituskyvyltään, ominaisuuksiltaan ja toimintavarmuudeltaan lähinnä opiskeluun, harjoittelunn, prototypointiin käytettäviä helppoja defaultteja, mutta eivät tuotantoympäristöön riittäviä.

Mitäpä sitten pitäisi käyttää? Hyvämaineisia ovat esim. apache commons dbcp, ja c3p0 joiden piirteistä väännetään parrat päristen nettifoorumeilla. Uusi tulokas on Tomcat dbcp, joka tuli tomcat 7:n mukana, se on vielä toistaiseksi vähemmän tunnettu.

http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html

Cachejen osalta ehcache löytyy usein tuotannosta, ja kun cachella on dramaattinen vaikutus suorituskykyyn, sen käyttöönotto kannattaakin tuunailla rauhassa toimivaksi testi edellä.

http://www.mchange.com/projects/c3p0/#hibernate-specific

http://commons.apache.org/dbcp/

Eli väärän cache/connection pool mallin käyttämisestä voi tulla mm. seuraavia seurauksia:

– muistivuotoja (no javassahan ei niitä ole mutta jos objektiin jättää viittauksen muistia ei myöskään  vapaudu, eli..)

– cacheen jää ikivanhaa dataa jota ei virkistetä

– pooliin jää rikkinäisiä yhteyksiä ja lopulta pool lakkaa toimimasta

– säätöjen puutteen vuoksi optimointivaraa ei ole, muistia käytetään liikaa tai ei tarpeeksi

 

P.S. ehcachessa on mielenkiintoinen lisäfeature joka on oletuksena päällä: Se soittelee kotipuoleen ja lähettää koneesta aikalailla tietoja. Meilläpäin tämmöisiä softia kutsuttiin haittaohjelmiksi tai viruksiksi, nykyään ne ovat laajalti käytettyjä apukirjastoja 😉 Reilua? Kenties, kenties ei, you make the call. No hätä ei kuitenkaan ole tämännäköinen, sen saa helposti pois päältä – ja kannattaapi ottaakin tuotannossa:

http://martijndashorst.com/blog/2011/02/21/ehcache-and-quartz-phone-home-during-startup/

Yksi syy lisää lukea manuaalit.

 

JPQL ja SQL lauseet ulos JPA Criteria kyselyistä

Jahas, yksi mielenkiintoinen kysymys mihin on koulutuksissa ja projekteissa törmännyt on, miten JPA Criteria kyselyitä saisi jotenkin paremmin ymmärrettyä/debugattua. Nehän voivat olla aika hirveätä luettavaa ja ymmärrettävää ja vaikeaa hahmottaa miten ne muuttuvat SQL lauseiksi kannassa. Toki voi laittaa show_sql asetuksen päälle mutta muukin näkyvyys auttaisi.

Törmäsin artikkeliin jossa tätä ihmeteltiin. Näyttäisi pähkinänkuoressa siltä että nykyään pitää tehdä joka toteutukselle eri tavalla – no niinpä tietysti! Mutta artikkelin kirjoittaja heitti pallon JPA 2.1 kehittäjille joten tiedä vaikka tähänkin standardoituisi vielä joskus siirrettävä rajapinta esim. Java EE 7 releaseen. Sellaista odotellessa ,tässä on koodia eri toteutuksille:

EclipseLink:

findAllBooks.unwrap(JpaQuery.class).getDatabaseQuery().getJPQLString(); // doesn’t work for CriteriaQuery
findAllBooks.unwrap(JpaQuery.class).getDatabaseQuery().getSQLString();

Hibernate:

findAllBooks.unwrap(org.hibernate.Query.class).getQueryString()

OpenJPA:

findAllBooks.unwrap(org.apache.openjpa.persistence.QueryImpl.class).getQueryString()

Pääideana on siis käyttää CriteriaQueryn (findAllBooks) unwrap-metodia, valitettavasti Query toteutus on erilainen eri toteutuksille, tässä taulukko eroista:

ORM Framework Query implementation How to get the JPQL String How to get the SPQL String
EclipseLink JpaQuery getDatabaseQuery().getJPQLString()* getDatabaseQuery().getSQLString()**
Hibernate Query N/A getQueryString()
OpenJPA QueryImpl getQueryString() N/A

 

Enpä testaillut näitä vielä mutta pistän muistiin vastaisen varalle. Criteria API sopii erityisen hyvin dynaamisiin kyselyihin joiden rakenne elää parametrien perusteella.

Ja tarinan lähde: http://agoncal.wordpress.com/2012/05/24/how-to-get-the-jpqlsql-string-from-a-criteriaquery-in-jpa/

 

Google App Engine pilvipalvelu ja Java EE 6, osa 3

No niin, huomasin etten ole mennyt paljonkaan yksityiskohtiin näissä pilvipalveluissa, eli ihan omaksi muistintueksikin, tässä yksityiskohtia. Varoitus, tästä tulee rumaa, kuten aina kun yksityiskohtiin mennään.

Toimiva Maven pom jossa on mukana JPA 1 ja JSF 2, on tässä:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.tieturi</groupId>
  <artifactId>PilvenVeikko</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>PilvenVeikko Java EE 6 Webapp</name>
  <url>http://maven.apache.org</url>
  <repositories>
    <repository>
      <id>java.net2</id>
      <name>Repository hosting the jee6 artifacts</name>
      <url>http://download.java.net/maven/2</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>maven-gae-plugin-repo</id>
      <name>maven-gae-plugin repository</name>
      <url>http://maven-gae-plugin.googlecode.com/svn/repository</url>
    </pluginRepository>
  </pluginRepositories>
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-api</artifactId>
      <version>2.0.2</version>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-impl</artifactId>
      <version>2.0.2</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>el-impl</artifactId>
      <version>2.2</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-jpa</artifactId>
      <version>1.1.5</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>persistence-api</artifactId>
      <version>1.0</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-core</artifactId>
      <version>1.1.5</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>javax.jdo</groupId>
      <artifactId>jdo2-api</artifactId>
      <version>2.3-eb</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-enhancer</artifactId>
      <version>1.1.4</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-rdbms</artifactId>
      <version>1.1.5</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google</groupId>
      <artifactId>appengine-local-runtime-shared</artifactId>
      <version>1.2.0</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-appengine</artifactId>
      <version>1.0.7.final</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google</groupId>
      <artifactId>appengine-sdk-api</artifactId>
      <version>1.3.8</version>
      <type>jar</type>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
           <source>1.6</source>
          <target>1.6</target>
       </configuration>
    </plugin>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.1-beta-1</version>
        <configuration>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
    </plugin>
    <plugin>
        <groupId>net.kindleit</groupId>
        <artifactId>maven-gae-plugin</artifactId>
        <version>0.6.0</version>
       <configuration>
          <sdkDir>D:/java/appengine-java-sdk-1.3.8</sdkDir>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.datanucleus</groupId>
        <artifactId>maven-datanucleus-plugin</artifactId>
        <version>1.1.4</version>
        <configuration>
          <mappingincludes>**/domain/*.class</mappingincludes>
          <verbose>true</verbose>
          <enhancername>ASM</enhancername>
          <api>JPA</api>
        </configuration>
        <executions>
          <execution>
            <phase>compile</phase>
            <goals>
              <goal>enhance</goal>
            </goals>
          </execution>
      </executions>
      <dependencies>
        <dependency>
          <groupId>org.datanucleus</groupId>
          <artifactId>datanucleus-core</artifactId>
          <version>1.1.5</version>
          <exclusions>
           <exclusion>
              <groupId>javax.transaction</groupId>
              <artifactId>transaction-api</artifactId>
            </exclusion>
          </exclusions>
        </dependency>
        <dependency>
          <groupId>org.datanucleus</groupId>
          <artifactId>datanucleus-rdbms</artifactId>
          <version>1.1.5</version>
        </dependency>
        <dependency>
          <groupId>org.datanucleus</groupId>
          <artifactId>datanucleus-enhancer</artifactId>
          <version>1.1.4</version>
        </dependency>
      </dependencies>
      </plugin>
    </plugins>
    <finalName>PilvenVeikko</finalName>
  </build>
  <profiles>
    <profile>
      <id>netbeans-private</id>
      <activation>
        <property>
          <name>netbeans.execution</name>
          <value>true</value>
        </property>
      </activation>
      <properties>
        <netbeans.deployment.server.id>[C:\Program Files (x86)\glassfish-3.0.1\glassfish]deployer:gfv3ee6:localhost:4848</netbeans.deployment.server.id>
      </properties>
    </profile>
  </profiles>
  <properties>
    <netbeans.hint.deploy.server>gfv3ee6</netbeans.hint.deploy.server>
  </properties>
</project>

Julkaisin tuon kokonaisena copy-pastea ajatellen. Pieni mutta tärkeä kohta on lopussa oleva DataNucleus enhancer plugin, koska DataNucleus käyttää vielä vanhanaikaista bytecode vahvistusta, tuo vaihe tehdään käännöksen jälkeen, tärkeää tarkistaa että pakettirakenne-parametri on oikein. Vierastan lisä ant scriptien käyttöä joten yritin tehdä kaiken Mavenillä ja näyttäisi toimivan.

Toinen jippo oli imaista gae kirjastot paikalliseen maven repositoryyn etukäteen, koska tietääkseni niitä ei globaaleista repositoryistä löydy. Kirjastot mitä näemmä tarvittiin olivat datanucleus-jpa, datanucleus-core, jdo2-api, datanucleus-enhancer, datanucleus-rdbms, appengine-local-runtime-shared, datanucleus-appengine, sekä appengine-sdk-api. Lisäksi javan core kirjastoista tarvitsin persistence-api ja servlet-api ja jsp-api pakettien lisäksi jsf-api ja jsf-impl kirjastot, versio 2, sekä el-impl. Huom! JSF-impl on google app enginelle tuunattu versio, johtuen parista rajoituksesta sen suhteen. Tässä pari linkkiä siihen:

Take a look at this site
http://javadocs.wordpress.com/2009/10/17/mojarra-jsf-2-0-rc2-and-google-app-engine-sdk-1-2-6/

and download this file
http://code.google.com/p/joshjcarrier/source/browse/trunk/Sun%20JSF%20GAE/jsf-impl-gae.jar

Rakensin tällaisen scriptin joka vie ne repositoryyn:

call mvn install:install-file -Dfile=lib/appengine-tools-api.jar -DgroupId=com.google -DartifactId=appengine-tools -Dversion=1.2.0 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/shared/appengine-local-runtime-shared.jar -DgroupId=com.google -DartifactId=appengine-local-runtime-shared -Dversion=1.2.0 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/appengine-api-1.0-sdk-1.3.8.jar -DgroupId=com.google -DartifactId=appengine-sdk-api -Dversion=1.3.8 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/orm/datanucleus-appengine-1.0.7.final.jar -DgroupId=org.datanucleus -DartifactId=datanucleus-appengine -Dversion=1.0.7.final -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/orm/datanucleus-core-1.1.5.jar -DgroupId=org.datanucleus -DartifactId=datanucleus-core -Dversion=1.1.5 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/orm/datanucleus-jpa-1.1.5.jar -DgroupId=org.datanucleus -DartifactId=datanucleus-jpa -Dversion=1.1.5 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/orm/jdo2-api-2.3-eb.jar -DgroupId=org.datanucleus -DartifactId=jdo2-api -Dversion=2.3 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/orm/geronimo-jpa_3.0_spec-1.1.1.jar -DgroupId=org.geronimo -DartifactId=geronimo.jpa -Dversion=1.1.1 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=lib/user/orm/geronimo-jta_1.1_spec-1.1.1.jar -DgroupId=org.geronimo -DartifactId=geronimo.jpa -Dversion=1.1.5 -Dpackaging=jar -DgeneratePom=true
call mvn install:install-file -Dfile=jsf-impl-gae.jar -DgroupId=com.sun.faces -DartifactId=jsf-impl -Dversion=2.0.2 -Dpackaging=jar -DgeneratePom=true

No niin , ehkäpä siinä oli taas tarpeettoman paljon tietoa tällä kertaa 😉 Joka tapauksessa, itselleni muistiin mitä askelia tarvittiin pilveen pääsemiseen.

Google App Engine pilvipalvelu ja Java EE 6, osa 2

Jahas, seuraava episodi pilvipalveluiden opiskelussa. Virittelin Maven 3 projektin siten että sen ohella että pilvipalveluiden Hello World onnistuu, myös JSF 2.0 ja JPA on tuettuna. Näin on mahdollista tehdä Solakka Java-periaatteiden mukaan hyvinkin nopeasti ja helposti yksikkötestattavaa sovelluskoodia. Samalla kävi ilmi myös Google App Engine JPA:n rajoitukset:

– JPA 1.0 on tuettuna, JPA 2.0 ei ole (ainakaan nyt). Erona on lähinnä piirteet joita en muutenkaan käyttäisi, Criteria Query on yksi niistä.
– Alla oleva DataNucleus engine EI ole relaatiotietokanta, vaan se on NoSQL kanta, joten kaikki JPA piirteet eivät toimi ollenkaan, esim.
– Many-to-Many ei toimi,
– Join kyselyt eivät toimi (lapsi-entiteetin kenttiä ei voi käyttää hakuehtoina),
– group by, having, sum, avg, max, min ei toimi
– polymorfiset kyselyt eivät toimi

Aika tyly lista rajoituksia, siis. Kuitenkin.. Kaikki nuo piirteet tulevat relaatiokantateorian puolelta, ja tässä meillä on uuden ajan kanta jota käytetään uudella tapaa. Aika näyttää riittääkö siinä rahkeet, mutta hieman pitää suunnittelua miettiä tarkemmin.

Aluksi oli hieman ongelmia sen kanssa että mitä kirjastoja tarvitaan, ja puuttuvat kirjastot antoivat kryptisiä virheilmoituksia. Hyvä myös huomata että  DataNucleus käyttää bytekoodin vahvistamista, eli tarvitaan post-processor plugin maveniin kääntäjän lisäksi. Lisäksi CDI:n ja injektioiden sijaan pelasin toistaiseksi varman päälle ja en käyttänyt niitä, vaan EntityManagerFactory saadaan singletonilta, ja annotaatioissa käytin JSF:n omia ManagedBean jne annotaatioita, enkä CDI:n Named ja Inject annotaatioita. Jatkossa tarkoitus muokata ympäristöstä parempaa, mutta nyt se näyttäisi toimivan kivasti, ja App Enginestä löytyy jopa työkalut joilla voi tarkastella logeja ja tietokannan sisältöä.

Lueksin että Spring framework toimii ongelmattomammin App Enginessä, ja JDO on JPA:ta luontevampi rajapinta tässä tapauksessa, kun kanta ei olekaan relaatiokanta. Mutta itselleni tärkeämpää on standardoidut rajapinnat ja ydinstandardit ohi kolmannen osapuolen frameworkien, eli katsotaan miten tässä käy.