Spring und Stored Procedure mit User-defined Types (Tutorial)
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 Literaturempfehlungen für dieses Beispiel
- Referenzdokumentation des Spring Framework’s
- Spring 3: Framework für die Java-Entwicklung (*)
- PostgreSQL 9.1 Documentation
- Oracle 11g Release 2 Documentation
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.
- Stored Procedure mit User-defined Types unter PostgreSQL
- Stored Procedure mit User-defined Types unter Oracle
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:
- 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! - Kopieren der Datei in das Verzeichnis
src/oracle
- 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.
- Stored Procedure mit User-defined Types unter PostgreSQL
- Stored Procedure mit User-defined Types unter Oracle
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 einemjava.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 derjavax.sql.DataSource
initialisiert. Zusätzlich wird der SQL-Name der Procedure gesetzt. - In der Zeile 56 wird ein spezieller
SqlParameter
für den benutzerdefinierten DatentypenP_USER
des aktuellen Benutzers über den entsprechenden Mapper als Eingabewert (false
) gesetzt. - In der Zeile 57 wird ein spezieller
SqlParameter
für den benutzerdefinierten DatentypenP_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 demSqlParameter
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 TypparameternUserObject
undJdbcType
definiert (siehe oben). Zusätzlich implementiert dieser Mapper die SchnittstelleSqlReturnType
. - 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 dieseConnection
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 SchnittstelleSqlReturnType
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 SchnittstelleSqlTypeValue
. Diese Instanz nutzt das Spring Framework, wenn es dasjava.sql.PreparedStatement
mit den Eingabeparametern bestückt. Dazu wird die abstrakte KlasseAbstarctSqlTypeValue
des Spring Frameworks erweitert. Diese Methode wird imStandardSearchPersonsDAO
in der Zeile 70 verwendet und die erzeugte Instanz wird, wenn das Spring Framework dasjava.sql.PreparedStatement
erzeugt hat, beijdbcCall.execute()
aufgerufen. - Die Implementierung in Zeile 63 deligiert die Verarbeitung aus der Instanz
SqlTypeValue
an die MethodecreateSqlValue()
. 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-KlasseStruct
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 demUser
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 imStandardSearchPersonsDAO
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)
- Wer ist der optimale Java Bean Mapper? - Freitag, 22. September 2023
- Spring Boot Webanwendung: Die ersten Schritte (Tutorial) - Montag, 28. März 2016
- Mainframe-Zugriff via Java - Sonntag, 04. Mai 2014
Hi Frank,
This was really neat and detailed explanation. Although i have one question How would i access the procedure if it is declared inside a package.
Hi Frank, the solution is just what I needed. I don’t speak German, but I can read java 🙂
Thank you from Argentina!!!
Vielen Dank.
Saludos a Argentina. 🙂