DB4O - Obiektowa baza danych
Wstęp
Wszędobylski tandem: baza danych + ORM
Myśląć "przechowywanie danych" w kontekście aplikacji komputerowych prawie na pewno myślimy "relacyjna baza danych i mapowanie obiektowo-relacyjne". Bazy danych, które są dojrzałymi i niezawodnymi produktami zapewniają nam trwałe przechowywanie danych. Natomiast rozwiązania typu ORM umożliwiają nam dostęp do tych danych w postaci użytecznej dla programów napisanych w sposób obiektowy. Podejście to zostało spopularyzowane przez framework Hibernate a następnie ustandaryzowane jako Java Persistence API stanowiące element specyfikacji Java EE 5.
Podejście takie izoluje w dużym stopniu projektanta/programistę od czynności
związanych z relacyjnymi bazami danych (takimi jak zakładanie indeksów,
rozmiary kolumn itp.) Jednak w przypadku bardziej złożonych aplikacji kod w
obiektowym języku programowania zostaje "zaśmiecony" pewnymi
elementami relacyjnymi, np. adnotacjami JPA; @Column
,
@ManyToMany
.
Obiektowa baza danych
Alternatywą mogą być obiektowe bazy danych. Obiekty są w nich przechowywane tak, jak używamy ich w programach napisanych zgodnie z paradygmatem obiektowym. Nie trzeba ich mapować na model relacyjny ani modelować takich podstawowych zależności jak dziedziczenie i kompozycja.
Nie mogą one jeszcze zagrozić pozycji relacyjnych baz danych, jednak warto przyjrzeć się możliwościom, jakie oferują. db4o jest bazą danych dostępną na platformy Java i .NET. Możemy jej używać na licencji GPL oraz komercyjnej.
Instalacja
Instalacja sprowadza się do ściągnięcia paczki ze wszystkimi niezbędnymi
komponentami. Później należy tylko dodać odpowiedni plik JAR do zmiennej
CLASSPATH
(np. dla db4o w wersji 7.4 i javy 1.5 -
db4o-7.4.121.14026-java5.jar).
Można również dodać db4o jako zależność w projektach zarządzanych przez Mavena, odpowiednie artefakty znajdują się pod adresem http://source.db4o.com/maven.
Podstawowe interfejsy
Db4o
Klasa-fabryka, która służy do rozpoczęcia korzystania z bazy w jednym z dostępnych trybów, jak również zarządzaniem konfiguracją.
ObjectContainer
Interfejs, przy pomocy którego wykonujemy wszystkie podstawowe operacje
CRUD
na bazie. Każdy obiekt typu ObjectContainer
implementuje również
interfejs rozszerzony ExtObjectContainer
(metoda
ObjectContainer.ext()
), który zapewnia funkcjonalność dodatkową
(np. dostęp do wewnętrznych identyfikatorów obiektów).
Interfejs ten w db4o zapewnia podobną funkcjonalność jak EntityManager w Java Persistence API.
ObjectSet
Obiekty tego typu są zwracane jako wyniki zapytań. Interfejs ten rozszerza
kilka innych interfejsów z biblioteki standardowej Javy; Collection
,
List
, Iterator
i Iterable
. Możemy go
zrzutować na taki, który aktualnie potrzebujemy w naszej aplikacji.
Tryby działania
Baza danych db4o można używać w dwóch trybach; wbudowanym i klient-serwer. Pierwszy z nich nadaje się do mniejszych aplikacji, bez przetwarzania współbieżnego, natomiast tryb klient-serwer będzie idealny dla aplikacji wielowątkowych, z wieloma transakcjami wykonującymi się równolegle. Czyli m.in. w popularnych aplikacjach internetowych. Działanie bazy w trybie klient-serwer możemy ograniczyć tylko do jednej maszyny wirtualnej a komunikacja pomiędzy klientami a serwerem polegać będzie na wymianie referencji do obiektów Javy (analogia do interfejsów lokalnych w EJB). Oczywiście klienty i serwer mogą znajdować się w różnych maszynach wirtualnych. Wtedy komunikacja odbywać się będzie po sieci TCP/IP (tym razem analogia do interfejsów zdalnych w EJB).
Tryb wbudowany
// Listing 1 ObjectContainer oc = Db4o.openFile(PATH_TO_DB4O_FILE); // operacje na bazie oc.close();
Tryb klient-serwer
Jedna maszyna wirtualna
// Listing 2 // serwer ObjectServer server = Db4o.openServer(PATH_TO_DB4O_FILE, 0); // klient ObjectContainer oc = server.openClient();
Wiele maszyn wirtualnych
// Listing 3 // serwer ObjectServer server = Db4o.openServer(PATH_TO_DB4O_FILE, PORT); server.grantAccess(USERNAME, PASSWORD); // klient ObjectContainer oc = Db4o.openClient(HOST, PORT, USERNAME, PASSWORD);
Podstawowe operacje
// ObjectContainer oc – uzyskany w którymkolwiek z trybów działania
Zapisywanie
oc.store(new Person("Jan", "Kowalski"));
Prościej chyba się już nie da...
Usuwanie
oc.delete(person);
Modyfikowanie
// person – obiekt pobrany z bazy // nawet jeśli będzie zawierał te same wartości pól, to db4o uzna go jako // nowy oc.store(person);
Wyszukiwanie
W relacyjnych bazach używamy języka SQL do wykonywania wszystkich operacji, również wyszukiwania. Baza db4o oferuje trzy rodzaje obiektowego API do wyszukiwania obiektów.
Wyszukiwanie przez przykład
Jest to naprostszy sposób, który jednocześnie obarczony jest największymi ograniczeniami. Jako kryteria wyszukiwania należy podać obiekt z ustawionymi pewnymi właściwościami. Baza znajdzie obiekty tej samej klasy, które będą miały takie same wartości atrybutów podanego przykładu.
// Listing 4 Person example = new Person(); example.setLastname("Kowalski"); example.setAge(34); List<Person> results = oc.queryByExample(example);
Metoda queryByExample()
zwraca obiekt typu ObjectSet
,
który implementuje m.in. interfejs List
. Otrzymamy listę osób,
które będą miały na nazwisko Kowalski i będą miały 34 lata. W ten
sposób bardzo łatwo możemy konstruować proste zapytania. Ma on
jednak kilka niedogodności. Dostęp do pól klasy db4o
uzyskuje poprzez refleksję – jest to więc dosyć wolna metoda. Nie można
tworzyć rozbudowanych warunków zawierających operatory logiczne, wywołania
metod i operatorów innych niż ==
. Powyższy przykład odpowiada
warunkowi lastname.equals("Kowalski") && age == 34
. Niemożliwe
jest również wyszukiwanie obiektów, których pola mają mieć wartości domyślne
(0 dla int
i long
, false
dla
boolean
, null
dla referencji), gdyż db4o potraktuje
to jako brak kryterium dla danego atrybutu. Obiekty, które chcemy użyć jako
przykład dla wyszukiwania muszą mieć możliwość pozostawienia pewnych pól
niezainicjalizowanych lub ustawienia wartości domyślnych. Jest tak zazwyczaj
dla obiektów typu JavaBean.
Zapytania natywne
Zapytania natywne są głównym i preferowanym sposobem wyszukiwania w db4o,
gdyż pozwalają na sprawdzanie typów w trakcie kompilacji i łatwą refaktoryzację
kodu. Przetwarzanie zapytań natywnych polega na sprawdzeniu wyrażenia dla
wszystkich obiektów pewnej klasy i zwróceniu tych, dla których wyrażenie
jest prawdziwe. Wyrażenia te kodujemy implementując interfejs Predicate
.
Parametryzowany jest typem obiektów, które chcemy wyszukać i zawiera jedną
metodę match()
, która zwraca wartość logiczną true
dla obiektów spełniających założone kryteria.
// Listing 5 List<Person> people = oc.query(new Predicate<Person>() { @Override public boolean match(Person candidate) { return candidate.getLastname().startsWith("K"); } });
Powyższe zapytanie natywne zwróci wszystkie osoby w bazie o nazwiskach
zaczynających się od "K". Wszystkie zmiany w klasie Person
,
np. zmianę atrybutu lastname
z typu String
na
jakikolwiek inny zostanie od razu zauważone przez kompilator.
Metoda query()
przyjmująca jako pierwszy argument obiekt typu
Predicate<T>
posiada jeszcze dwie dodatkowe odmiany
pozwalające na uporządkowanie listy wyników; jedna przyjmuje komparator typu
java.util.Comparator<T>
a druga
com.db4o.query.QueryComparator<T>
.
db4o próbuje optymalizować wyrażenia typu Predicate
tak, żeby użyć wewnętrzne indeksy bazy i utworzyć jak najmniej instancji.
Prace nad optymalizatorem ciągle trwają a ulepszenia pojawiają się w każdym
kolejnym wydaniu bazy.
Zapytania SODA
SODA jest niskopoziomowym API to tworzenia zapytań w db4o. Za jego pomocą możemy dowolnie modelować zapytanie:
// Listing 6 Query q = oc.query(); q.constrain(Person.class); // ograniczamy zapytanie od obiektów klasy Person q.descend("lastname"); // przechodzimy do atrybutu "lastname" q.constrain("K").startsWith(true); // i ograniczamy go do wartości // rozpoczynających się od "K"
Metoda query
tworzy zapytanie, które zwraca wszystkie obiekty w
bazie. Kolejne metody dodają ograniczenia. Już w tym przykładzie widać, że
to API nie zapewnia bezpieczeństwa typów; w trakcie kompilacji nie można
sprawdzić, czy atrybut lastname
w ogóle istnieje oraz czy
wartość "K" jest poprawnym ograniczeniem dla niego. Sposób ten ma jednak
dużą zaletę; dzięki niemu można generować zapytania dynamicznie.
Prosta aplikacja internetowa typu CRUD
Rozpoczynając naukę jakiegokolwiek języka programowania pierwszą napisaną aplikacją jest zazwyczaj Hello World! W świecie aplikacji i frameworków webowych odpowiednikiem jest prosta aplikacja typu CRUD (Create, Read, Update, Delete). W niniejszym artykule ograniczymy się tylko do operacji Create i Read; stworzymy prostą listę zadań.
// Listing 7 public class ClientFactory { private static final ObjectServer server; static { File home = new File(System.getProperty("user.home")); File db = new File(home, "crud.db4o"); server = Db4o.openServer(db.getPath(), 0); } public static ObjectContainer openClient() { return server.openClient(); } public static void close() { server.close(); } }
Klasa ClientFactory
zawiera statyczną referencję do serwera
db4o a metoda getClient()
tworzy nam nową instancję typu
ObjectContainer
. Będziemy ją pobierać przed wykonaniem operacji
na bazie i zamykać po jej zakończeniu.
// Listing 8 public class Db4oListener implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { try { Class.forName("eu.pawelcegla.db4ocrud.ClientFactory"); } catch (ClassNotFoundException ex) { throw new RuntimeException("Error starting db4o client factory", ex); } } public void contextDestroyed(ServletContextEvent sce) { ClientFactory.close(); } }
// Listing 9 <listener> <listener-class>eu.pawelcegla.db4ocrud.Db4oListener</listener-class> </listener>
Db4oListener
obsługuje zdarzenia zainicjalizowania i usunięcia
kontekstu aplikacji webowej – czyli po prostu uruchomienia i zatrzymaniu
aplikacji. Przy starcie wymuszamy załadowanie klasy ClientFactory
a jednocześnie wykonania jej bloku static
. W nim zaś uruchamiamy
serwer db4o, który będzie pracował na pliku crud.db4o umieszczonym
w katalogu domowym użytkownika. Wraz z zatrzymaniem aplikacji zamykamy serwer.
Żeby zaimplementowany listener obsługiwał te zdarzenia należy dodać podany na
listingu 9 fragment do pliku web.xml.
// Listing 10 public class Task { private final String description; private final Date due; public Task(String description, Date due) { this.description = description; this.due = due; } @Override public String toString() { DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); return "(" + df.format(due) + ") " + description; } public static Comparator<Task> getComparator() { return new Comparator<Task>() { public int compare(Task o1, Task o2) { return o1.due.compareTo(o2.due); } }; } }
Klasa reprezentująca zadanie do wykonania. Zawiera tekstowy opis i termin zadania. Statyczna metoda zwraca komparator sortujący zadania wg terminu.
// Listing 11 public class TaskServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ObjectContainer oc = ClientFactory.openClient(); try { String pathInfo = request.getPathInfo(); if ("/add".equals(pathInfo) && "post".equalsIgnoreCase(request.getMethod())) { String description = request.getParameter("description"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date dueDate = df.parse(request.getParameter("dueDate")); oc.store(new Task(description, dueDate)); oc.commit(); response.sendRedirect(request.getContextPath() + "/task/list"); } else if ("/list".equals(pathInfo)) { List<Task> tasks = oc.query(new Predicate<Task>() { @Override public boolean match(Task candidate) { return true; } }, Task.getComparator()); request.setAttribute("tasks", tasks); request.getRequestDispatcher("/WEB-INF/jsp/tasks.jsp").forward(request, response); } } catch (ParseException ex) { throw new ServletException("Cannot parse due date", ex); } finally { oc.close(); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } }
// Listing 12 <servlet> <servlet-name>TaskServlet</servlet-name> <servlet-class>eu.pawelcegla.db4ocrud.TaskServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>TaskServlet</servlet-name> <url-pattern>/task/*</url-pattern> </servlet-mapping>
Serwlet TaskServlet
zawiera praktycznie całą logikę aplikacji.
Obsługuje dwa zgłoszenia:
-
wyświetlenie listy zadań; metoda
GET
, URL /task/list, -
dodanie nowego zadania; metoda
POST
, URL /task/add.
Listing 12 przedstawia niezbędną konfigurację do dodania w pliku web.xml.
// Listing 13 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>db4o crud</title> </head> <body> <h1>db4o crud</h1> <table> <tr> <th>Task</th> </tr> <c:forEach var="task" items="${tasks}"> <tr> <td>${task}</td> </tr> </c:forEach> </table> <strong>New task</strong> <form action="add" method="post"> Description: <input name="description" /> <br /> Due date (yyyy-mm-dd): <input name="dueDate" /> <input type="submit" value="Add" /> </form> </body> </html>
Prosta strona JSP służąca do wyświetlenia listy zadań oraz formularza do dodawania nowych.
Źródła aplikacji w postaci spakowanego projektu NetBeans dostępne są pod adresem http://pawelcegla.eu/java-express/Db4oCrud.zip.
Podsumowanie
Niniejszy artykuł starał się przedstawić jedynie absolutnie niezbędne podstawy używania obiektowej bazy danych db4o (samouczek dostępny razem z bazą ma 171 stron). Autor ma nadzieję, że czytelnicy spróbują jej użyć w swoich rozwiązaniach i sprawdzić, czy może zastąpić znany duet – relacyjną bazę danych i ORM.
Odnośniki
- http://db4o.com – główna strona projektu
- http://developer.db4o.com – zasoby dla programistów
- http://pawelcegla.eu/category/db4o - kilka wpisów o db4o na moim blogu
Czy autor mógłby przejrzeć projekt: http://pawelcegla.eu/java-express/Db4oCrud.zip ?
Bo u mnie niby deploy się udaje ale dalej nic się nie wyświetla, ani Tomcat nie sypie żadnym wyjątkiem.
Jasne, już proszę Pawła, żeby na to zerknął.
@Marcin
A jaki URL wpisujesz w przeglądarce?
Otworzyłem ściągnięty projekt w NetBeans. Rozwiązałem problem zależności (nie miałem utworzonej biblioteki 'db4o'). Deploy na Tomcat'cie, wpisałem http://localhost:9080/crud/task/list i wyświetliła się stronka z pustą listą zadań oraz formularzem do dodania nowego.
@Paweł Ok nie doczytałem w kodzie, że akurat taki adres musi być. Projekt działa na podanym adresie(z dokładnością do portu), jednak sugerowałbym dodać przekierowanie z /crud do adresu który podałeś.