Spring und Stored Procedure mit User-defined Types (Tutorial)

Die Stored Procedure "searchPersons" mit User-defined Types (UDT)

Dieser Beitrag ist Teil einer (Tutorial-) Serie über die Einführung in das Spring Framework und beschreibt die Nutzung von Stored Procedures mit User-definied Types (UDT) und dem Spring Framework.

Die Struktur des Projektes

Die verwendeten Frameworks und Werkzeuge sind hier beschrieben. In diesem Beispiel wird mit Spring JDBC auf eine Stored Procedure zugegriffen. Dabei werden benutzerdefinierte strukturierte Datentypen, sogenannte User-defined Types (UDT), verwendet. Diese komplexen SQL-Typen sind mit der Version 2.1 zum JDBC Standard hinzugekommen. In diesem Beitrag wird gezeigt, wie mit Spring JDBC das Mapping dieser UDTs auf Java-Klassen durchgeführt werden kann.

Im folgendem Bild sind die benötigten Bibliotheken dargestellt.

Die benötigten Bibliotheken (Dependencies) für das Projekt "test-spring-jdbc"

Die benötigten Bibliotheken (Dependencies) für das Projekt 'test-spring-jdbc' (© Frank Rahn)

Die Literaturempfehlungen für dieses Beispiel

Die User-defined Types (UDT)

Die benutzerdefinierten Datentypen (user-defined Types) wurden im SQL-Standard 2003 (ANSI/ISO/IEC 9075, SQL3) aufgenommen und zum JDBC Standard mit der Version 2.1 hinzugefügt. Die folgende Ziele wurden mit den benutzerdefinierten Datentypen verfolgt:

  • Komplexe Datentypen
    Mit den komplexen Datentypen sollen Objekte der realen Welt abgebildet werden. Dazu wurden Konzepte (Strukturierung, Kapselung, …) aus der Objektorientierung übernommen.
CREATE TYPE Vector AS (x INTEGER, y INTEGER, z INTEGER);
  • Strenge Typisierung
    Durch die strenge Typisierung sollen semantische unsinnige Vergleich vermieden werden, die nur aufgrund von gleichen Datentypen möglich waren (z. B. INTEGER, Vergleich oder Zuweisung von Gehalt und Hausnummer oder Kundennummer). Dazu konnten mit den DISTINCT-Datentypen getypte und benannte Varianten der Standarddatentypen erzeugt werden.
CREATE TYPE Gehalt AS DECIMAL(10,2) FINAL;

Die benutzerdefinierten Datentypen werden insbesondere bei den Stored Procedures / Functions zum Typisieren der Parameter verwendet.

CREATE PROCEDURE f(x IN Vector) ... ;

Zusätzlich können diese benutzerdefinierten Datentypen in Tabellen verwendet werden.

Das Anlegen und Einrichten der Datenbank für dieses Beispiel

Das Anlegen einer Datenbank und das Erzeugen der Stored Procedure wurden in einzelne Beiträge ausgelagert.

Den Oracle-JDBC-Treiber im lokalen Maven Repository zu Verfügung stellen

Der Oracle-Treiber für JDBC in der benötigten Oracle Version 11g ist im Central Maven Repository nicht vorhanden. Daher sind folgende Schritte notwendig:

  1. Download des Treibers von Oracle in der Version 11.2.0.3 (ojdbc6.jar für JDK 1.6)
    Ist nur über eine Anmeldung bzw. Registrierung bei Oracle möglich!
  2. Kopieren der Datei in das Verzeichnis src/oracle
  3. Ausführen des Skripts install.sh
$ cd src/oracle/
$ ls
install.sh  ojdbc6.jar
$ bash install.sh 
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-install-plugin:2.3.1:install-file (default-cli) @ standalone-pom ---
[INFO] Installing ojdbc6.jar to /m2_repo/com/oracle/ojdbc6/11.2.0.3/ojdbc6-11.2.0.3.jar
[INFO] Installing /tmp/mvninstall6728251.pom to /m2_repo/com/oracle/ojdbc6/11.2.0.3/ojdbc6-11.2.0.3.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.633s
[INFO] Finished at: Fri Jan 04 21:52:10 CET 2013
[INFO] Final Memory: 5M/179M
[INFO] ------------------------------------------------------------------------
$

Das Skript installiert den Treiber unter der Group-Id com.oracle und der Artifact-Id ojdbc6 im lokalen Maven Repository.

