czwartek, 12 kwietnia 2007

Zend Framework - obsługa baz danych


Przyznam szczerze, że na Zend Framework trafiłem dzięki praktykom w IBM, gdzie przyszło mi testować hybrydowy silnik baz danych (pureXML w DB2 9). Postanowiłem pobawić się bowiem serwerem Zend Core for IBM (dość szybko przerzuciłem się na Zend Core 2) i przy jego instalacji zauważyłem ZF.
W dobie programowania obiektowego mamy szeroki wybór API, które zdeserializują nam rekord tablicy na obiekt klasy, pozwolą go zmodyfikować w łatwy sposób i dokonać ponownej serializacji. Sztandarowym przykładem jest na chwilę obecną zapewne Java Persistence API opisywana szczegółowo przez Jacka Laskowskiego, ale oczywiście takiej możliwości w Zend Framework zabraknąć nie mogło.
Postanowiłem zatem omówić ten mechanizm i zwrócić uwagę na pewną ważną moim zdaniem kwestię - unikanie deserializacji do obiektów i wykorzystanie tablic bądź tablic asocjacyjnych tam (w Javie byłoby to używanie kolekcji lub tablic typów wbudowanych), gdzie istotne jest wydajne przetwarzanie informacji.


W moim przykładzie posłużę się tabelą opisującą pęk promieni kosmicznych, podobną do tej z którą pracuję na co dzień:

CREATE TABLE cosmic_rays(
id serial NOT NULL, -- Automatycznie generowany identyfikator
nix_date float8, -- Unix'owy stempel czasu
peak_particles float 8, -- Liczba zarejestrowanych cząstek
az float4, -- Położenie źródła we współrzędnych horyzontalnych - azymut
ze float4, -- Położenie źródła we współrzędnych horyzontalnych - odległość od zenitu
ra float4, -- Położenie źródła we współrzędnych niebieskich - rekt ascencja
dec float4, -- Położenie źródła we współrzędnych niebieskich - deklinacja
l float4, -- Położenie źródła we współrzędnych galaktycznych - szerokość gal
b float4, -- Położenie źródła we współrzędnych galaktycznych - długość gal
CONSTRAINT id PRIMARY KEY (id)
) WITHOUT OIDS;

Jak łatwo się domyślić tabela jest przygotowana dla bazy opartej o PostgreSQL, ale nie trzeba wielu zabiegów, aby przystosować ją do innego systemu baz danych.


Struktura katalogów w projekcie


Podobnie jak w poprzednim przykładzie opartym o Zend Framework struktura będzie wyglądać w następujący sposób:


  • application

    • controllers

      • IndexController.php


    • models

      • CosmicRays.php


    • views


  • configuration

    • config.xml


  • library

    • Zend

    • Zend.php


  • public

  • .htaccess

  • index.php



Plik .htaccess zawiera również jedynie informacje sterujące dla modułu rewrite:

RewriteEngine on
RewriteRule .* index.php



Inicjalizacja połączenia z bazą danych


Plik config.xml będzie zawierać niezbędne do zestawienia połączenia z bazą danych informacje:

<?xml version="1.0" encoding="utf-8">
<configData>
<db>
<adapter>PDO_Pgsql</adapter>
<configuration>
<host>localhost</host>
<port>5432</port>
<dbname>baza</dbname>
<username>uzytkownik</username>
<password>*****</password>
</configuration>
</db>
</configData>

Możemy zatem przystąpić do konfiguracji bootstrapa w pliku index.php:

//Ustawiamy ścieżki dostępu
set_include_path('.' . PATH_SEPARATOR
. './library' . PATH_SEPARATOR
. './application/models/' . PATH_SEPARATOR
. get_include_path() );

//Ładujemy klasę Zend_Loader
require_once 'Zend/Loader.php';

