Zielony Smok - logo witryny

Apache Derby: ładowanie klas z plików JAR w bazie danych

Gdy tworzymy np. własny typ danych to opisuje go klasa Java implementująca Externalizable. Gdy tworzymy procedury i metody – również przygotowujemy stosowne metody Java. Ta klasa i metody muszą być dostępne tam, gdzie jest baza danych. Najlepiej, gdyby całość materiału Java mogła być umieszczona na stałe w bazie i była zawsze widoczna gdy jest potrzebna. I właśnie do tego celu służy ten świetny, choć kiepsko opisany mechanizm ładowania klas z plików JAR umieszczonych w bazie danych.

Klasy te są widoczne jedynie dla funkcji i procedur SQL zawartych w bazie danych.

Klasy umieszczone w plikach JAR w bazie danych są ładowane w pierwszej kolejności.

Spróbujmy zatem jak to działa.

Przygotowanie klas Java

Klasa Kolor.java

Tworzymy klasę aderby.specyf.jars.Kolor będąca nowym typem danych do przechowywania informacji o kolorze w postaci pojedynczej liczby int. Oczywiście spokojnie moglibyśmy przechowywać kolor jako liczbę INT, ale musimy jakoś zilustrować recepturę.

package aderby.specyf.jars;

import java.io.*;

public class Kolor implements Externalizable {
    private int kolor;

    public Kolor() {
        this(0);
    }