In folgendem Ausschnitt der pom.xml wird die Referenzierung des Treibers dargestellt.

    <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc6</artifactId>
        <version>11.2.0.3</version>
        <scope>provided</scope>
    </dependency>

Die Schnittstelle der Stored Procedure „searchPersons“

Der folgende verallgemeinerte SQL-Code-Ausschnitt zeigt wie die Schnittstelle der Stored Procedure aussieht. Die Prozedur nimmt die zwei Parameter p_num und p_user entgegen und liefert ein Liste (Array) von Personen zurück. Die Liste hat die Länge p_num und wird mit den Daten des aktuellen Benutzers p_user aufgefüllt.

...
CREATE PROCEDURE searchPersons ( -- Suche Personen
    p_num       IN  INTEGER,    -- Anzahl der gefundenen Personen
    p_user      IN  s_User,     -- Der aktuelle Benutzer
    p_persons   OUT a_Person    -- Eine Collection von Person
) ... ;
...

Wie konkret die Stored Procedure in einer Datenbank erzeugt wird, wird Datenbank-spezifisch in den beiden folgenden Beiträgen beschrieben.

Die Entitäten

Die Stored Procedure benötigt zwei Entitäten s_User und s_Person. Hier zunächst die verallgemeinerte SQL-Definition der beiden benutzerdefinierten Datentypen.

...
CREATE TYPE s_User AS ( -- Der aktuelle Benutzer
    id      CHAR(10),
    name    VARCHAR(30),
    dept    VARCHAR
);
...
...
CREATE TYPE s_Person AS ( -- Eine Person
    id      BIGINT,
    name        VARCHAR(30),
    salary      DECIMAL,
    dateOfBirth DATE
);
...

Diese beiden benutzerdefinierten Datentypen werden in einfache Java-Klassen überführt.

Für die Entität User werden die Datentypen CHAR und VARCHAR unabhängig ihrer Länge auf die Java-Klasse String abgebildet.

/**
 * Der aktuelle Benutzer.
 * @author Frank W. Rahn
 */
public class User {

    private String id;

    private String name;

    private String department;
...

Für die Entität s_Person werden die Datentypen BIGINT auf den Java-Type long und DECIMAL auf die Java-Klasse java.math.BigDecimal abgebildet.

/**
 * Eine Person.
 * @author Frank W. Rahn
 */
public class Person {

    private long id;

    private String name;

    private BigDecimal salary;

    private Date dateOfBirth;
...

Das Datenzugriffsobjekt

Entsprechend der Architektur aus dem Beitrag Spring mit JPA und Hibernate aus dieser (Tutorial-) Serie, wird zunächst die Schnittstelle des Datenzugriffsobjekts für die Stored Procedure searchPersons definiert.

/**
 * Die Schnittstelle des DAO für die Stored Procedure "searchPersons".
 * @author Frank W. Rahn
 */
public interface SearchPersonsDAO {

    /**
     * Suche Personen.
     * @param num die Anzahl der zu lieferenden Personen
     * @param user der aktuelle Benutzer
     * @return die Personen
     */
    Person[] searchPersons(int num, User user);

}

In der folgenden Implementierung dieses Datenzugriffsobjekts sind alle Informationen über die Stored Procedure enthalten.

  • In der Zeile 25 bis 26 werden die SQL-Namen der Stored Procedure und der drei SQL-Parameter definiert.
/**
 * Das DAO für die Stored Procedure.
 * @author Frank W. Rahn
 */
@Repository
public class StandardSearchPersonsDAO implements SearchPersonsDAO {

    /** Name der Stored Procedure. */
    private static final String NAME = "searchPersons";

    /** 1. Parameter der Stored Procedure. */
    private static final String P_NUM = "p_num";

    /** 2. Parameter der Stored Procedure. */
    private static final String P_USER = "p_user";

    /** 3. Parameter der Stored Procedure. */
    private static final String P_PERSONS = "p_persons";

