JAVA exPress > Archiwum > Numer 8 (2010-09-03) > DB4O - Obiektowa baza danych

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

Komentarze

  1. 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.

  2. Jasne, już proszę Pawła, żeby na to zerknął.

  3. @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.

  4. @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ś.

Tylko zalogowani użytkowincy mogą pisać komentarze

Developers World