W poprzednim wpisie przedstawiłem podstawową funkcjonalność db4o w kontekście integracji bazy z frameworkiem webowym Tapestry. Ze statystyk dla tamtego wpisu wynika, że kilka osób zainteresowanych było czymś konkretniejszym, mianowicie wyszukiwaniem. Niniejszy wpis stanowi zatem przegląd poszczególnych sposobów wyszukiwania obiektów w db4o.

Punktem początkowym dla wyszukiwania jest zawsze obiekt typu ObjectContainer. W nim dostępne są różne metody query odpowiadające sposobom wyszukiwania.

Dla przypomnienia podaję dwa schematy, jak uzyskać ObjectContainer:

W trybie “stand-alone”:

ObjectContainer oc = Db4o.openFile("database.db4o");

W trybie klient-serwer:

// server
int portNumber = 12345;
String user = "db4oUser";
String password = "pw";
String dbFilename = "database.db4o";
ObjectServer os = Db4o.openServer(dbFilename, portNumber);
os.grantAccess(user, password);
// client
String host = "localhost";
int portNumber = 12345;
String user = "db4oUser";
String password = "pw";
ObjectContainer oc = Db4o.openClient(host, portNumber, user, password);

Wyszukiwanie całych klas obiektów

Przykład:

class A {}
// ...
List<A> all = oc.query(A.class);

Działa to również dla całych hierarchii klas:

abstract class A {}
class B extends A {}
class C extends A {}
// ...
List<A> list = oc.query(A.class);
for (A a : list) {
    System.out.println(a.getClass().getSimpleName()); // B C B B C ...
}

Wyszukiwanie przez przykład

W wyszukiwaniu przez przykład (ang. query by example, QBE) jako parametr dla metody query podajemy obiekt klasy, którą chcemy przeszukiwać. W obiekcie tym ustawiamy te właściwości, które mają być kryteriami naszego wyszukiwania a reszcie pozostawiamy wartości domyślne. db4o znajdzie wszystkie obiekty, które będą mieć takie same wartości atrybutów jak przekazany obiekt:

class Person {
    private String firstname, lastname;
    Person(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
    @Override
    public String toString() {
        return firstname + " " + lastname;
    }
}
// ...
oc.store(new Person("Jan", "Kowalski"));
oc.store(new Person("Zbigniew", "Nowak"));
oc.store(new Person("Adam", "Kowalski"));
oc.commit();
// ...
List<Person> people = oc.queryByExample(new Person(null, "Kowalski"));
for (Person p : people) {
    System.out.println(p); // Jan Kowalski, Adam Kowalski
}

Tryb ten posiada pewne ograniczenia:

  • Do wszystkich atrybutów przykładowego obiektu db4o uzyskuje dostęp poprzez refleksję.
  • Nie można w ten sposób tworzyć bardziej złożonych zapytań (and, or, itd.)
  • Nie można szukać obiektów o atrybutach równych 0, null i false, ponieważ db4o potraktuje to jako brak kryterium dla danego atrybutu.
  • Musi istnieć możliwość tworzenia obiektów z niezainicjowanymi atrybutami, żeby później móc ustawić te, które mają być naszym kryterium wyszukiwania.

Zapytania natywne

Zapytania natywne są głównym i preferowanym sposobem wyszukiwania w db4o. Zapytania te są zakodowane w sposób pozwalający na sprawdzanie typów w trakcie kompilacji i łatwą refaktoryzację.

Ideą stojącą za zapytaniami natywnymi jest ewaluacja wyrażenia dla każdego obiektu i zwróceniu wartości true dla tych, które spełniają nasze kryteria:

class Person {
    private String firstname, lastname;
    Person(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }
    @Override
    public String toString() {
        return firstname + " " + lastname;
    }
    public String getFirstname() {
        return firstname;
    }
    public String getLastname() {
        return lastname;
    }
}
// ...
oc.store(new Person("Adam", "Nowak"));
oc.store(new Person("Adam", "Kowalski"));
oc.store(new Person("Adam", "Kowalczyk"));
oc.commit();
// ...
List<Person> people = oc.query(new Predicate<Person>() {
    @Override
    public boolean match(Person candidate) {
        return candidate.getLastname().startsWith("K");
    }
});
for (Person p : people) {
    System.out.println(p); // Adam Kowalski, Adam Kowalczyk
}

Jak widzimy w powyższym przykładzie implementujemy anonimową klasę, która rozszerza Predicate. Jest ona parametryzowana typem, którego obiekty chcemy wyszukać. Metoda match w tym przypadku zwraca true dla obiektów Person, których atrybut lastname rozpoczyna się od litery K.

db4o spróbuje zoptymalizować wyrażenie zawarte w metodzie match i wywoła je na wewnętrznych indeksach, żeby utworzyć jak najmniej instancji obiektów. Nie zawsze jednak się to udaje. Prace nad rozwojem optymalizatora trwają i ulepszenia pojawiają się wraz z każdym nowym wydaniem bazy.

Zapytania SODA

SODA jest niskopoziomowym API to tworzenia zapytań w db4o. Za jego pomocą możemy dowolnie modelować zapytanie:

// class Person {...} jak z poprzedniego przykładu
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ą kolejne 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.

Podsumowanie

Każdy sposób wyszukiwania ma swoje wady i zalety oraz zastosowania, w których sprawdza się najlepiej. Może ten krótki przegląd okaże się pomocny przy wyborze właściwej metody oraz tworzeniu zapytań. Po więcej informacji zapraszam do dokumentacji oraz czekam na wszelkie pytania i komentarze.