    @Autowired
    private DataSource dataSource;
...
  • Die Mapper in Zeile 41 und 44 sind für die Konvertierung der benutzerdefinierten Datentypen in die entsprechenden Java-Klassen zuständig. Die Implementierung der Mapper wird im Anschluss beschrieben.
  • Die Spring-Klasse SimpleJdbcCall aus Zeile 46 stellt Funktionen für die Durchführung von Stored Procedures mit einem java.sql.CallableStatement aus dem JDBC Standard bereit. Diese Klasse hat die Eigenschaften multi-thread (nebenläufig) und stateless (zustandslos).
    Zusätzlich besitzt sie ein Fluent Interface.
  • In der Zeile 54 wird der SimpleJdbcCall erzeugt und mit der javax.sql.DataSource initialisiert. Zusätzlich wird der SQL-Name der Procedure gesetzt.
  • In der Zeile 56 wird ein spezieller SqlParameter für den benutzerdefinierten Datentypen P_USER des aktuellen Benutzers über den entsprechenden Mapper als Eingabewert (false) gesetzt.
  • In der Zeile 57 wird ein spezieller SqlParameter für den benutzerdefinierten Datentypen P_PERSONS der Liste von Personen über den entsprechenden Mapper als Rückgabewert (true) gesetzt.
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PersonsMapper personsMapper;

    private SimpleJdbcCall jdbcCall;

    /**
     * Initialisiere das DAO.
     */
    @PostConstruct
    public void initialize() {
        jdbcCall =
            new SimpleJdbcCall(dataSource).withProcedureName(NAME)
                .declareParameters(
                    userMapper.createSqlParameter(P_USER, false),
                    personsMapper.createSqlParameter(P_PERSONS, true));

        jdbcCall.compile();
    }
...

Im folgendem Code-Abschnitt wird die Methode searchPersons dargestellt – sie entspricht der Stored Procedure.

  • In den Zeilen 68 bis 70 wird eine Map mit den Eingabedaten für die Stored Procedure gefüllt.
  • In der Zeile 70 wird, über einen Mapper, die Instanz der Java-Klasse user in eine benutzerdefinierten Datentyp (java.sql.Struct) konvertiert.
  • In der Zeile 72 wird, über die Instanz der Spring-Klasse SimpleJdbcCall die Stored Procedure ausgeführt.
  • In der Zeile 74 wird, aus der Map mit den Rückgabedaten, das Ergebnis abgeholt. Das Ergebnis muss nicht mehr konvertiert werden, da beim Initialisieren dieses Datenbankzugriffsobjekts mit dem SqlParameter für die Personen ein Mapper für Rückgabe dieses Types registriert worden ist. Näheres bei der Beschreibung der Mapper weiter unten.
   /**
     * {@inheritDoc}
     * @see SearchPersonsDAO#searchPersons(int, User)
     */
    @Override
    public Person[] searchPersons(int num, User user) {
        Map<String, Object> in = new HashMap<>();
        in.put(P_NUM, num);
        in.put(P_USER, userMapper.createSqlTypeValue(user));

        Map<String, Object> out = jdbcCall.execute(in);

        return (Person[]) out.get(P_PERSONS);
    }

}

Die Mapper

Im folgendem Abschnitt werden die Mapper zwischen den Java-Klassen (UserObject) und den zugehörigen benutzerdefinierten Datentypen (JdbcType: Struct, Array, Blob, Clob, ...) beschrieben.

Zunächst wird ein abstrakter Mapper SqlParameterMapper erstellt. Diese abstrakte Klasse wird mit den beiden generischen Typparametern UserObject und JdbcType definiert. Zusätzlich werden einige Funktionalitäten des Spring Framework (Spring JDBC) verwendet:

  • org.springframework.jdbc.core.SqlReturnType
    Diese Schnittstelle wird für das Abrufen von benutzerdefinierten Datentypen aus dem Ergebnis einer Datenbankaktion verwendet.
  • org.springframework.jdbc.core.SqlTypeValue
    Diese Schnittstelle wird für das Setzen von benutzerdefinierten Datentypen in die Parameter einer Datenbankaktion verwendet.
  • org.springframework.jdbc.core.SqlParameter
    Diese Klasse wird zur Definition von Eingabeparametern von Datenbankaktionen verwendet.
  • org.springframework.jdbc.core.SqlOutParameter
    Diese Klasse wird zur Definition von Rückgabeparametern von Datenbankaktionen verwendet.

Im folgendem Code-Ausschnitt wird die Definition des abstrakten Mappers beschrieben.