try{

//Ładujemy niezbędne klasy
Zend_Loader::loadClass('Zend_Config_Xml');
Zend_Loader::loadClass('Zend_Db');
Zend_Loader::loadClass('Zend_Db_Table');

Zend_Loader::loadClass('Zend_Registry');
Zend_Loader::loadClass('Zend_View');
Zend_Loader::loadClass('Zend_Controller_Front');

//Ustalamy adres bazowy
$baseUrl = substr($_SERVER['PHP_SELF'], 0,
strpos($_SERVER['PHP_SELF'], '/index.php'));

//Ładujemy konfigurację bazy danych z pliku XML (gałąź db)
$config = new Zend_Config_Xml('./configuration/config.xml','db');

//Tworzymy połączenie dla bazy danych i czynimy je domyślnym dla modeli tabel
$db = Zend_Db::factory($config->adapter,
$config->configuration->asArray());
Zend_Db_Table::setDefaultAdapter($db);


//Konfigurujemy klasę widoku
$view = new Zend_View();
$view->baseUrl = $baseUrl;
$view->setScriptPath('./application/views');
//Umieszczamy ją w rejestrze
Zend_Registry::set('view',$view);

//Inicjalizacja głównego kontrolera
$frontController = Zend_Controller_Front::getInstance();
$frontController->setRouter($router);
$frontController->setBaseUrl($baseUrl);
$frontController->setControllerDirectory(
'./application/controllers');
$frontController->throwExceptions(true);
$frontController->returnResponse(true);

//Inicjalizacja obiektu odpowiedzi
$response = $frontController->dispatch();
$response->renderExceptions(true);
$response->setHeader('Pragma', 'No-cache');
$response->setHeader('Cache-Control', 'no-cache');

//Wyświetlenie strony
echo $response;

} catch (Exception $e){
//Obsługa wyjątków
die($e->getMessage());
}

Jak widać dodanie obsługi naszej bazy danych wymagało od nas 6 linijek kodu (wraz z importem plików odpowiednich bibliotek).
Obiekt Zend_Config_Xml (dziedziczący po Zend_Config) jest obiektem pozwalającym na łatwe ładowanie plików konfiguracyjnych w postaci dokumentów XML. Jego użycie jest bardzo podobne do obsługi plików XML przy pomocy SimpleXML (patrz wcześniejszy post).
Inicjalizacji połączenia dokonujemy przez statyczną metodę factory klasy Zend_Db, której podajemy typ adaptera (dla większości baz są to adaptery PDO, własne adaptery posiadają jedynie DB2, Oracle i Mysqli) oraz parametry konfiguracyjne.
Z kolei statyczną metodą setDefaultAdapter klasy Zend_Db_Table ustawiamy utworzone połączenie dla wszystkich obiektów związanych z tabelami.


Nasz pierwszy model


W pliku CosmicRays.php umieścimy nasz model dla tabeli cosmic_rays:

Zend_Loader::loadClass('Zend_Db_Table');

class CosmicRays extends Zend_Db_Table{
protected function _setup(){
//Standardowo ZF ustala nazwę tabeli jako łańcuch znaków występujący
//po ostatnim znaku _ w nazwie klasy modelu, więc musimy ją zmienić
$this->_name = 'cosmic_rays';
//Możemy również ustalić nazwę klucza głównego (jeśli inna niż id)
//oraz schemat tabeli (szczególnie ważne dla baz takich jak DB2 czy Oracle)

//Na koniec ładujemy ustawienia klasy bazowej
parent::_setup();
}
}

I to by było na tyle. Musimy tylko stworzyć obiekt klasy CosmicRays i już możemy modyfikować jej obiekty. Do dyspozycji mamy metody (wymieniłem najważniejsze):

  • fetchNew() - tworzy nowy pusty "rekord"

  • fetchRow($where, $order) - zwraca pierwszy "rekord" z wyniku spełniającego warunek $where z uporządkowaniem $order

  • fetchAll($where, $order) - zwraca wszystkie "rekordy"

  • insert($data) - wstawia "rekord" na podstawie tablicy asocjacyjnej

  • update($data, $where) - aktualizuje dane na podstawie tablicy asocjacyjnej


Warto zwrócić uwagę, iż metoda fetchAll zwraca nam obiekt typu Zend_Db_Table_Rowset z iteratorem dla obiektów Zend_Db_Table_Rows (dzięki temu możliwe jest przeglądanie kolekcji za pomocą pętli foreach). Metody fetchNew i fetchRow zwracają nam obiekt typu Zend_Db_Table_Row.
Każdy z obiektów Zend_Db_Table_Row udostępnia nam dane w sposób podobny do obiektów stClass. Możemy zatem modyfikować je w łatwy sposób (niedopuszczalna jest jedynie modyfikacja klucza głównego), np.