    public Kolor(int kolor) {
        this.kolor = kolor;
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException {
        setKolor(in.readInt());
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(kolor);
    }

    public int getKolor() {
        return kolor;
    }

    public void setKolor(int kolor) {
        this.kolor = kolor;
    }
}

Klasie towarzyszy kilka metod umieszczonych w klasie DerbyUtil pozwalających na przeliczanie kolorów na RGB i liczby heksadecymalne i odwrotnie.

Działanie klasy Kolor ilustruje klasa:

Klasa R097_TEST.java
package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_TEST {
    public static final String createTypeKolor = "CREATE TYPE kolor "
            + "EXTERNAL NAME 'aderby.specyf.jars.Kolor' LANGUAGE JAVA";
    public static final String createTable = "CREATE TABLE colors("
            + "id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS "
            + "IDENTITY(START WITH 1, INCREMENT BY 1), color KOLOR)";

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine("EmbeddedDriver");
        Connection con = DerbyUtil.connectEmbeddedDB("C:/Przyklady/r097_types",
                ";create=true");
        Statement stat = null;
        try {
            stat = con.createStatement();
            stat.addBatch(createTypeKolor);
            stat.addBatch(createTable);
            stat.executeBatch();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        PreparedStatement stmt = null;
        String insertSQL = "INSERT INTO colors (id,  color) values (DEFAULT,?)";
        try {
            stmt = con.prepareStatement(insertSQL);
            stmt.setObject(1, new Kolor(1234567));
            stmt.executeUpdate();
            stmt.clearParameters();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
        String selectSQL = "SELECT  * FROM colors";
        PreparedStatement pstm = null;
        ResultSet rs = null;
        int id = -1;
        int col = -1;
        try {
            pstm = con.prepareStatement(selectSQL);
            rs = pstm.executeQuery();
            while (rs.next()) {
                id = rs.getInt("id");
                col = ((Kolor) rs.getObject("color")).getKolor();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        System.out.println("id: " + id);
        int[] cols = DerbyUtil.IntToRGB(col);
        DerbyUtil.print(cols);
        DerbyUtil.close(rs);
        DerbyUtil.close(stat);
        DerbyUtil.close(stmt);
        DerbyUtil.close(pstm);
        DerbyUtil.close(con);
        DerbyUtil.shutdownDerbyEngine();
    }
}

A oto wynik uruchomienia klasy.

id: 1
[18, 214, 135]

Utworzenie pliku archiwum

W sposób podany w innej recepturze w pliku typy.jar umieszczamy klasę Kolor w pakiecie types. Plik umieszczamy w folderze C:/Przykłady.

Plik *.jar nie powinien zawierać plików z pakietów java i javax oraz innych klas JDK.

Klasy te nawet jeśli zostaną spakowane w archiwum i tak nie będą z niego załadowane.

Jeśli poza powyższymi klasami do uruchomienia twojej klasy potrzebne są klasy z innych bibliotek Java w plikach *.jar to problem można rozwiązać przez:

  • rozpakowanie archiwum takiej biblioteki i dodanie do swojego projektu potrzebnych klasy, o ile prawa autorskie na to pozwalają
  • umieszczenie plików *.jar tych bibliotek w bazie Derby podobnie jak to uczynisz z własną aplikacją. System Derby pozwala na załadowanie dowolnej liczby klas z dowolnej liczby archiwów z dowolnej liczby schematów i baz danych (w obrębie systemu)
  • dostarczenie użytkownikowi bibliotek i zadbanie, żeby znalazły się one u ścieżce klas użytkownika

Utworzenie bazy danych

Tworzymy bazę danych, która będzie zawierać plik typy.jar. Wykorzystamy klasę R097_CREATE_DB.

Klasa R097_CREATE_DB
package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_CREATE_DB {
    private static final String baza = "C:/Przyklady/baza_typy";

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine(DerbyUtil.embdriver);
        Connection conn = DerbyUtil.connectEmbeddedDB(baza, ";create=true");
        DerbyUtil.close(conn);
        DerbyUtil.shutdownEmbeddedDB(baza);
        DerbyUtil.shutdownDerbyEngine();
    }
}

Po uruchomieniu klasy w folderze C:/Przyklady pojawia się baza danych (Rys. 290):

Baza danych baza_typy
Rys. 290. Baza danych ‘baza_typy’

Struktura bazy przedstawiona jest na obrazku (Rys. 291):

Struktura bazy danych
Rys. 291. Struktura bazy danych

Instalowanie pliku archiwum w bazie danych

Do bazy danych można dodać więcej niż jeden plik archiwum.

Aby zainstalować plik archiwum w bazie danych musimy użyć procedury systemowej bazy danych
SQLJ.INSTALL_JAR, która może być uruchomiona przez obiekt JDBC – polecenie wywołujące klasy CallableStatement.

Składnia polecenia dla naszego przykładu jest następująca:

CALL SQLJ.INSTALL_JAR('C:\Przyklady\typy.jar', APP.typy, 0)

CALL – oznacza ‘wywołaj’

SQLJ – to biblioteka zewnętrzna do unifikacji SQL z Javą – użyta przez Derby.

INSTALL_JAR – to jedna z procedur tej biblioteki.

Argumenty metody oznaczają kolejno:

  • ścieżka do pliku *.jar, który będzie umieszczony w bazie
  • nazwa archiwum w bazie poprzedzona nazwą schematu. O schematach powiemy więcej przy omawianiu budowy bazy danych. Tutaj powiemy tylko, że schemat jest czymś w rodzaju folderu, czy kontenera, kory zawiera tabele, procedury, jary, etc. Schematów w bazie może być wiele. Jeśli nie utworzysz schematu, w bazie jest obecny tylko jeden schemat (domyślny)- APP
  • tak więc archiwum zostanie umieszczone w schemacie APP pod nazwą typy
  • można wstawić 0 lub 1. ‘1’ oznacza że istnieje plik deskryptora. ‘0’ oznacza, że pliku nie ma. W Derby umownie wstawia się ‘0’ – chociaż można wstawić ‘1’, ponieważ ten argument i tak jest ignorowany przez Derby
Dodanie pliku archiwum do bazy danych

Użyjemy metody DerbyUtil.installJar. Po uruchomieniu klasy R097_INSTALL_JAR w bazie danych pojawi się dodatkowy folder jar (Rys. 292):

Folder 'jar' w bazie danych
Rys. 292. Folder ‘jar’ w bazie danych
Klasa R097_INSTALL_JAR.java
package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_INSTALL_JAR {
    private static final String baza = "C:/Przyklady/baza_typy";
    public static final String jarPath = "C:/Przyklady/typy.jar";
    public static final String jarName = "APP.typy";
    public static final int deploy = 0;

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine(DerbyUtil.embdriver);
        Connection conn = DerbyUtil.connectEmbeddedDB(baza, "");
        DerbyUtil.installJar(conn, jarPath, jarName, deploy);
        DerbyUtil.close(conn);
        DerbyUtil.shutdownEmbeddedDB(baza);
        DerbyUtil.shutdownDerbyEngine();
    }
}
Ustawienie archiwum w ścieżce klas bazy danych

Musimy poinformować bazę o ścieżce do pliku archiwum. Możemy to uczynić przez wywołanie procedury systemowej:

CALL SYSCS.SYSCS_SET_DATABASE_PROPERTY

i uruchomienie jej przy użyciu obiektu JDBC klasy polecenia wywołującego CallableStatement.

Składnia procedury jest następująca:

CALL SYSCS.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.baza1')

Procedura ma dwa argumenty:

  • nazwa ustawianej właściwości bazy danych
  • nazwa pliku archiwum w bazie danych poprzedzona nazwą schematu. Jeśli ustawiamy ścieżkę do więcej niż jednego archiwum – nazwy archiwów oddzielamy ‘:’, np. ‘APP.b1:APP.b2’

Do ustawienia właściwości u użyjemy metody DerbyUtil.setDBProperty:

public static boolean setDBProperty(Connection conn,
   String property, String value) {
	CallableStatement cstat = null;
	boolean done = false;
	try{
	    cstat = conn.prepareCall(
		"CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY(?,?)");
		cstat.setString(1, property);
		cstat.setString(2, value);
		done = cstat.execute();
	} catch (SQLException e){
		e.printStackTrace();
	} finally{
		close(cstat);
	}
	return done;
}

Ustawienia dokonamy przez uruchomienie klasy R097_SET_PROP.java.

Klasa R097_SET_PROP.java
package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_SET_PROP {
    private static final String baza = "C:/Przyklady/baza_typy";
    public static final String jarName = "APP.typy";
    public static final String property = "derby.database.classpath";
    public static final String sql1 = "VALUES SYSCS_UTIL.SYSCS_GET_DATABASE_PROPERTY('derby.database.classpath')";

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine(DerbyUtil.embdriver);
        Connection conn = DerbyUtil.connectEmbeddedDB(baza, "");
        DerbyUtil.setDBProperty(conn, property, jarName);
        Statement stat = null;
        ResultSet rs = null;
        try {
            stat = conn.createStatement();
            rs = stat.executeQuery(sql1);
            rs.next();
            System.out.println(rs.getString(1));
        } catch (SQLException e) {
            e.printStackTrace();
        }
        DerbyUtil.close(rs);
        DerbyUtil.close(stat);
        DerbyUtil.close(conn);
        DerbyUtil.shutdownEmbeddedDB(baza);
        DerbyUtil.shutdownDerbyEngine();
    }
}

Po uruchomieniu tej klasy mamy ustawioną ścieżkę klas do pliku archiwum w bazie

APP.typy

Ścieżka musi być ustawiana przy każdym ‘odpaleniu’ bazy danych.

h4 class=”derby”>Ładowanie klas w obrębie bazy danych

Tworzymy nową klasę R097_GET_CLASS.

Klasa R097_GET_CLASS
package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_GET_CLASS {
    public static final String createTypeKolor = "CREATE TYPE kolor "
            + "EXTERNAL NAME 'types.Kolor' LANGUAGE JAVA";
    public static final String createTable = "CREATE TABLE colors("
            + "id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS "
            + "IDENTITY(START WITH 1, INCREMENT BY 1), color KOLOR)";
    public static final String jarPath = "C:/Przyklady/typy.jar";
    public static final String jarName = "APP.typy";
    public static final int deploy = 0;
    public static final String property = "derby.database.classpath";

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine("EmbeddedDriver");
        Connection conn = DerbyUtil.connectEmbeddedDB("C:/Przyklady/baza_types",
                ";create=true");
        DerbyUtil.installJar(conn, jarPath, jarName, deploy);
        DerbyUtil.setDBProperty(conn, property, jarName);
        Statement stat = null;
        try {
            stat = conn.createStatement();
            stat.addBatch(createTypeKolor);
            stat.addBatch(createTable);
            stat.executeBatch();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        PreparedStatement stmt = null;
        String insertSQL = "INSERT INTO colors (id,  color) values (DEFAULT,?)";
        try {
            stmt = conn.prepareStatement(insertSQL);
            //stmt.setObject(1, new Kolor(1234567));
            stmt.setObject(1, new types.Kolor(1234567));
            stmt.executeUpdate();
            stmt.clearParameters();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
        String selectSQL = "SELECT  * FROM colors";
        PreparedStatement pstm = null;
        ResultSet rs = null;
        int id = -1;
        int col = -1;
        try {
            pstm = conn.prepareStatement(selectSQL);
            rs = pstm.executeQuery();
            while (rs.next()) {
                id = rs.getInt("id");
                //col = ((Kolor) rs.getObject("color")).getKolor();
                col = ((types.Kolor) rs.getObject("color")).getKolor();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        System.out.println("id: " + id);
        int[] cols = DerbyUtil.IntToRGB(col);
        DerbyUtil.print(cols);
        DerbyUtil.close(rs);
        DerbyUtil.close(stat);
        DerbyUtil.close(stmt);
        DerbyUtil.close(pstm);
        DerbyUtil.close(conn);
        DerbyUtil.shutdownDerbyEngine();
    }
}

Wszystko jest podobnie, ale typ KOLOR jest tworzony w oparciu o klasę types.Kolor.

Przy próbie uruchomienia kodu wyrzucany jest błąd.

Jak widać mamy tutaj dwa odniesienia do klasy Kolor:

  • aderby.specyf.jars.Kolor
  • types.Kolor

Typ types.Kolor jest już widoczny w obrębie bazy danych. SQL protestuje przed próbą umieszczenia obiektu klasy aderby.specyf.jars.Kolor w polu types.Kolor. Rozwiązanie jest oczywiste:

  • w aplikacji klasa Kolor powinna być w takim samym pakiecie jak klasa w pliku *.jar.

Gdy klasę Kolor umieścimy (dodatkowo – była już używana) w pakiecie types i w kodzie aplikacji odwołamy się do typu types.Kolor to otrzymamy prawidłowy wynik.

W kodzie klasy R097_GET_CLASS trzeba więc dokonać dwu zmian:

   //stmt.setObject(1, new Kolor(1234567));
     stmt.setObject(1, new types.Kolor(1234567));
//col = ((Kolor) rs.getObject("color")).getKolor();
col = ((types.Kolor) rs.getObject("color")).getKolor();
id: 1
[18, 214, 135]

Wymiana pliku archiwum w bazie danych

Plik w archiwum można wymienić używając procedury SQLJ:

CALL SQLJ.REPLACE_JAR('C:/Przyklady/new_jar.jar','APP.types')

gdzie:

  • C:\Przyklady\new_jar.jar’ – jest adresem do pliku *,jar, który ma się znaleźć w bazie danych w miejscu aktualnego pliku/li>
  • ‘APP.types’ – to nazwa jaru istniejącego w bazie danych, który ma zostać podmieniony.

Po wykonaniu operacji w bazie pod nazwą wewnętrzną ‘APP.baza1’ będzie się znajdował ‘new_jar.jar’.

Możemy użyć metody klasy DerbyUtil:

public static boolean replaceJar(Connection conn, String newJar,
		String oldJarDBName) {
    CallableStatement cstat = null;
    boolean done = false;
    try{
      cstat = conn.prepareCall("CALL SQLJ.REPLACE_JAR(?,?)");
      cstat.setString(1, newJar);
      cstat.setString(2, oldJarDBName);
      done = cstat.execute();
	} catch (SQLException e){
		e.printStackTrace();
	} finally{
		DerbyUtil.close(cstat);
	}
	return done;
}

Przykład w klasie R097_REPLACE.java.

R097_REPLACE.java
package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_REMOVE {
    private static final String baza = "C:/Przyklady/baza_types";
    public static final String embdriver = "EmbeddedDriver";
    public static final String newJar = "C:/Przyklady/typy1.jar";
    public static final String oldJarDBName = "APP.typy";

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine(embdriver);
        Connection conn = DerbyUtil.connectEmbeddedDB(baza);
        DerbyUtil.removeJar(conn, oldJarDBName, 0);
        DerbyUtil.close(conn);
        DerbyUtil.shutdownEmbeddedDB(baza);
        DerbyUtil.shutdownDerbyEngine();
    }
}

Następuje podmiana pliku typy.jar na
typy1.jar

Odinstalowanie pliku archiwum w bazie danych

Plik *.jar znajdujący się w bazie danych można usunąć używając procedury SQLJ:

CALL SQLJ.REMOVE_JAR('APP.baza1')

‘APP.baza1’ oznacza tutaj plik *.jar do usunięcia.

Można użyć metody:

public static boolean removeJar(Connection conn, String jarDBName,
		int undeploy) {
   CallableStatement cstat = null;
   boolean done = false;
   try{
  	cstat = conn.prepareCall("CALL SQLJ.REMOVE_JAR(?,?)");
	cstat.setString(1, jarDBName);
	cstat.setInt(2, undeploy);
	done = cstat.execute();
} catch (SQLException e){
	e.printStackTrace();
} finally{
	DerbyUtil.close(cstat);
}
return done;
}

Przykład w klasie R097_REMOVE.java. Usuwamy plik typy1.jar z bazy danych.

R097_REMOVR.java
span class="s0">package aderby.specyf.jars;

import aderby.DerbyUtil;

import java.sql.*;

public class R097_REMOVE {
    private static final String baza = "C:/Przyklady/baza_types";
    public static final String embdriver = "EmbeddedDriver";
    public static final String newJar = "C:/Przyklady/typy1.jar";
    public static final String oldJarDBName = "APP.typy";

    public static void main(String[] args) {
        DerbyUtil.startDerbyEngine(embdriver);
        Connection conn = DerbyUtil.connectEmbeddedDB(baza);
        DerbyUtil.removeJar(conn, oldJarDBName, 0);
        DerbyUtil.close(conn);
        DerbyUtil.shutdownEmbeddedDB(baza);
        DerbyUtil.shutdownDerbyEngine();
    }
}

Plik jar został usunięty z bazy danych chociaż pozostał folder ‘jar’ w którym ten plik się mieścił.

Dynamika zmian

Po zainstalowaniu lub wymianie pliku archiwum w bazie danych nie ma potrzeby zamknięcia i ponownego uruchomienia bazy danych jeżeli spełniony jest warunek:

  • jeżeli ścieżka klas bazy danych jest już ustawiona prawidłowo, a zainstalowanie nowego archiwum lub wymiana pliku archiwum nie wymaga zmiany w tej ścieżce (czyli nie ma potrzeby zmiany derby.database.classpath)

Przy zmianie derby.database.classpath i restarcie – wszystkie klasy w jarach są ładowane od nowa, nawet z tych jarów, które nie były zmieniane.

Pliki do ściągnięcia

Aktualny (tworzony narastająco) plik module-info.java

Aktualny (tworzony narastająco) plik DerbyUtil.java

Pliki tworzone narastająco zastępują poprzednie pliki o tej samej nazwie i działają dla wszystkich wcześniej opublikowanych przykładów we wszystkich wpisach w projekcie. W przypadku pliku module-info.java może być potrzebne skreślenie niepotrzebnych wpisów.

types.zip (zestaw klas wymienionych w tekście)