  • In der Zeile 20 wird der abstrakte Mapper SqlParameterMapper mit den generischen Typparametern UserObject und JdbcType definiert (siehe oben). Zusätzlich implementiert dieser Mapper die Schnittstelle SqlReturnType.
  • In der Zeile 47 wird die abstrakte Methode definiert, die im konkrete Mapper den benutzerdefinierten Datentyp in eine Java-Klasse konvertiert.
  • In der Zeile 78 wird die abstrakte Methode definiert, die im konkrete Mapper die Instanz der Java-Klasse in den benutzerdefinierten Datentyp konvertiert. Dazu wird eine Instanz der Datanbankverbindung (Connection) benötigt. Über diese Connection werden die Datentypen erzeugt (createStruct(), createArrayOf(), createBlob(), …) und an die Datenbankverbindung gebunden.
  • In der Zeile 87 wird eine abstrakte Methode für die Erzeugung einer Beschreibung des SQL Parameters für den benutzerdefinierten Datentyp definiert.
  • Ab der Zeile 28 wird die Methode getTypevalue() der Schnittstelle SqlReturnType implementiert. Diese Methode wird von Spring JDBC aufgerufen, wenn eine Datensatz (ROW) aus dem Ergebnis (CURSOR) gelesen und ein komplexer Datentyp erwartet wird.
  • In der Zeile 32 wird ein benutzerdefinierten Datentyp von der Datenbank gelesen. Der Parameter paramIndex gibt die Nummer der Spalte, beginnend bei 1 für die erste Spalte, an.
  • In der Zeile 38 wird die Methode aufgerufen, die aus dem benutzerdefinierten Datentyp eine Instanz der Java-Klasse erzeugt.
  • Ab der Zeile 55 wird die Methode createSqltypeValue() implementiert. Diese Methode liefert eine Instanz der Schnittstelle SqlTypeValue. Diese Instanz nutzt das Spring Framework, wenn es das java.sql.PreparedStatement mit den Eingabeparametern bestückt. Dazu wird die abstrakte Klasse AbstarctSqlTypeValue des Spring Frameworks erweitert. Diese Methode wird im StandardSearchPersonsDAO in der Zeile 70 verwendet und die erzeugte Instanz wird, wenn das Spring Framework das java.sql.PreparedStatement erzeugt hat, bei jdbcCall.execute() aufgerufen.
  • Die Implementierung in Zeile 63 deligiert die Verarbeitung aus der Instanz SqlTypeValue an die Methode createSqlValue(). Diese Methode erzeugt den benutzerdefinierten Datentyp.
/**
 * Ein abstrakter Mapper, zwischen den User Objekt und dem JDBC Datenbankobjekt
 * mappt.
 * @author Frank W. Rahn
 * @param <UserObject> das Userobjekt
 * @param <JdbcType> das JDBC-Datenbankobjekt (z. B. {@link Struct} ...)
 */
public abstract class SqlParameterMapper<UserObject, JdbcType> implements
    SqlReturnType {

    /**
     * {@inheritDoc}
     * @see SqlReturnType#getTypeValue(CallableStatement, int, int, String)
     */
    @Override
    public final Object getTypeValue(CallableStatement cs, int paramIndex,
        int sqlType, String typeName) throws SQLException {

        @SuppressWarnings("unchecked")
        final JdbcType jdbcType = (JdbcType) cs.getObject(paramIndex);

        if (jdbcType == null) {
            return null;
        }

        return createObject(jdbcType);
    }

    /**
     * Konvertiere das JDBC-Datenbankobjekt in ein Userobjekt.
     * @param jdbcType JDBC-Datenbankobjekt
     * @return das neue Userobjekt
     * @throws SQLException falls ein Fehler bei den Datenbankzugriffen auftritt
     */
    protected abstract UserObject createObject(JdbcType jdbcType)
        throws SQLException;

    /**
     * Erzeuge einen {@link SqlTypeValue} Objekt aus dem Userobjekt.
     * @param userObject das Userobjekt
     * @return das {@link SqlTypeValue} Objekt
     */
    public final SqlTypeValue createSqlTypeValue(final UserObject userObject) {
        return new AbstractSqlTypeValue() {

            /**
             * @see AbstractSqlTypeValue#createTypeValue(Connection, int,
             * String)
             */
            @Override
            protected final Object createTypeValue(Connection con, int sqlType,
                String typeName) throws SQLException {

                return createSqlValue(con, userObject);
            }
        };
    }

    /**
     * Konvertiere das Userobjekt in ein JDBC-Datenbankobjekt.
     * @param con die Datenbankverbindung
     * @param userObject das Userobjekt
     * @return das neue JDBC-Datenbankobjekt
     * @throws SQLException falls ein Fehler bei den Datenbankzugriffen auftritt
     */
    protected abstract JdbcType createSqlValue(Connection con,
        UserObject userObject) throws SQLException;

    /**
     * Erzeuge einen {@link SqlParameter} für diesen Mapper.
     * @param paramaterName der Name des Parameters
     * @param outParameter Ist der Parameter ein Ausgabe?
     * @return der Parameter.
     */
    public abstract SqlParameter createSqlParameter(String paramaterName,
        boolean outParameter);

}

Die Java-Klasse de.rahn.jdbc.call.entity.User wird durch die JDBC-Klasse java.sql.Struct auf den benutzerdefinierten Datentyp S_USER abgebildet.