$cr = new CosmicRays();
$row = $cr->fetchRow("id = 10");
$row->ra = 0;
$row->update();

Przytoczona powyżej metoda update() daje nam możliwość zaktualizowania obiektu. Istnieje również metoda save() pozwalająca na zapis nowego obiektu utworzonego za pomocą metody fetchNew().


Problem wydajności


Problemem na jaki natrafiłem było pobranie jedynie dwóch kolumn z całej tabeli. Oczywiście możemy stworzyć obiekt select. Jednak wymaga to umieszczenia obiektu $db w rejestrze, bądź wyłuskania go z obiektu modelu.
Oczywiście dla zapytań łączących dane z wielu tabeli jest to praktycznie jedyna metoda. Co jeśli jednak chcemy dane wyłuskać TYLKO z jednej tabeli?

Stwórzmy nową metodę w klasie modelu:

public function fetchAllColumns($columns = null, $where = null, $order = null,
$count = null, $offset = null){

// selection tool
$select = $this->_db->select();

//Set columns
$columns = (is_null($columns))?$this->_cols:$columns;


// the FROM clause
$select->from($this->_name, $columns);

// the WHERE clause
$where = (array) $where;
foreach ($where as $key => $val) {
// is $key an int?
if (is_int($key)) {
// $val is the full condition
$select->where($val);
} else {
// $key is the condition with placeholder,
// and $val is quoted into the condition
$select->where($key, $val);
}
}

// the ORDER clause
if (!is_array($order)) {
$order = array($order);
}
foreach ($order as $val) {
$select->order($val);
}

// the LIMIT clause
$select->limit($count, $offset);

// return the results
$stmt = $this->_db->query($select);
$data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);

return $data;
}

Osoby, które przyglądały się klasie Zend_Db_Table_Abstract zauważą szybko, że moja metoda nie jest niczym innym jak lekko zmodyfikowaną metodą _fetch(...).
Warto jednak spojrzeć na implementacje metody fetchAll(...) tej klasy. Otóż tablicę asocjacyjną opakowuje ona w obiekt Zend_Db_Table_Rowset. W sytuacjach kiedy zależy nam na wydajności oraz niskim zapotrzebowaniu na pamięć warto zrezygnować z ułatwienia jakie niesie za sobą klasa Zend_Db_Table_Rowset.

Na koniec jeszcze jedna rada. Warto korzystać z instrukcji unset($data), szczególnie jeśli obiekt $data jest pokaźnych rozmiarów tablicą, której dalej nie zamierzamy używać. Tymbardziej, że często administratorzy serwerów nie dają dla skryptów php więcej niż 16MB pamięci.

środa, 4 kwietnia 2007

Przetwarzanie XML w językach Java i PHP (cz.2)


W przypadku PHP dostępność standardów obsługi XMLa ewoluowała wraz z kolejnymi wersjami. Dla wersji 3 dostępny był interfejs SAX, który nie zmienił się do chwili obecnej (problemy kompatybilności mogą być związane jedynie z obsługą przestrzeni nazw dla elementów, jeżeli zainstalowana jest biblioteka libxml2 w wersji starszej niż 2.6).


W wersji 4 dodano obsługę DOM (rozszerzenie domxml) oraz XSLT (rozszerzenie xslt). Pierwsze z nich nie spełniało jednak założeń W3C, powodowało wiele wycieków pamięci, przez co nie było zbyt popularne. Drugie wymagało do działania biblioteki Sablotron lub Expat.


Prawdziwy przełom przyniosła wersja 5. Obsługa DOM została przepisana na nowo, zgodnie ze standardami W3C i umieszczona w rozszerzeniu DOM. Dodano obsługę XPath oraz walidację według plików DTD (odnośnik umieszczony w pliku XML), XSchema oraz RelaxNG (nazwa pliku definiowana w wywołaniu funkcji).
XSLT zostało oparte o bibliotekę libxslt (najszybsze rozwiązanie na rynku) i umieszczone w rozszerzeniu XSL.
Oba nabrały również prawdziwie obiektowego charakteru.
Co więcej dodano interfejs SimpleXML umożliwiający łatwą modyfikacje plików XML bez znacznego obciążenia systemu charakterystycznego dla DOM.