  • In der Zeile 20 wird dieser konkrete Mapper mit der Java-Klassen User und der JDBC-Klasse Struct parametrisiert.
  • In der Zeile 31 wird ein neuer User aus einem Datensatz erzeugt.
  • In der Zeile 33 wird aus dem Datentyp die Werte ausgelesen. Die Datenbank-spezifische Implementierung der JDBC-Klasse kann an dieser Stelle einen Zugriff auf die Datenbank durchführen.
  • In den Zeilen 34 bis 36 werden die Werte des Datentypes in den neuen User geschrieben. In dieser Implementierung wird dazu die Technik der Instance Initializer (seit Java 1.1; Java SE 7 Edition of Java Language Specification: §8.6. Instance Initializers) verwendet.
  • In den Zeilen 50 bis 53 wird mit Hilfe einer bestehenden Datenbankverbindung eine Instanz der JDBC-Klasse S_USER erzeugt und mit den Werten aus dem User gefüllt.
  • In den Zeilen 63 bis 67 wird die Beschreibung des SQL Parameters für diese benutzerdefinierten Datentypen angelegt. Sie besteht aus der JDBC-Typnummer, den Namen des benutzerdefinierten Datentyps und dem Parameternamen bei der Verwendung in einer Eingabe- bzw. Rückgabeliste. Bei einem Rückgabeparameter wird eine Instanz diese Klasse als Handler für die Erstellung des Ergebnisses registriert.
    Ein Beispiel der Verwendung findet sich im StandardSearchPersonsDAO in Zeile 57.
/**
 * Mapping zwischen einer {@link Struct} und einem {@link User}.
 * @author Frank W. Rahn
 */
@Component
public class UserMapper extends SqlParameterMapper<User, Struct> {

    /** Der SQL-Name des JDBC-Typnamens. */
    private static final String TYPE_NAME = "S_USER";

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createObject(Object)
     */
    @Override
    protected User createObject(final Struct struct) throws SQLException {
        return new User() {
            {
                Object[] attributes = struct.getAttributes();
                setId((String) attributes[0]);
                setName((String) attributes[1]);
                setDepartment((String) attributes[2]);
            }
        };
    }

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createSqlValue(Connection, Object)
     */
    @Override
    protected Struct createSqlValue(Connection con, User user)
        throws SQLException {

        return con
            .createStruct(
                TYPE_NAME,
                new Object[] { user.getId(), user.getName(),
                    user.getDepartment() });
    }

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createSqlParameter(String, boolean)
     */
    @Override
    public SqlParameter createSqlParameter(String paramaterName,
        boolean outParameter) {
        if (outParameter) {
            return new SqlOutParameter(paramaterName, STRUCT, TYPE_NAME, this);
        } else {
            return new SqlParameter(paramaterName, STRUCT, TYPE_NAME);
        }
    }

}

Die Java-Klasse de.rahn.jdbc.call.entity.Person wird, wie die Klasse User, auf die JDBC-Klasse Struct abgebildet. Der restliche Aufbau der Klasse ähnelt der Klasse User.

/**
 * Mapping zwischen einer {@link Struct} und einem {@link Person}.
 * @author Frank W. Rahn
 */
@Component
public class PersonMapper extends SqlParameterMapper<Person, Struct> {