SAX


Parser SAX tworzymy poprzez wywołanie funkcji xml_parser_create(); Kolejnym etapem jest utworzenie funkcji odpowiadających zdarzeniom XML i ich zarejestrowanie. Pod adresem http://pl.php.net/manual/en/ref.xml.php#id7330424 dostępny jest przykład takiego parsera (łącznie ze sprawdzaniem zewnętrznych encji).


DOM i SimpleXML


W przypadku PHP 5 użycie DOM jest analogiczne jak w innych językach programowania (JavaScript, Java...). Pracę z nim rozpoczynamy od utworzenia obiektu DOMDocument.
SimpleXML jest natomiast bardzo ciekawym intefejsem. Na podstawie pliku XML tworzy on coś na kształt klasy PHP stdClass (dla innych języków obiektowych można porównać to do klasy będącej kolekcją obiektów). Dzięki czemu do danego elementu odwołujemy się w bardzo przyjemny sposób (Zend wykorzystał ten intefejs przy tworzeniu klasy Zend_Config_XML).
Warto zauważyć, że poprzez funkcję simplexml_import_dom i dom_import_simplexml możemy łatwo konwertować dane z jednego standardu na drugi (jest to szczególnie przydatne w przypadku zapisywania dokumentu XML do pliku, której to SimpleXML funkcji nie posiada).


Przykład


Na koniec chciałbym przedstawić przykład, który pobiera dane z tabeli Customers (baza Sample XML dla IBM DB2 9) i wyciąga z nich imię i nazwisko klienta. Pragnę zwrócić uwagę na dwa elementy. Po pierwsze różnice w objętości dla SimpleXML i DOM realizujących tą samą operację. Po drugie warto zauważyć, iż metody klas DOMDocument w PHP i Document dla Javy są takie same:

error_reporting(E_ALL|E_STRICT);

$user = "db2admin";
$password = "***";
$database = "SAMPLE";
$hostname = "localhost";
$port = 50000;

$connect_string = "DRIVER={IBM DB2 ODBC DRIVER};" .
"DATABASE=$database;" .
"HOSTNAME=$hostname;" .
"PORT=$port;" .
"PROTOCOL=TCPIP;" .
"UID=$user;" .
"PWD=$password;";
$connection = db2_connect($connect_string,'','');
if ($connection){
echo "Connection successful.<br />";
$sql = "SELECT INFO FROM CUSTOMER WHERE CID = ?";
$statement = db2_prepare($connection, $sql);
db2_execute($statement,array(1001));

while ($customer = db2_fetch_object($statement)){
$info = $customer->INFO;

//SimpleXML
$simpleXML = new SimpleXMLElement($info);
echo $simpleXML->name."<br />";

//DOM
$domDocument = new DOMDocument('1.0','UTF-8');
$domDocument->loadXML($info);
$names = $domDocument->getElementsByTagName('name');
foreach ($names as $name) {
echo $name->nodeValue.'<br />';
}
}
db2_close($connection);
}else{
echo "Connection failed.";
}



Podsumowanie


Pokazane w obu częściach przykłady stanowią jedynie czubek góry lodowej związanej z przetwarzaniem dokumentów XML w językach Java i PHP. Jednak mogę okazać się przydatnym wstępem do dalszego zgłębiania tematu.

Wszelakie komentarze i uwagi mile widziane

Przetwarzanie XML w językach Java i PHP (cz.1)


Realizując praktyki z bazą IBM DB2 9 (w szczególności jej hybrydowym silnikiem) napotkałem na brak informacji na temat przetwarzania danych XML dostępnych w postaci strumieni.

Podczas lektury książki J2EE. Vademecum Programisty doszedłem nawet do błędnego wniosku jakoby przed wersją 1.6 Javy nie była taka obsługa zaimplementowana w standardzie. Wszędzie były tylko informacje jak parsować pliki XML.

Po przeszukaniu internetu natrafiłem na STreaming Api for XML (StAX) i jego dokumentacje w postaci JSR-173. Okazało się, że Java SE 1.6 zawiera nawet interfejs do niego, jednak brak jest implementacji. Na stronie http://stax.codehaus.org/ szybko tą implementację znalazłem, a później natrafiłem jeszcze na implementację Sun'a (pod adresem https://sjsxp.dev.java.net/) oraz Bea Weblogic (pod adresem http://dev2dev.bea.com/xml/stax.html). Ponieważ pierwsza z tych trzech stanowi implementację referencyjną będzie użyta w niniejszym tutorialu.



Niezależnie od wszystkiego postanowiłem jeszcze raz sprawdzić możliwości analizy strumieni tekstowych z XMLem dla standardów Simple Api for XML (SAX) i Document Object Model (DOM). Okazało się, że tkwiłem w błędnym przekonaniu.



SAX



SAX jest uznawany za najszybszy i najmniej pamięciożerny parser XML. Opiera się o reprezentację zdarzeniową, w której otwarcie i zamknięcie każdego dokumentu, znacznika czy występowanie tekstu jest określane jako zdarzenie.

Niewątpliwą wadą SAXa jest brak wiedzy o położeniu w pliku XML (stanu). Jest to również dość ciężki do implementacji we własnych projektach interfejs.

Pracę z SAXem rozpoczynamy od stworzenia klasy reagującej na zdarzenia (u mnie MyHandler). Poniższy przykład opiera się na kodzie zawartym w książce J2EE. Vademecum Programisty dostępnej w sprzedaży wydawnictwa Helion:

package org.f2k.test;


import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class MyHandler extends DefaultHandler {
StringBuffer textBuffer;
static private Writer out;

public MyHandler() throws UnsupportedEncodingException{
out = new OutputStreamWriter(System.out, "UTF8");
}

public void startDocument() throws SAXException{
nl();
nl();
emit("START OF DOCUMENT");
nl();
emit("");
}

public void endDocument() throws SAXException{
nl();
emit("END OF DOCUMENT");
try {
nl();
out.flush();
} catch (IOException e) {
throw new SAXException("IO Exception", e);
}
}

public void startElement(String namespaceURI,
String sName, // nazwa skrócona
String qName, // nazwa pełna
Attributes attrs) throws SAXException{

echoText();
nl();
emit("ELEMENT: ");
String eName = sName; // nazwa elementu
// bez uwzględnienia nazwy przestrzeni nazw
if ("".equals(eName))
eName = qName;
emit("<"+eName);
if (attrs != null) {
for (int i = 0; i < attrs.getLength(); i++) {
String aName = attrs.getLocalName(i); // nazwa atrybutu
if ("".equals(aName)) aName = attrs.getQName(i);
nl();
emit(" ATR: ");
emit(aName);
emit("\t\"");
emit(attrs.getValue(i));
emit("\"");
}
}
if (attrs.getLength() > 0) nl();
emit(">");
}

public void endElement(String namespaceURI,
String sName, // nazwa skrócona
String qName // nazwa pełna
)throws SAXException {
echoText();
nl();
emit("END_OF_ELEM: ");
String eName = sName; // nazwa elementu
if ("".equals(eName))
eName = qName;
emit("");
}

public void characters(char buf[], int offset, int len)
throws SAXException{
String s = new String(buf, offset, len);
if (textBuffer == null) {
textBuffer = new StringBuffer(s);
} else {
textBuffer.append(s);
}
}


// Metody pomocnicze
private void echoText() throws SAXException{
if (textBuffer == null) return;
nl();
emit("CHARS: |");
String s = ""+textBuffer;
emit(s);
emit("|");
textBuffer = null;
}

private void emit(String s) throws SAXException{
try {
out.write(s);
out.flush();
} catch (IOException e) {
throw new SAXException("IO Exception", e);
}
}

private void nl() throws SAXException{
String lineEnd = System.getProperty("line.separator");
try {
out.write(lineEnd);
} catch (IOException e) {
throw new SAXException("IO Exception", e);
}
}
}