    /** Der SQL-Name des JDBC-Typnamens. */
    private static final String TYPE_NAME = "S_PERSON";

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createObject(Object)
     */
    @Override
    protected Person createObject(final Struct struct) throws SQLException {
        return new Person() {
            {
                Object[] attributes = struct.getAttributes();

                BigDecimal d = (BigDecimal) attributes[0];
                if (d != null) {
                    setId(d.longValue());
                }

                setName((String) attributes[1]);
                setSalary((BigDecimal) attributes[2]);
                setDateOfBirth((Date) attributes[3]);
            }
        };
    }

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createSqlValue(Connection, String, Object)
     */
    @Override
    protected Struct createSqlValue(Connection con, Person person)
        throws SQLException {

        return con.createStruct(TYPE_NAME, new Object[] { person.getId(),
            person.getName(), person.getSalary(), person.getDateOfBirth() });
    }

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createSqlParameter(String, boolean)
     */
    @Override
    public SqlParameter createSqlParameter(String paramaterName,
        boolean outParameter) {
        if (outParameter) {
            return new SqlOutParameter(paramaterName, STRUCT, TYPE_NAME, this);
        } else {
            return new SqlParameter(paramaterName, STRUCT, TYPE_NAME);
        }
    }

}

Die Liste der Java-Klasse de.rahn.jdbc.call.entity.Person wird auf die JDBC-Klasse java.sql.Array abgebildet.

  • In den Zeilen 31, 48 und 65: Dieser Mapper verwendet für das Behandeln einer Person den spezifischen Mapper für die Person.
  • In der Zeile 39 werden die gelesenen Werte aus der Datenbank aus der JDBC-Klasse gelesen.
  • In der Zeile 68 wird der benutzerdefinierten Datentyp für ein Liste von Personen erzeugt.
/**
 * Mapping zwischen einem {@link Array} und einer Liste von {@link Person}s.
 * @author Frank W. Rahn
 */
@Component
public class PersonsMapper extends SqlParameterMapper<Person[], Array> {

    /** Der SQL-Name des JDBC-Typnamens. */
    private static final String TYPE_NAME = "A_PERSON";

    /**
     * Der Mapper für eine Peson.
     */
    @Autowired
    private PersonMapper mapper;

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createObject(Object)
     */
    @Override
    protected Person[] createObject(Array array) throws SQLException {
        Object[] values = (Object[]) array.getArray();

        if (values == null || values.length == 0) {
            return null;
        }

        Person[] persons = new Person[values.length];

        for (int i = 0; i < values.length; i++) {
            persons[i] = mapper.createObject((Struct) values[i]);
        }

        return persons;
    }

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createSqlValue(Connection, String, Object)
     */
    @Override
    protected Array createSqlValue(Connection con, Person[] persons)
        throws SQLException {

        Object[] values = new Object[persons.length];

        for (int i = 0; i < persons.length; i++) {
            values[i] = mapper.createSqlValue(con, persons[i]);
        }

        return con.createArrayOf(TYPE_NAME, values);
    }

    /**
     * {@inheritDoc}
     * @see SqlParameterMapper#createSqlParameter(String, boolean)
     */
    @Override
    public SqlParameter createSqlParameter(String paramaterName,
        boolean outParameter) {
        if (outParameter) {
            return new SqlOutParameter(paramaterName, ARRAY, TYPE_NAME, this);
        } else {
            return new SqlParameter(paramaterName, ARRAY, TYPE_NAME);
        }
    }

}

Das Arbeiten mit Stored Procedure mit User-defined Types wurden in diesem Beitrag trotz der Verwendung von Spring JDBC sehr nahe an JDBC und Oracle beschrieben.

Mittlerweile gibt es auch eine Implementierung aus dem Spring Data Projekt:

Die Anwendung mit Logging

Die Anwendung ist wie in dem Beispiel Spring an einem einfachem Beispiel aufgebaut. Es ändert sich nur die Anwendung, die XML Konfiguration de/rahn/app/application.xml und der Starter bleiben gleich. Die Änderung an der Application sind hier dargestellt.

/**
 * Die Anwendung zum Aufrufen des Taschenrechners.
 * @author Frank W. Rahn
 */
@Component
public class Application implements Runnable {

    private static final Logger logger = getLogger(Application.class);

    @Autowired(required = true)
    private SearchPersonsDAO searchPersonsDAO;

    /**
     * {@inheritDoc}
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        User user = new User();
        user.setId("4711");
        user.setName(System.getProperty("user.name"));
        user.setDepartment("Development");

        // Aufruf des Taschenrechners
        Person[] persons = searchPersonsDAO.searchPersons(15, user);

        logger.info("Ergebnis = {}", (Object) persons);
    }

}

Die XML Konfigurationen zum Einstieg in die Anwendung muß um die Datenbank- und die Transaktionsdefinitionen erweitert werden.

  • In der Zeile 21 und 22 wird auf die XML Konfiguration mit den Datenbank- und die Transaktionsdefinitionen verwiesen.
  • In der Zeile 23 wird auf die XML Konfiguration des JDBC-Calls verwiesen.
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für die Anwendungen.
    </description>

    <!-- Enabling den AspectJ Support -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <!-- Die projektspezifischen Konfigurationen laden -->
    <import resource="classpath:META-INF/spring/db.xml" />
    <import resource="classpath:META-INF/spring/tx.xml" />
    <import resource="classpath:/de/rahn/jdbc/call/call.xml" />
    <import resource="classpath:/de/rahn/app/application.xml" />

</beans>

Die XML Konfiguration des JDBC-Calls.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für die JDBC-Calls.
    </description>

    <!-- Scanne das Package nach Spring Beans -->
    <context:component-scan base-package="de.rahn.jdbc.call" />

</beans>

In der folgenden XML Konfiguration wird die Datenbankverbindung definiert.

  • In der Zeilen 19 bis 24 werden die Definitionen für den Datenbankzugriff vorgenommen. Dabei wird der JDBC Datenbanktreiber über Properties konfiguriert.
  • In der Zeile 28 oder 32 wird die jeweilige Properties-Datei durch einen PropertyPlaceholderConfigurer geladen und der Spring Konfiguration zu Verfügung gestellt. Damit können die Properties am Datenbanktreiber ersetzt werden.
  • In den Zeilen 26 und 30 wird die Anweisung zum Laden der Properties jeweils einem Profile zu geordnet. Dadurch kann beim Programmstart mit setzen eines System-Properties -Dspring.profiles.active="Oracle" gesteuert werden, welche Datenbank genutzt wird.
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für die Datenbank.
    </description>

    <!-- Treiber zur Datenbank -->
    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource"
        p:driverClassName="${jdbc.driverClassName}"
        p:url="${jdbc.url}"
        p:username="${jdbc.username}"
        p:password="${jdbc.password}" />

    <beans profile="PostgreSQL">
        <!-- Property-Configurer Definitions -->
        <context:property-placeholder location="classpath:META-INF/postgresql.properties" />
    </beans>
    <beans profile="Oracle">
        <!-- Property-Configurer Definitions -->
        <context:property-placeholder location="classpath:META-INF/oracle.properties" />
    </beans>

</beans>

In der folgenden XML Konfiguration wird nur die Transaktionsdefinitionen vorgenommen.

  • In Zeile 19 wird ein Transaktionmanager definiert, der die Transaktion der JDBC Datenbank verwendet.
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für die Datenbank.
    </description>

    <!-- Einen Transaktionmanager erzeugen -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
        p:dataSource-ref="dataSource" />

    <!-- Das Verwenden von Annotationen für die Transaktionen ermöglichen -->
    <tx:annotation-driven proxy-target-class="true" />

</beans>

In der folgenden Klasse wurde eine Erweiterung vorgenommen, damit immer das Profile für die Oracle Datenbank aktiv ist.

  • In der Zeile 17 wird der ClassPathXmlApplicationContext mit dem letztem Parameter angewiesen, die Konfiguration noch nicht zu verarbeitet.
  • In der Zeile 19 bis 20 wird über das Environment das aktive Profil gesetzt.
  • In der Zeile 21 wird der ClassPathXmlApplicationContext angewiesen die Konfiguration zu verarbeiten.
/**
 * Die Klasse zum Starten der Anwendung.
 * @author Frank W. Rahn
 */
public class Starter {