Skoro mamy już klasę obsługującą zdarzenia przygotujmy klasę testową, dzięki której będziemy mogli podać String zawierający XML:

package org.f2k.test;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.helpers.DefaultHandler;

public class SAXTest {
SAXTest(String xml){
this(new ByteArrayInputStream(xml.getBytes()));
}

SAXTest(InputStream stream){
try {
DefaultHandler handler = new MyHandler();
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(stream, handler);
} catch (Exception e) {
e.printStackTrace();
}
}
}

Mamy właściwie wszystko co potrzeba dla SAX. Możemy jeszcze dla obiektu factory ustawić opcję sprawdzania dokumentu XML według DTD, itp. Ale to pozostawiam już programistom:)



DOM



DOM jest parserem, który wykorzystuje się do złożonych modyfikacji XML, czy jego reprezentacji w postaci drzewa obiektu. Jego wadą jest fakt, że jest zasobożerny. Cały dokument jest bowiem przetwarzany w pamięci komputera.

W przypadku DOM możemy od razu napisać naszą klasę testową:

package org.f2k.test;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

public class DOMTest {
public DOMTest(String xml){
this(new ByteArrayInputStream(xml.getBytes()));
}

public DOMTest(InputStream stream){
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(stream);
NodeList names = document.getElementsByTagName("name");
for(int i=0; i < names.getLength(); i++){
System.out.println(names.item(i).getTextContent());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

Nasz test wyszukuje w dokumencie XML węzły o nazwie name i wyświetla ich zawartość.



StAX



StAX jest pareserem opracowanym przez firmę BEA. Choć jego działanie opiera się na zdarzeniowej formie XML, to udostępnia nam wiedzę o położeniu w dokumencie poprzez iterator. Pozwala to na szybszą niż w przypadku DOM deserializację prostych obiektów.


Najprostszym użyciem StAXa jest stworzenie funkcji, która będzie analizować zdarzenia. W poniższym przykładzie funkcja processEvent będzie realizować podobne zadanie do klasy MyHandler z testu SAX:

package org.f2k.test;

import java.io.*;
import javax.xml.stream.*;
import javax.xml.stream.events.*;


public class StAXTest {
StringBuffer textBuffer;
static private Writer out;

StAXTest(String xml){
this(new ByteArrayInputStream(xml.getBytes()));
}

StAXTest(InputStream stream){
try {
out = new OutputStreamWriter(System.out, "UTF8");
} catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
/*Implementation of StAX*/
System.setProperty("javax.xml.stream.XMLInputFactory",
"com.bea.xml.stream.MXParserFactory");

try {
XMLInputFactory inputFactory
= XMLInputFactory.newInstance();
XMLEventReader eventReader
= inputFactory.createXMLEventReader(stream);
while (eventReader.hasNext()){
XMLEvent event = (XMLEvent) eventReader.next();
processEvent(event);
}
} catch (Exception e) {
e.printStackTrace();
}
}

public void processEvent(XMLEvent event) throws IOException{
if (event.isStartElement()){
echoText();
nl();
emit("ELEMENT: <");
emit(((StartElement) event).getName().toString());
emit(">");
}
if (event.isCharacters()){
String s = ((Characters) event).getData();
if (textBuffer==null){
textBuffer = new StringBuffer(s);
} else {
textBuffer.append(s);
}
}
if (event.isEndElement()){
echoText();
nl();
emit("END_OF_ELEM: ";
emit("");
}
}

// Metody pomocnicze
private void echoText() throws IOException{
if (textBuffer == null) return;
nl();
emit("CHARS: |");
String s = ""+textBuffer;
emit(s);
emit("|");
textBuffer = null;
}
private void emit(String s) throws IOException{
out.write(s);
out.flush();
}
private void nl() throws IOException{
String lineEnd = System.getProperty("line.separator");
out.write(lineEnd);
}
}

Skoro mamy już wszystkie testy pora sprawdzić działanie każdego z parsera. Do tego celu wykorzystałem tabelę Customer z bazy SAMPLE (w wersji XML) dostępnej z DB2 9. Poniżej kod zestawiający połączenie i pobierający rekord:

package org.f2k.test;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import com.ibm.db2.jcc.DB2Xml;

public class DB2Connector {
String password="***";
String user="db2admin";

String url = "jdbc:db2://localhost:50000/SAMPLE";
public DB2Connector(){
try {
Class.forName("com.ibm.db2.jcc.DB2Driver").newInstance();
Connection conn = DriverManager.getConnection(url,
user, password);
PreparedStatement stmt = conn.prepareStatement(
"SELECT INFO FROM MCUSTOMER WHERE CID = ?");
//Należy wcześniej sprawdzić czy użytkownik o CID 1001 istnieje
stmt.setInt(1, 1001);
ResultSet result = stmt.executeQuery();
while (result.next()){
DB2Xml info = (DB2Xml) result.getObject("INFO");
String xmlText = info.getDB2XmlString();
System.out.print("DB2XmlString:\n"+xmlText);
//Dla kodowania innego niż UTF-8 tylko StAX przechodzi test
InputStream stream = info.getDB2XmlBinaryStream("UTF-8");
System.out.println("\n");
System.out.println("DB2Xml from StAX test:");
new StAXTest(stream);
System.out.println("\n");
System.out.println("DB2Xml from SAX test:");
stream.reset();
new SAXTest(stream);
System.out.println("\n");
System.out.println("DB2Xml from DOM test:");
stream.reset();
new DOMTest(stream);
}
result.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args){
new DB2Connector();
}
}




Wyniki



Parsery StAX i SAX powinny zwrócić w wyniku poszatkowaną strukturę XML z opisami zdarzeń. Natomiast DOM powinien wyświetlić imię i nazwisko klienta, wyciągnięte z XMLa (u mnie Kathy Smith).



W drugiej części opiszę systemy przetwarzania XML z PHP. Również na podstawie danych zaczerpniętych z DB2.

Zend_Controller_Router_Route i tworzenie nowych tras (dla wersji 0.9 i nowszych)


Jak słusznie zauważył Wojciech w komentarzu do poprzedniego postu od wersji 0.9 wprowadzono kilka zmian. Między innymi w celu umieszczenia biblioteki w jednym katalogu postanowiono podzielić klasę Zend na kilka innych. Plik Zend.php zapewnie zniknie w wersji 1.0.

Jako, że ja w swoim projekcie opierałem się o wersję 0.8 postanowiłem podać zmodyfikowaną wersję kodu, a właściwie jedynie fragmentu pliku index.php:

//Od tej pory zamiast pliku Zend.php ładujemy klasę loadera

require_once 'Zend/Loader.php';


try {

//Ładowanie klas odbywa się poprzez metodę klasy

//Zend_Loader

//Dodatkowo wymagana jest klasa obsługi rejestu

Zend_Loader::loadClass('Zend_Registry');

Zend_Loader::loadClass('Zend_View');

Zend_Loader::loadClass('Zend_Controller_Front');

Zend_Loader::loadClass('Zend_Controller_Router_Rewrite');

//Ustalamy adres bazowy
$baseUrl = substr($_SERVER['PHP_SELF'], 0,

strpos($_SERVER['PHP_SELF'], '/index.php'));

//Konfigurujemy klasę widoku
$view = new Zend_View();
$view->baseUrl = $baseUrl;
$view->setScriptPath('./application/views');
//Umieszczamy ją w rejestrze poprzez metodę nowej klasy
Zend_Registry::set('view',$view);



Reszta kodu pozostaje bez zmian

wtorek, 3 kwietnia 2007

Zend_Controller_Router_Route i tworzenie nowych tras


Ostatnio na polskim forum Zend Framework spotkałem się z problemem obsługi przyjaznych adresów. Standardowo biblioteka ta obsługuje adresy w postaci:

http://nazwa_strony/nazwa_kontrolera/nazwa_akcji/parametr/wartość

Jednak strona miała zawierać treść wielojęzyczną. I choć Zend obsługuje możliwość odczytania informacji z przeglądarki to programista chciał sterować językiem poprzez umieszczenie kodu przed nazwą kontrolera, np.:

http://nazwa_strony/pl/nazwa_kontrolera/...

Postanowiłem zatem zajrzeć do dokumentacji i napisać niezbędny kod. Zaczniemy od struktury katalogów, którą zaczerpnąłem z jednego tutoriala:

  • application
    • controllers
      • IndexController.php
    • models
    • views
  • library
    • Zend
    • Zend.php
  • public
  • .htaccess
  • index.php

Plik .htaccess zawiera informacje sterujące dla modułu rewrite:

RewriteEngine on
RewriteRule .* index.php


Plik index.php stanowi tak zwany bootstrap dla ZF. W nim dokonujemy należy zainicjalizować wszystkie niezbędne do działania elementy środowiska. A więc do dzieła:

//Konfiguracja debugowania

define('_DEBUG',1);



if (defined('_DEBUG'))

if (_DEBUG == 1)

error_reporting(E_ALL|E_STRICT);



date_default_timezone_set('Europe/Warsaw');



//Konfigurujemy ścieżki bibliotek

set_include_path('.' . PATH_SEPARATOR

. './library' . PATH_SEPARATOR

. './application/models/' . PATH_SEPARATOR

. get_include_path() );



//Ładujemy jądro ZF

include('Zend.php');



try {

//Ładujemy niezbędne klasy

Zend::loadClass('Zend_View');

Zend::loadClass('Zend_Controller_Front');

Zend::loadClass('Zend_Controller_Router_Rewrite');



//Ustawiamy adres bazowy

$baseUrl = substr($_SERVER['PHP_SELF'], 0,

strpos($_SERVER['PHP_SELF'], '/index.php'));



//Konfiguracja klasy widoku
$view = new Zend_View();

$view->baseUrl = $baseUrl;

$view->setScriptPath('./application/views');

Zend::register('view',$view);



//Tutaj konfigurujemy naszą nową trasę

$route = new Zend_Controller_Router_Route(

':lang/:controller/:action',

//Zmienna określająca język znajduje się przed kontrolerem

array('lang'=>'pl', 'controller'=>'index',

'action'=>'index'), //Wartości domyślne

array('lang'=>'[a-z_]+')); //Warunek dla lang



//Utworzenie routera z naszą trasą

$router = new Zend_Controller_Router_Rewrite();

$router->removeDefaultRoutes();

$router->addRoute('user', $route);



//Inicjalizacja głównego kontrolera

$frontController = Zend_Controller_Front::getInstance();

$frontController->setRouter($router);

$frontController->setBaseUrl($baseUrl);

$frontController->setControllerDirectory(

'./application/controllers');

$frontController->throwExceptions(true);

$frontController->returnResponse(true);



//Inicjalizacja obiektu odpowiedzi

$response = $frontController->dispatch();

$response->renderExceptions(true);

$response->setHeader('Pragma', 'No-cache');

$response->setHeader('Cache-Control', 'no-cache');



//Wyświetlenie strony

echo $response;

} catch (Exception $exception) {

//Obsługa wyjątku

$message = 'Application setup exception: ';

$message.= $exception->getMessage();

if (defined('_DEBUG')) if (_DEBUG){

$message .= 'Trace: '.$exception->getTraceAsString();

}

die($message);

}

Nie pozostało zatem nic innego jak odczytać informacje o języku. Dokonamy tego w pliku IndexController.php:

class IndexController extends Zend_Controller_Action

{

function indexAction()

{

echo $this->_getParam('lang','pl');

echo " in IndexController::indexAction()";

}

}

Po przekopiowaniu plików na serwer dostaniemy informacje o języku i wywołanej metodzie

Powitanie

Na wstępie nie wypada mi nie przywitać wszystkich osób, które pojawiły się na moim blogu. Powstał on właściwie pod wpływem Jacka Laskowskiego, który w przejrzysty sposób dzieli się swoimi doświadczeniami z zakresu J2EE.
Co prawda nie mogę porównać swojej wiedzy do zakresu wiadomości Jacka. Jednak postanowiłem dzielić się swoimi doświadczeniami z programowaniem. Często zdarzało mi się bowiem dotykać technologii, które nie były popularne na rynku polskim i ciężko było znaleźć dokładny opis ich działania.
Zapraszam zatem do czytania i komentowania moich nowych postów.