    /**
     * @param args die Argumente der Anwendung
     */
    public static void main(String[] args) {
        // Initialisierung von Spring
        ApplicationContext ctx =
            new ClassPathXmlApplicationContext(
                new String[] { "/META-INF/spring/context-app.xml" }, false);

        ((ClassPathXmlApplicationContext) ctx).getEnvironment()
            .setActiveProfiles("Oracle");
        ((ClassPathXmlApplicationContext) ctx).refresh();

        // Aufruf der Anwendung
        Runnable service = ctx.getBean("application", Runnable.class);
        service.run();
    }

}

In der folgenden Properties-Datei werden die Datenbank-spezifischen Einstellungen vorgenommen. Für jede Datenbank ist eine eigene Datei anzulegen. Bitte dementsprechend anpassen!

# Properties file with JDBC-related settings.
 
jdbc.driverClassName=oracle.jdbc.OracleDriver oder org.postgresql.Driver
jdbc.url=jdbc:oracle:thin:@//localhost:1521/orcl oder jdbc:postgresql:TEST_SPRING_JDBC
jdbc.username=...
jdbc.password=...
jdbc.testquery=SELECT 1 FROM dual oder SELECT 1

Nachfolgend wird noch die Konsolenausgabe dargestellt, wenn die Anwendung ausgeführt wird.

INFO : de.rahn.app.Application - Ergebnis = [Person [id=1, name=frank, salary=314, dateOfBirth=2013-11-02 17:22:01.0], Person [id=2, name=frank, salary=628, dateOfBirth=2013-11-02 17:22:01.0], Person [id=3, name=frank, salary=942, dateOfBirth=2013-11-02 17:22:01.0], Person [id=4, name=frank, salary=1257, dateOfBirth=2013-11-02 17:22:01.0], Person [id=5, name=frank, salary=1571, dateOfBirth=2013-11-02 17:22:01.0], Person [id=6, name=frank, salary=1885, dateOfBirth=2013-11-02 17:22:01.0], Person [id=7, name=frank, salary=2199, dateOfBirth=2013-11-02 17:22:01.0], Person [id=8, name=frank, salary=2513, dateOfBirth=2013-11-02 17:22:01.0], Person [id=9, name=frank, salary=2827, dateOfBirth=2013-11-02 17:22:01.0], Person [id=10, name=frank, salary=3142, dateOfBirth=2013-11-02 17:22:01.0], Person [id=11, name=frank, salary=3456, dateOfBirth=2013-11-02 17:22:01.0], Person [id=12, name=frank, salary=3770, dateOfBirth=2013-11-02 17:22:01.0], Person [id=13, name=frank, salary=4084, dateOfBirth=2013-11-02 17:22:01.0], Person [id=14, name=frank, salary=4398, dateOfBirth=2013-11-02 17:22:01.0], Person [id=15, name=frank, salary=4712, dateOfBirth=2013-11-02 17:22:01.0]]

Der Quellcode und Download des Beispiels

Quellcode ansehen bei GitHub:
Spring und JDBC

Download einer ZIP-Datei von GitHub:
Spring und JDBC

Die Maven Befehle

Eclipse Konfiguration neu erzeugen: $ mvn eclipse:clean eclipse:eclipse

Anwendung bauen: $ mvn clean install

Anwendung ausführen: $ mvn exec:java

Update 29.02.2014: Unverständliche Exception

Falls die Exception AbstractMethodError auftritt, liegt es meistens an eine falsche Zuordnung der benutzerdefinierten Datentypen im JDBC (z. B. java.sql.Struct zu java.sql.Array oder VARCHAR).

...
-------------------------------------------------------------------------------
Test set: de.rahn.jdbc.OracleConnectionTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.146 sec <<< FAILURE!
testDatabaseConnectionPerSql(de.rahn.jdbc.OracleConnectionTest)  Time elapsed: 0.738 sec  <<< ERROR!
java.lang.AbstractMethodError: oracle.jdbc.driver.OracleResultSetImpl.getObject(ILjava/lang/Class;)Ljava/lang/Object;
    ...

Diesen Fehler kann simuliert werden, in dem in der Test-Klasse OracleConnectionTest folgende Änderung vorgenommen wird.

                         return result.getObject(1, Struct.class).toString();

Die Änderung hab ich im Branch mismatch-exception in GitHub hochgestellt.

Der Quellcode und Download des Updates

Quellcode ansehen bei GitHub:
Spring und JDBC (Update von 28.02.2014)

Download einer ZIP-Datei von GitHub:
Spring und JDBC (Update von 28.02.2014)

Frank Rahn
Letzte Artikel von Frank Rahn (Alle anzeigen)
4 Kommentare

Trackbacks & Pingbacks

  1. […] mit User-defined Types (UDT) in einer Oracle Database programmiert wird. Im übergeordneten Beitrag Spring und Stored Procedure mit User-defined Types wird das Aufrufen dieser Stored Procedure aus Java heraus gezeigt. Im Beitrag Stored Procedure mit […]

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Ihre E-Mail-Adresse wird nicht veröffentlicht. Ihr Kommentar wird verschlüsselt an meinen Server gesendet. Erforderliche Felder sind mit * markiert.

Weitere Informationen und Widerrufshinweise finden Sie in meiner Datenschutzerklärung.