16. Mai 2013

19. Juni 2013: DOAG Development Konferenz in Bonn

This blog posting is about an event in Germany and therefore in German only.
Am 19. Juni 2013 findet in Bonn die DOAG Development Konferenz statt, auf der ich mit zwei Vorträgen ("Oracle Datenbank für Entwickler" und "Neue Entwicklungen rund um APEX") vertreten sein werde. Ich freue mich jetzt schon auf das Wiedersehen, Diskussion und Erfahrungsaustausch.

Am 19. Juni 2013 verwandelt sich das Maritim Hotel Bonn zum Brennpunkt der Development-Szene im Oracle-Umfeld. Seien Sie dabei und tauchen Sie tief in die Software-Entwicklung mittels Tools und Technologien aus dem Hause Oracle ein! „Agile and Beyond – Projektmanagement in der Oracle-Softwareentwicklung“ lautet das Motto der zweiten Auflage der Community Konferenz. Die effektive Durchführung von Softwareprojekten steht thematisch im Zentrum der Konferenz für Entwickler, Softwarearchitekten und Projektleiter.

Scheinbar unvereinbare Ansprüche an die Softwareprojekte dieser Zeit stellen Entwickler vor neue Herausforderungen. Entwicklungswerkzeuge werden stetig komplexer und die Anforderungen auf der Fachseite wachsen, während der Zeit- und Kostendruck zunimmt. Trotz kurzer Entwicklungszyklen darf ein hohes Maß an Agilität, Transparenz sowie Qualität bei der täglichen Arbeit nicht zurückstecken. Welchen Einfluss die Oracle-Entwicklungstechnologien nehmen und ob sie bei der individuellen Softwareentwicklung unterstützen können, erfahren Entwickler auf der DOAG 2013 Development. Die Frage nach dem Verbesserungsbedarf wird diskutiert und der Austausch von Projekterfahrungen bietet wertvolle praktische Tipps.

Die DOAG 2013 Development gibt bestmögliche Orientierungshilfen und Entscheidungsgrundlagen für Ihre laufenden und zukünftigen Projekte. Profitieren Sie von einer idealen Plattform für eine intensive, aber kompakte Weiterbildungsmaßnahme! Entwickler, Software-Architekten und Projektleiter erhalten eine Fülle an wertvollen Informationen rund um Technologien und Tools sowie wichtige Trendausblicke.

Sehen wir uns in Bonn ...?

30. April 2013

ICAL mit PL/SQL erzeugen: Oracle-Tabellen mit Desktop-Kalenderanwendungen integrieren ...

Generate ICAL with PL/SQL - integrate your tables with your Desktop Calendar Applications
Bereits vor einiger Zeit hatte ich in der deutschsprachigen APEX und PL/SQL Community einen Tipp zum Thema APEX-Kalender mit Thunderbird oder Microsoft Outlook integrieren veröffentlicht.
Nun, dieser Tipp hatte mit APEX eigentlich gar nix zu tun - er funktioniert mit jeder beliebigen Tabelle, welche in einem Kalender anzeigbare Daten enthält - auch völlig ohne APEX. Die einzige Voraussetzung ist eine DATE- oder TIMESTAMP-Spalte. Und wie das geht, zeigt dieses Blog-Posting. Wir fangen mit der Tabelle an.
drop table tipp_kalender
/

create table tipp_kalender(
  id              number(10),
  bezeichnung     varchar2(200),
  ort             varchar2(200),
  organisator     varchar2(200),
  org_email       varchar2(200),
  beschreibung    varchar2(4000),
  datum_beginn    date,
  datum_ende      date,
  constraint pk_tippkalender primary key (id)
)
/

insert into tipp_kalender values (1, 'Cloud Computing mit APEX', 'Düsseldorf', 'Max Mustermann', 'max.mustermann@firma.de', 'Eine Beschreibung', to_date('20110503','YYYYMMDD'), to_date('20110503','YYYYMMDD'));
insert into tipp_kalender values (2, 'Cloud Computing mit APEX', 'Hamburg', 'Max Mustermann', 'max.mustermann@firma.de', 'Eine Beschreibung', to_date('20110510','YYYYMMDD'), to_date('20110510','YYYYMMDD'));
insert into tipp_kalender values (3, 'Cloud Computing mit APEX', 'München', 'Max Mustermann', 'max.mustermann@firma.de', 'Eine Beschreibung', to_date('20110518','YYYYMMDD'), to_date('20110518','YYYYMMDD'));
insert into tipp_kalender values (4, 'DOAG SIG Development', 'Kassel', 'DOAG', 'max.mustermann@firma.de', 'DOAG Veranstaltung', to_date('20110609','YYYYMMDD'), to_date('20110609','YYYYMMDD'));
insert into tipp_kalender values (5, 'DOAG SIG Database', 'Hannover', 'DOAG', 'max.mustermann@firma.de', 'DOAG Veranstaltung', to_date('20110519','YYYYMMDD'), to_date('20110519','YYYYMMDD'));
insert into tipp_kalender values (6, 'DOAG Konferenz 2011', 'Nürnberg', 'DOAG', 'max.mustermann@firma.de', 'DOAG Veranstaltung', to_date('20111115','YYYYMMDD'), to_date('20111117','YYYYMMDD'));

commit
/
Aus diesen Daten muss nun ein Format erzeugt werden, welches die Desktop-Kalender wie MS Outlook oder Thunderbird verstehen. ICAL bietet sich hier an.
BEGIN:VCALENDAR
VERSION:2.0
PRODID:http://www.example.com/calendarapplication/
METHOD:PUBLISH
BEGIN:VEVENT
UID:461092315540@example.com
ORGANIZER;CN="Alice Balder, Example Inc.":MAILTO:alice@example.com
SUMMARY:Eine Kurzinfo
DESCRIPTION:Beschreibung des Termines
CLASS:PUBLIC
DTSTART:20060910T220000Z
DTEND:20060919T215900Z
DTSTAMP:20060812T125900Z
END:VEVENT
END:VCALENDAR
Der folgende PL/SQL-Code generiert - anhand der Tabellendaten - das ICAL-Format.
create or replace procedure generate_ical is
begin
  /*
   * Schritt 1: HTTP-Headerinformationen setzen 
   */
  owa_util.mime_header('text/calendar', false);
  htp.p('Content-Disposition: inline; filename=apexcommunity-events.ics');
  owa_util.http_header_close;
  
  /*
   * Schritt 2: iCalendar-Format - Kopfdaten
   */
  htp.p('BEGIN:VCALENDAR');
  htp.p('VERSION:2.0');
  htp.p('PRODID:http://www.oracle.com/webfolder/global/de/community/tipps/kalender-integrieren/index.html');
  htp.p('METHOD:PUBLISH');
  
  /*
   * Schritt 3: Ereignisdaten per PL/SQL Schleife
   */
  for i in ( select * from tipp_kalender ) loop
    htp.p('BEGIN:VEVENT');
    htp.p('UID:EVENT_'||i.id||'_APEXCOMMUNITY@meinefirma.de');
    htp.p('ORGANIZER;CN='||i.organisator||':MAILTO:'||i.org_email);
    htp.p('SUMMARY:'||i.bezeichnung);
    htp.p('LOCATION:'||i.ort);
    htp.p('DESCRIPTION:'||replace(i.beschreibung,chr(10), '\n'));
    htp.p('CLASS:PUBLIC');
    htp.p('DTSTART:'||to_char(i.datum_beginn, 'YYYYMMDD'));
    htp.p('DTEND:'||to_char(i.datum_ende + 1, 'YYYYMMDD'));
    htp.p('DTSTAMP:'||to_char(sysdate, 'YYYYMMDD')||'T000000Z');
    htp.p('END:VEVENT');
  end loop;
  
  /*
   * Schritt 4: iCalendar-Format: Abschluß
   */
  htp.p('END:VCALENDAR');
end;
/
sho err
Der PL/SQL-Code arbeitet mit den Packages HTP, HTF und OWA_UTIL - die Inhalte werden also per HTTP bereitgestellt. Dabei kann APEX seine Vorteile natrülich ausspielen, denn alle APEX-Seiten werden mit PL/SQL in der Datenbank generiert und per HTTP zum Browser ausgeliefert - die ganze nötige Infrastruktur ist also schon da. Ohne APEX kann man sich aber auch helfen: Mit dem PL/SQL Embedded Gateway (DBMS_EPG) weiterhelfen. Das sieht dann wie folgt aus (im folegenden nehmen wir an, Tabelle und PL/SQL-Prozedur zum Erzeugen des ICAL liegen im Schema SCOTT):
begin
  dbms_epg.drop_dad(
     dad_name => 'SHOW_ICAL_DAD'
  );
end;
/
sho err

begin
  dbms_epg.create_dad(
    dad_name => 'SHOW_ICAL_DAD',
    path     => '/ical/*'
  );
  DBMS_EPG.SET_DAD_ATTRIBUTE( 
    dad_name => 'SHOW_ICAL_DAD', 
    attr_name => 'database-username', 
    attr_value => upper('SCOTT')
  );
  DBMS_EPG.AUTHORIZE_DAD(
    dad_name => 'SHOW_ICAL_DAD',
    user => upper('SCOTT')
  );
end;
/
Nun sollte noch überprüft werden, ob die HTTP-Protokollserver aktiviert ist. Das geschieht am einfachsten auf dem Datenbankserver mit einem lsnrctl status.
$ lsnrctl status

LSNRCTL for Linux: Version 11.2.0.2.0 - Production on 22-APR-2013 15:50:16

Copyright (c) 1991, 2010, Oracle.  All rights reserved.

Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=IPC)(KEY=EXTPROC1521)))
STATUS of the LISTENER
------------------------
Alias                     LISTENER
:
Listener Log File         /opt/oracle/diag/tnslsnr/sccloud030/listener/alert/log.xml
Listening Endpoints Summary...
  (DESCRIPTION=(ADDRESS=(PROTOCOL=ipc)(KEY=EXTPROC1521)))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sccloud030.de.oracle.com)(PORT=1521)))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sccloud030.de.oracle.com)(PORT=8080))(Presentation=HTTP)(Session=RAW))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sccloud030.de.oracle.com)(PORT=2100))(Presentation=FTP)(Session=RAW))
Services Summary...
:
Wenn die hier fett markierte Zeile mit (PORT=8080) fehlt, solltet Ihr folgendes prüfen:
  • Ist die XML DB in der Datenbank vorhanden (Dictionary View DBA_REGISTRY) ...?
    SQL> select comp_name, version from dba_registry
    
    COMP_NAME                                VERSION
    ---------------------------------------- --------------------
    Oracle Application Express               4.2.1.00.08
    OWB                                      11.2.0.2.0
    :
    Oracle Multimedia                        11.2.0.2.0
    Oracle XML Database                      11.2.0.2.0
    Oracle Text                              11.2.0.2.0
    :
    
  • Enthalt der Datenbankparameter DISPATCHERS wenigstens den Inhalt (PROTOCOL=TCP)(SERVICE={oracle-SID}XDB) ...?
    SQL> sho parameter dispatchers
    
    NAME                                 TYPE        VALUE
    ------------------------------------ ----------- -------------------------------
    dispatchers                          string      (PROTOCOL=TCP)(SERVICE=orclXDB)
    
  • Ist der HTTP-Port mit DBMS_XDB.SETHTTPPORT gesetzt worden ...?
    SQL> exec dbms_xdb.sethttpport(8080);
    
    PL/SQL procedure successfully completed.
    
Wenn das alles passiert ist, könnt Ihr den Setup in eurem Kalender testen - die folgenden Screenshots wurden mit Mozilla Thunderbird gemacht - in Microsoft Outlook funktioniert es ganz ähnlich ...
http://{datenbank-hostname}:8080/ical/generate_ical
Einrichten des Kalender als Netzwerk-Kalender im "ICS-Format"
Betrachten des Kalenders
Der APEX-Community-Tipp enthält darüber hinaus noch ausführliche Beschreibungen zum Einbinden in Thunderbird oder Outlook.
Some time ago, the german APEX and PL/SQL Community published, how to integrate APEX calendar regions with desktop applications like Mozilla Thunderbird or Microsoft Outlook (non german readers might try Google Translate - or go on reading this blog posting).
Well, this howto does not depend on APEX - it can be used within any Oracle database on any table. The only (obvious) requirement is, that the table contains a DATE or TIMESTAMP column. And this blog posting shows, how the contents of such an arbitrary table can be displayed within a desktop calendar application - we'll start with creating the table.
drop table tipp_kalender
/

create table tipp_kalender(
  id              number(10),
  bezeichnung     varchar2(200),
  ort             varchar2(200),
  organisator     varchar2(200),
  org_email       varchar2(200),
  beschreibung    varchar2(4000),
  datum_beginn    date,
  datum_ende      date,
  constraint pk_tippkalender primary key (id)
)
/

insert into tipp_kalender values (1, 'Cloud Computing mit APEX', 'Düsseldorf', 'Max Mustermann', 'max.mustermann@firma.de', 'Eine Beschreibung', to_date('20110503','YYYYMMDD'), to_date('20110503','YYYYMMDD'));
insert into tipp_kalender values (2, 'Cloud Computing mit APEX', 'Hamburg', 'Max Mustermann', 'max.mustermann@firma.de', 'Eine Beschreibung', to_date('20110510','YYYYMMDD'), to_date('20110510','YYYYMMDD'));
insert into tipp_kalender values (3, 'Cloud Computing mit APEX', 'München', 'Max Mustermann', 'max.mustermann@firma.de', 'Eine Beschreibung', to_date('20110518','YYYYMMDD'), to_date('20110518','YYYYMMDD'));
insert into tipp_kalender values (4, 'DOAG SIG Development', 'Kassel', 'DOAG', 'max.mustermann@firma.de', 'DOAG Veranstaltung', to_date('20110609','YYYYMMDD'), to_date('20110609','YYYYMMDD'));
insert into tipp_kalender values (5, 'DOAG SIG Database', 'Hannover', 'DOAG', 'max.mustermann@firma.de', 'DOAG Veranstaltung', to_date('20110519','YYYYMMDD'), to_date('20110519','YYYYMMDD'));
insert into tipp_kalender values (6, 'DOAG Konferenz 2011', 'Nürnberg', 'DOAG', 'max.mustermann@firma.de', 'DOAG Veranstaltung', to_date('20111115','YYYYMMDD'), to_date('20111117','YYYYMMDD'));

commit
/
Based on this data, we now want to generate a data format which a desktop application is able to understand. ICAL is most appropriate here - it's pretty easy and understood by most calendar applications. Below is a sample ...
BEGIN:VCALENDAR
VERSION:2.0
PRODID:http://www.example.com/calendarapplication/
METHOD:PUBLISH
BEGIN:VEVENT
UID:461092315540@example.com
ORGANIZER;CN="Alice Balder, Example Inc.":MAILTO:alice@example.com
SUMMARY:Eine Kurzinfo
DESCRIPTION:Beschreibung des Termines
CLASS:PUBLIC
DTSTART:20060910T220000Z
DTEND:20060919T215900Z
DTSTAMP:20060812T125900Z
END:VEVENT
END:VCALENDAR
The following PL/SQL code generated ICAL data based on the table's contents.
create or replace procedure generate_ical is
begin
  /*
   * Step 1: Set the HTTP header 
   */
  owa_util.mime_header('text/calendar', false);
  htp.p('Content-Disposition: inline; filename=apexcommunity-events.ics');
  owa_util.http_header_close;
  
  /*
   * Step 2: iCalendar format - header data
   */
  htp.p('BEGIN:VCALENDAR');
  htp.p('VERSION:2.0');
  htp.p('PRODID:http://www.oracle.com/webfolder/global/de/community/tipps/kalender-integrieren/index.html');
  htp.p('METHOD:PUBLISH');
  
  /*
   * Step 3: iCalendar format - event data per PL/SQL loop
   */
  for i in ( select * from tipp_kalender ) loop
    htp.p('BEGIN:VEVENT');
    htp.p('UID:EVENT_'||i.id||'_APEXCOMMUNITY@meinefirma.de');
    htp.p('ORGANIZER;CN='||i.organisator||':MAILTO:'||i.org_email);
    htp.p('SUMMARY:'||i.bezeichnung);
    htp.p('LOCATION:'||i.ort);
    htp.p('DESCRIPTION:'||replace(i.beschreibung,chr(10), '\n'));
    htp.p('CLASS:PUBLIC');
    htp.p('DTSTART:'||to_char(i.datum_beginn, 'YYYYMMDD'));
    htp.p('DTEND:'||to_char(i.datum_ende + 1, 'YYYYMMDD'));
    htp.p('DTSTAMP:'||to_char(sysdate, 'YYYYMMDD')||'T000000Z');
    htp.p('END:VEVENT');
  end loop;
  
  /*
   * Step 4: iCalendar format: footer data
   */
  htp.p('END:VCALENDAR');
end;
/
sho err
This PL/SQL code works with the packages HTP, HTF and OWA_UTIL - so the output is to be consumed over the HTTP protocol. In an APEX installation all we need is an Application Process in order to publich the procedure. Without APEX we need to do some more work - since we cannot just point the Thunderbird or MS Outlook application to the database. We need to provide an HTTP endpoint, and for this we have the PL/SQL embedded gateway (there is another blog posting about this). So, the next steps are about configuring the embedded gateway to enable an HTTP endpoint for our "database ICAL service" ... let's assume, we did all the work within the schema SCOTT.
begin
  dbms_epg.drop_dad(
     dad_name => 'SHOW_ICAL_DAD'
  );
end;
/
sho err

begin
  dbms_epg.create_dad(
    dad_name => 'SHOW_ICAL_DAD',
    path     => '/ical/*'
  );
  DBMS_EPG.SET_DAD_ATTRIBUTE( 
    dad_name => 'SHOW_ICAL_DAD', 
    attr_name => 'database-username', 
    attr_value => upper('SCOTT')
  );
  DBMS_EPG.AUTHORIZE_DAD(
    dad_name => 'SHOW_ICAL_DAD',
    user => upper('SCOTT')
  );
end;
/
Next, we should check, whether the HTTP protocol server is enabled - the most easy way is to execute a lsnrctl status.
$ lsnrctl status

LSNRCTL for Linux: Version 11.2.0.2.0 - Production on 22-APR-2013 15:50:16

Copyright (c) 1991, 2010, Oracle.  All rights reserved.

Connecting to (DESCRIPTION=(ADDRESS=(PROTOCOL=IPC)(KEY=EXTPROC1521)))
STATUS of the LISTENER
------------------------
Alias                     LISTENER
:
Listener Log File         /opt/oracle/diag/tnslsnr/sccloud030/listener/alert/log.xml
Listening Endpoints Summary...
  (DESCRIPTION=(ADDRESS=(PROTOCOL=ipc)(KEY=EXTPROC1521)))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sccloud030.de.oracle.com)(PORT=1521)))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sccloud030.de.oracle.com)(PORT=8080))(Presentation=HTTP)(Session=RAW))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sccloud030.de.oracle.com)(PORT=2100))(Presentation=FTP)(Session=RAW))
Services Summary...
:
If the bold line is present, you are fine. If the HTTP-Port is a different value, you are also fine. If the line is missing, you should do the following 3 checks:
  • Is XML DB installed in the data dictionary of the database...? Use the dictionary view DBA_REGISTRY to check this.
    SQL> select comp_name, version from dba_registry
    
    COMP_NAME                                VERSION
    ---------------------------------------- --------------------
    Oracle Application Express               4.2.1.00.08
    OWB                                      11.2.0.2.0
    :
    Oracle Multimedia                        11.2.0.2.0
    Oracle XML Database                      11.2.0.2.0
    Oracle Text                              11.2.0.2.0
    :
    
  • Is the database parameter DISPATCHERS configured correctly ...? It must contain at least (PROTOCOL=TCP)(SERVICE={oracle-SID}XDB)
    SQL> sho parameter dispatchers
    
    NAME                                 TYPE        VALUE
    ------------------------------------ ----------- -------------------------------
    dispatchers                          string      (PROTOCOL=TCP)(SERVICE=orclXDB)
    
  • Did you set the HTTP port by calling DBMS_XDB.SETHTTPPORT ...?
    SQL> exec dbms_xdb.sethttpport(8080);
    
    PL/SQL procedure successfully completed.
    
When the HTTP endpoint is running, you might check your setup within Mozilla thunderbird or Microsoft Outlook; the following screenshots have been created with Thunderbird. Just create a new calendar, choose a Network calendar and use the following URL as the service location ...
http://{datenbank-hostname}:8080/ical/generate_ical
Setup a network calendar within Mozilla Thunderbird.
View the calendar contents (the table data) within Thunderbird.
The document within the german APEX and PL/SQL community also contains comprehensive setup guides for Outlook and Thunderbird - but in German language. So you might use a translation tool here ...

15. April 2013

PL/SQL Email Client Package: Version 1.0 verfügbar

PL/SQL Email Client package: version 1.0 is available
Nach einer sehr langen Zeit (um genau zu sein: nach 2 1/2 Jahren), konnte ich endlich eine neue Version meines Projektes PL/SQL Email Client online stellen. Neu in Version 1.0 sind
  • Eine GET_RECEIVEDATE -Funktion, die den Zeitstempel, zu dem die Mail empfangen wurde, zurückgibt (nicht das Sendedatum)
  • Das Package MAIL_FILTER zum Setzen von Suchfiltern auf dem Mailserver. Damit ruft das Package nicht mehr zwingend alle Emails aus einem Folder ab, sondern nur die, die den gesetzten Filterkriterien genügen. Das Filtern wird vom Mailserver gemacht; nicht von der Datenbank.
  • Und schließlich sind noch einige interne Optimierungen enthalten: So instanziiert der Java-Code nun weniger interne Objekte.
Am besten gleich mal ausprobieren - und so ruft Ihr mit PL/SQL alle ungelesenen Emails aus eurem Postfach ab.
begin
  mail_client.connect_server(
    p_hostname => 'mailserver.mycompany.de',
    p_port     => '993',
    p_protocol => mail_client.protocol_imap,
    p_userid   => '****',
    p_passwd   => '****',
    p_ssl => true
  );
  mail_client.open_inbox;
  mail_filter.clear_filters;
  mail_filter.add_seen_filter(false);
end;
/

PL/SQL procedure successfully completed.

select msg_number, subject, sender, sent_date, message_size from table(mail_client.get_mail_headers_p())e ;

MSG_NUMBER
----------
SUBJECT
--------------------------------------------------------------------------------
SENDER
--------------------------------------------------------------------------------
SENT_DATE           MESSAGE_SIZE
------------------- ------------
         6
Testmail
Carsten Czarski
15.04.2013 14:09:31         6790

begin
  mail_client.close_folder;
  mail_client.disconnect_server;
end;
/

1 row selected.

PL/SQL procedure successfully completed.
Den ersten "Feature-Request" für Version 1.1 habe ich schon - es wird also nicht mehr 2 1/2 Jahre dauern.
Eines noch: Ich bekomme immer wieder Emails, die mir zeigen, dass dieses Package tatsächlich verwendet wird. Das würde ich gerne mal sammeln. Also: Wenn jemand das Package MAIL_CLIENT (produktiv) nutzt und dazu ein wenig als Kommentar zu diesem Posting schreibt, freue ich sehr mich darüber ... und vielen Dank schon mal im voraus.
After a long period of time (2 and a haf years, to be exact), I finally managed to release version 1.0 of my PL/SQL Email Client project. And these are the new features.
  • The GET_RECEIVEDATE function returns the timestamp at which the message has been received (not sent).
  • The MAIL_FILTER package allows to set server-side filters. The package will then retrieve not necessarily all emails from within a folder, but only these which suffice the filter set by the programmer. Filtering is done by the email server.
  • Beyond this, I did some internal optimizations: The java code now instantiates less objects.
So ... just try it out: The following SQL script retrieves all unread messages from your Inbox folder. You just need to replace the connection details for your mail server - and make sure that your database is able to reach the mail server via the network.
begin
  mail_client.connect_server(
    p_hostname => 'mailserver.mycompany.de',
    p_port     => '993',
    p_protocol => mail_client.protocol_imap,
    p_userid   => '****',
    p_passwd   => '****',
    p_ssl => true
  );
  mail_client.open_inbox;
  mail_filter.clear_filters;
  mail_filter.add_seen_filter(false);
end;
/

PL/SQL procedure successfully completed.

select msg_number, subject, sender, sent_date, message_size from table(mail_client.get_mail_headers_p())e ;

MSG_NUMBER
----------
SUBJECT
--------------------------------------------------------------------------------
SENDER
--------------------------------------------------------------------------------
SENT_DATE           MESSAGE_SIZE
------------------- ------------
         6
Testmail
Carsten Czarski
15.04.2013 14:09:31         6790

1 row selected.

begin
  mail_client.close_folder;
  mail_client.disconnect_server;
end;
/

PL/SQL procedure successfully completed.
I already have the next "feature request" - but the next version will be there sooner than 2 1/2 years ...
One more thing: In the past, I got some emails telling me about the usage of the mail client package or asking some questions. I'd like to collect this information here: So ... if you are using the MAIL_CLIENT package (in production?) and don't mind, I'd be very happy if you wrote a few words as a commect to this blog posting - thanks in advance.

25. März 2013

SQL*Plus für alle ... heute: Diagramme mit SQL*Plus

SQL*Plus everywhere ... today: Charts with SQL*Plus
Heute gibt es ein recht kurzes - und um ehrlich zu sein, auch eher nutzloses Posting - aber ich bin jüngst in der Dokumentation (SQL Developers Guide - Funktion RPAD auf ein nettes "Anwendungsbeispiel" zur RPAD-Funktion gestoßen: Barcharts in SQL*Plus.
SQL> select ename, sal, rpad('|',ceil(sal/500)+1, '*') as sal_chart from emp;

ENAME         SAL SAL_CHART
---------- ------ --------------------
SMITH         800 |**
ALLEN        1600 |****
WARD         1250 |***
JONES        2975 |******
MARTIN       1250 |***
BLAKE        2850 |******
CLARK        2450 |*****
SCOTT        3000 |******
KING         5000 |**********
TURNER       1500 |***
ADAMS        1100 |***
JAMES         950 |**
FORD         3000 |******
MILLER       1300 |***
Auch "Linecharts" sind möglich ;-)
SQL> select ename, sal, rpad('|',ceil(sal/500), ' ')||'*' as sal_chart 
  2  from emp 
  2  order by sal

ENAME         SAL SAL_CHART
---------- ------ --------------------
SMITH         800 | *
JAMES         950 | *
ADAMS        1100 |  *
WARD         1250 |  *
MARTIN       1250 |  *
MILLER       1300 |  *
TURNER       1500 |  *
ALLEN        1600 |   *
CLARK        2450 |    *
BLAKE        2850 |     *
JONES        2975 |     *
SCOTT        3000 |     *
FORD         3000 |     *
KING         5000 |         *
Gerne genommen sind auch Stacked Bar Charts ...
SQL> select
  2    ename,
  3    sal,
  4    comm,
  5    rpad(
  6      rpad('|',ceil(sal/300)+1,'*'),
  7      ceil((sal+nvl(comm,0))/300)+1,
  8      '#'
  9    ) as pay_chart
 10  from emp
 11* order by sal

ENAME             SAL       COMM PAY_CHART
---------- ---------- ---------- --------------------
SMITH             800            |***
JAMES             950            |****
ADAMS            1100            |****
WARD             1250        500 |*****#
MARTIN           1250       1400 |*****####
MILLER           1300            |*****
TURNER           1500          0 |*****
ALLEN            1600        300 |******#
CLARK            2450            |*********
BLAKE            2850            |**********
JONES            2975            |**********
SCOTT            3000            |**********
FORD             3000            |**********
KING             5000            |*****************
Auch für die Schule kann SQL*Plus nun wertvolle Dienste leisten ...
SQL> select level x, power(level,2) y, rpad('|', round(power(level,2)/10)+1,' ')||'*' yc 
  2  from dual 
  3  connect by level < 21;

         X      Y YC
---------- ------ ------------------------------------------------------------
         1      1 |*
         2      4 |*
         3      9 | *
         4     16 |  *
         5     25 |   *
         6     36 |    *
         7     49 |     *
         8     64 |      *
         9     81 |        *
        10    100 |          *
        11    121 |            *
        12    144 |              *
        13    169 |                 *
        14    196 |                    *
        15    225 |                       *
        16    256 |                          *
        17    289 |                             *
        18    324 |                                *
        19    361 |                                    *
        20    400 |                                        *
So ... nun ist es aber genug. Muss wieder was richtiges arbeiten.
Today's posting is short and - to be honest - rather useless. But recently, I encountered a nice usage example of the RPAD function within the Oracle Documentation - SQL Developers Guide: Diagrams in SQL*Plus.
SQL> select ename, sal, rpad('|',ceil(sal/500)+1, '*') as sal_chart from emp;

ENAME         SAL SAL_CHART
---------- ------ --------------------
SMITH         800 |**
ALLEN        1600 |****
WARD         1250 |***
JONES        2975 |******
MARTIN       1250 |***
BLAKE        2850 |******
CLARK        2450 |*****
SCOTT        3000 |******
KING         5000 |**********
TURNER       1500 |***
ADAMS        1100 |***
JAMES         950 |**
FORD         3000 |******
MILLER       1300 |***
We can change this to a line chart ...
SQL> select ename, sal, rpad('|',ceil(sal/500), ' ')||'*' as sal_chart 
  2  from emp 
  2  order by sal

ENAME         SAL SAL_CHART
---------- ------ --------------------
SMITH         800 | *
JAMES         950 | *
ADAMS        1100 |  *
WARD         1250 |  *
MARTIN       1250 |  *
MILLER       1300 |  *
TURNER       1500 |  *
ALLEN        1600 |   *
CLARK        2450 |    *
BLAKE        2850 |     *
JONES        2975 |     *
SCOTT        3000 |     *
FORD         3000 |     *
KING         5000 |         *
Stacked Bar Charts are also very nice ...
SQL> select
  2    ename,
  3    sal,
  4    comm,
  5    rpad(
  6      rpad('|',ceil(sal/300)+1,'*'),
  7      ceil((sal+nvl(comm,0))/300)+1,
  8      '#'
  9    ) as pay_chart
 10  from emp
 11* order by sal

ENAME             SAL       COMM PAY_CHART
---------- ---------- ---------- --------------------
SMITH             800            |***
JAMES             950            |****
ADAMS            1100            |****
WARD             1250        500 |*****#
MARTIN           1250       1400 |*****####
MILLER           1300            |*****
TURNER           1500          0 |*****
ALLEN            1600        300 |******#
CLARK            2450            |*********
BLAKE            2850            |**********
JONES            2975            |**********
SCOTT            3000            |**********
FORD             3000            |**********
KING             5000            |*****************
And, SQL*Plus gets usable for school kids ...
SQL> select level x, power(level,2) y, rpad('|', round(power(level,2)/10)+1,' ')||'*' yc 
  2  from dual 
  3  connect by level < 21;

         X      Y YC
---------- ------ ------------------------------------------------------------
         1      1 |*
         2      4 |*
         3      9 | *
         4     16 |  *
         5     25 |   *
         6     36 |    *
         7     49 |     *
         8     64 |      *
         9     81 |        *
        10    100 |          *
        11    121 |            *
        12    144 |              *
        13    169 |                 *
        14    196 |                    *
        15    225 |                       *
        16    256 |                          *
        17    289 |                             *
        18    324 |                                *
        19    361 |                                    *
        20    400 |                                        *
OK ... enough for the moment - have to do some real work again ;-)

5. März 2013

Einige Worte zu PL/SQL Abhängigkeiten oder: "Erweitere Deine Packages immer am Ende"

Some words on PL/SQL Dependencies - or "Always extend your packages at the botton"
Mit Oracle 11.1 wurde für PL/SQL Pakete das "fine grained dependency model" eingeführt. Kurz gesagt bedeutet das, das Änderungen an einem PL/SQL Paket nicht zwingend alle anderen Objekte, die das Paket irgendwie verwenden, invalidiert werden und neu kompiliert werden müssen. Ändert man im PL/SQL Paket die Prozedur P, so werden Objekte, welche die Prozedur A verwenden, nicht invalidiert. So weit, so gut: Das wollen wir testen.
create or replace package pkg_inv_test is
  function helloworld return varchar2;
  function today return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  function helloworld return varchar2 is begin return 'Hello World'; end helloworld;
  function today return date is begin return sysdate; end today;
end pkg_inv_test;
/

create or replace function get_today 
return date is
begin
  return pkg_inv_test.today;
end get_today;
/
Die Funktion GET_TODAY ist also abhängig vom Package PKG_INV_TEST Ein kurzer Test ...
select get_today from dual
/

GET_TODAY
-------------------
05.03.2013 10:31:13

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID
In Oracle10g würde jede Änderung an dem Package die Funktion invalidieren - in Oracle11g sollte das Invalidieren nur dann auftreten, wenn die Funktion PKG_INV_TEST.TODAY verändert wird. Auch dies testen wir kurz: Wir fügen der Funktion einen neuen Parameter mit Default-Wert hinzu.

create or replace package pkg_inv_test is
  function helloworld return varchar2;
  function today(p_diff in number default 1) return date;
end pkg_inv_test;
/


create or replace package body pkg_inv_test is
  function helloworld return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;
end pkg_inv_test;
/

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      INVALID

1 Zeile wurde ausgewählt.
Vor dem nächsten Aufruf wird diese Funktion nun automatisch neu kompiliert.
select get_today from dual;

GET_TODAY
-------------------
05.03.2013 10:38:57

1 Zeile wurde ausgewählt.

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 Zeile wurde ausgewählt.
So beim einfachen Ausprobieren sieht eine Invalidierung nicht schlimm aus - aber durch das Neukompilieren wird zum Einen der gesamte Status der Funktion zurückgesetzt - zum anderen darf die Funktion gerade nicht in Nutzung sein - wenn sie gerade für einen anderen User läuft, bekommt man die Änderung gar nicht durch. Änderugen an einer anderen Funktion im Package invalidieren GET_TODAY dagegen nicht - das ist der große Vorteil der fine-grained-dependencies in Oracle11g. Nun nehmen wir also eine Änderung allein an der Funktion HELLOWORLD vor - diese tangiert GET_TODAY nicht im Geringsten ...
create or replace package pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2;
  function today(p_diff in number default 1) return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;
end pkg_inv_test;
/

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 Zeile wurde ausgewählt.
Und siehe da: GET_TODAY bleibt VALID. Aber dann sollte es ja auch kein Thema sein, eine neue Funktion hinzuzufügen, oder ...?
create or replace package pkg_inv_test is
  -- Neue Funktion hier
  function new_function return varchar2;

  function helloworld(p_param in number default 1) return varchar2;
  function today(p_diff in number default 1) return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  -- Neue Funktion hier
  function new_function return varchar2 is begin return 'Das ist neu!'; end new_function;

  function helloworld(p_param in number default 1) return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;
end pkg_inv_test;
/


select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      INVALID

1 Zeile wurde ausgewählt.
Nanu? Die Funktion GET_TODAY hängt doch nur von der PKG_INV_TEST.TODAY ab ... und die wurde nicht angefasst. Trotzdem ist sie nun INVALID. Die Erklärung ist nicht einfach zu finden; im Advanced Application Developers Guide unter Invalidation of Dependent Objects wird man aber fündig. Dort heißt es zu Änderungen an einem Package, dass die Fine Grained Invalidation nicht gilt (Exception), wenn u.a. der Entry Point sich ändert (Dependent object references a package procedure or function whose call signature or entry-point number, changed.). In einer Fußnote kann man dann lesen, dass der "Entry Point" einer Funktion oder Prozedur sich aus der Position im Code des Package ergibt.
Also: Durch den Einbau der neuen Funktion zu Beginn des Package haben sich die Entry Points aller enthaltenen Prozeduren und Funktionen geändert - also haben wir die Fine Grained Invalidation dadurch ausgehebelt. Aber dann sollte es ja keine Invalidierung geben, wenn die neue Funktion am Ende des Package hinzugefügt wird ...
create or replace package pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2;
  function today(p_diff in number default 1) return date;

  -- Neue Funktion hier
  function new_function return varchar2;
end pkg_inv_test;
/


create or replace package body pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;

  -- Neue Funktion hier
  function new_function return varchar2 is begin return 'Das ist neu!'; end new_function;
end pkg_inv_test;
/

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 Zeile wurde ausgewählt.
Zusammengefasst (tl;dr) kann man sagen: Erweitere Deine Packages immer am Ende - alles, was vom Code hinter einer Änderung abhängig ist, wird invalidiert!
In database version 11.1, Oracle introduced the "fine grained dependency model". Briefly, this means, that a change to one function or procedure within a package does not affect objects depending on other components. So if we have the top level function F, which depends on A within package P (P.A) - only changes to P.A will invaldiate F. F will not be invalidated, when other components of P are being changed. So far - so good: Let's play with this ...
create or replace package pkg_inv_test is
  function helloworld return varchar2;
  function today return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  function helloworld return varchar2 is begin return 'Hello World'; end helloworld;
  function today return date is begin return sysdate; end today;
end pkg_inv_test;
/

create or replace function get_today 
return date is
begin
  return pkg_inv_test.today;
end get_today;
/
The top-level function GET_TODAY depends on TODAY within the package PKG_INV_TEST. All set and working ...
select get_today from dual
/

GET_TODAY
-------------------
05.03.2013 10:31:13

1 row selected.

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 row selected.
In Oracle10g, any change to PKG_INV_TEST would invalidate GET_TODAY - in 11g only changes to PKG_INV_TEST.TODAY should lead to an invalidiation - let's test this as well: We'll change GET_TODAY in order to add a new parameter with a default value.
create or replace package pkg_inv_test is
  function helloworld return varchar2;
  function today(p_diff in number default 1) return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  function helloworld return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;
end pkg_inv_test;
/

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      INVALID

1 row selected.
GET_TODAY is now invalid - as expected. When it's being called the next time, the database will recompile it automatically.
select get_today from dual;

GET_TODAY
-------------------
05.03.2013 10:38:57

1 row selected.

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 row selected.
At the first glance, an Invalidation is not a big thing - since the database recompiles the component automatically. But the recompilation will also reset the PL/SQL state - if the dependent object is another package (with global variables) - these will be reset. Also, the dependent object must not be used in order to become recompiled. So if GET_TODAY would be in use, we would have to wait until it's free in order to make the change. So it's always good practice to avoid invalidations as far as possible. Given this, a change to the other function within PKG_INV_TEST (HELLOWORLD) should not affect GET_TODAY at all.
create or replace package pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2;
  function today(p_diff in number default 1) return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;
end pkg_inv_test;
/

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 row selected.
As expected: GET_TODAY remains VALID. So it should also remain valid when we add a new component to the package, shouldn't it ...?
create or replace package pkg_inv_test is
  -- New function being added here ...
  function new_function return varchar2;

  function helloworld(p_param in number default 1) return varchar2;
  function today(p_diff in number default 1) return date;
end pkg_inv_test;
/

create or replace package body pkg_inv_test is
  -- New function being added here ...
  function new_function return varchar2 is begin return 'This is new!'; end new_function;

  function helloworld(p_param in number default 1) return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;
end pkg_inv_test;
/


select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      INVALID

1 row selected.
Hey ... what happened? GET_TODAY only depends on PKG_INV_TEST.TODAY - and this function did not change. But it has been invalidated anyway. The explanation for this behaviour is not easy to find, but the Advanced Application Developers Guide contains the relevant pieces in Invalidation of Dependent Objects. Table 18-2 (showing the fine-grained-dependecy behaviour for various ALTER commands) says in the row for ALTER PACKAGE, that fine grained invalidation does not apply (Exception), when the Entry point of a component is being changed (Dependent object references a package procedure or function whose call signature or entry-point number, changed.). The Footnote then explains that the "Entry Point" of a package component is based on its position in the package code.
Since we have added our new component at the beginning of the package, the code positions of all other components have been changed - therefore all entry points have been changed. And that means, that all dependent objects have been invalidated. So the invalidation should not occur when we add our new component at the end of the package ...
create or replace package pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2;
  function today(p_diff in number default 1) return date;

  -- New function being added here ...
  function new_function return varchar2;
end pkg_inv_test;
/


create or replace package body pkg_inv_test is
  function helloworld(p_param in number default 1) return varchar2 is begin return 'Hello World'; end helloworld;
  function today(p_diff in number default 1) return date is begin return sysdate; end today;

  -- New function being added here ...
  function new_function return varchar2 is begin return 'This is new!'; end new_function;
end pkg_inv_test;
/

select object_name, status from user_objects where object_name = 'GET_TODAY'
/

OBJECT_NAME                    STATUS
------------------------------ -------
GET_TODAY                      VALID

1 row selected.
Voilá! The summary (tl;dr) of this blog posting is very simple: Always extend your packages at the bottom! Everything, which depends on package code below your change, will be invalidated!

7. Februar 2013

Workspace Manager Teil II: Langlaufende Transaktionen in der Datenbank

Workspace Manager Part II: Long-Running transactions in the Database
Heute setze ich das Blog-Posting zum Thema "Oracle Workspace Manager" mit Teil 2 fort. Nachdem im ersten Teil die grundliegenden Fähigkeiten vorgestellt worden, wird dieses Blog-Posting sich mit weiterführenden Fähigkeiten, wie Savepoints, Workspace-Refresh oder Konfliktmanagement beschäftigen. Legen wir am besten gleich los. Wieder dient uns die wohlbekannte Tabelle EMP als Ausgangspunkt. Diese wird zunächst version-enabled (sofern noch nicht geschehen), dann legen wir einen neuen Workspace an und machen darin ein paar Änderungen ...
begin
  dbms_wm.enableVersioning('EMP,DEPT');
end;
/

begin
  dbms_wm.createWorkspace('PLANUNG_1');
end;
/

begin
  dbms_wm.gotoWorkspace('PLANUNG_1');
end;
/

SQL> update emp set sal = sal * 2 where deptno = 20;

SQL> delete from emp where job = 'SALESMAN';

SQL> commit;

SQL> select * from emp order by deptno;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7934 MILLER     CLERK      7782 23.01.1982 00:00:00  1300           10
 7839 KING       PRESIDENT       17.11.1981 00:00:00  5000           10
 7782 CLARK      MANAGER    7839 09.06.1981 00:00:00  2450           10
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  6000           20
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  1600           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  5950           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  2200           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  6000           20
 7900 JAMES      CLERK      7698 03.12.1981 00:00:00   950           30
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30

10 Zeilen ausgewählt.
Soweit waren wir beim letzten Mal auch schon. Nun könnten sich im übergeordneten Workspace LIVE ja auch noch Änderungen ergeben ... das ganze gilt natürlich dann auch analog für den Workspace PLANUNG_1 und einen diesem untergeordneten Workspace: Änderungen müssen nicht zwingend nur im Child-Workspace geschehen; auch im Parent können Änderungen gemacht werden ...

Änderungen vom Parent- zum Child-Workspace: Refresh

begin
  dbms_wm.gotoWorkspace('LIVE');
end;
/

SQL> update emp set job='SEN_CLERK' where deptno = 30 and job = 'CLERK';

SQL> select * from emp where deptno = 30;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7499 ALLEN      SALESMAN   7698 20.02.1981 00:00:00  1600   300     30
 7521 WARD       SALESMAN   7698 22.02.1981 00:00:00  1250   500     30
 7654 MARTIN     SALESMAN   7698 28.09.1981 00:00:00  1250  1400     30
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30
 7844 TURNER     SALESMAN   7698 08.09.1981 00:00:00  1500     0     30
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00   950           30

6 Zeilen ausgewählt.
Im Workspace PLANUNG_1 ist diese Änderung nicht sichtbar ... denn sie wurde ja gemacht, nachdem der Workspace angelegt wurde.
begin
  dbms_wm.gotoWorkspace('PLANUNG_1');
end;
/

SQL> select * from emp where deptno =30;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30
 7900 JAMES      CLERK      7698 03.12.1981 00:00:00   950           30
Mit einer Refresh-Operation (DBMS_WM.REFRESHWORKSPACE) werden Änderungen in einem Parent-Workspace zum Child Workspace übertragen - die Planung bzw. Simulation wird also mit den Änderungen in der Realität "aufgefrischt".
begin
  dbms_wm.refreshworkspace(
    workspace    => 'PLANUNG_1',
    auto_commit  => true,
    copy_data    => true
  );
end;
/

begin
  dbms_wm.gotoWorkspace('PLANUNG_1');
end;
/

SQL> select * from emp where deptno =30;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00   950           30
Die Änderung im LIVE-Workspace ist also bei der PLANUNG_1 angekommen. Beim Workspace-Refresh ist nun vor allem der dritte Parameter copy_data wichtig.
  • false ändert nur die Versionsinformation der im Parent-Workspace geänderten Tabellenzeile; dadurch wird sie im Child-Workspace und auch in allen hierarchisch nachfolgenden Workspaces sofort sichtbar. Die Änderung wird quasi "in der Zeit vor das Anlegen der Child-Workspaces zurückgeschickt". Wenn der Workspace PLANUNG_1 also noch einen Child-Workspace PLANUNG_1_A hat, dann ist der Refresh auch für diesen sofort sichtbar.
  • true kopiert die im Parent geänderten Tabellenzeilen - dadurch wird der Refresh im Child-Workspace sichtbar (wie gewünscht), in den hierarchisch darunter liegenden Workspaces ist die Änderung aber nicht - hierfür müssen dann nochmals eigene Refresh-Operationen durchgeführt werden.

Was tun bei Konflikten?

Nun wollen wir die Refresh-Operation nochmal ausprobieren. Diesmal machen wir aber eine Änderung an einer Tabellenzeile, die im Child-Workspace ebenfalls verändert wurde ...
begin
  dbms_wm.gotoWorkspace('LIVE');
end;
/

SQL> update emp set sal = sal * 3 where deptno = 20;

SQL> select * from emp where deptno = 20;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  2400           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  8925           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  9000           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  3300           20
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  9000           20
Im Workspace PLANUNG_1 sieht die DEPTNO 20 so aus ...
begin
  dbms_wm.gotoWorkspace('PLANUNG_1');
end;
/

SQL> select * from emp where deptno = 20;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  1600           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  5950           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  6000           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  2200           20
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  6000           20
Man sieht sofort, dass hier ein Konflikt vorliegt. Und das merkt man - sowohl beim Versuch eines Refresh von LIVE nach PLANUNG_1 ...
begin
  dbms_wm.refreshworkspace(
    workspace    => 'PLANUNG_1',
    auto_commit  => true,
    copy_data    => true
  );
end;
/

begin
*
FEHLER in Zeile 1:
ORA-20055: conflicts detected for workspace: 'PLANUNG_1' in table: 'WM.EMP'
ORA-06512: in "WMSYS.LT", Zeile 5293
ORA-06512: in Zeile 2
... als auch bei der umgekehrten Merge-Operation ...
begin
  dbms_wm.mergeworkspace(
    workspace    => 'PLANUNG_1'
  );
end;
/

begin
*
FEHLER in Zeile 1:
ORA-20055: conflicts detected for workspace: 'PLANUNG_1' in table: 'WM.EMP'
ORA-06512: in "WMSYS.LT", Zeile 6043
ORA-06512: in Zeile 2
... gibt es einen Fehlermeldung. Diese Konflikte müssen gelöst werden, bevor ein Refresh oder Merge erfolgreich durchgeführt werden kann. Dazu dienen die Prozeduren BEGINRESOLVE, RESOLVECONFLICTS und COMMITRESOLVE im Paket DBMS_WM.
  • Zunächst wird mit DBMW_WM.BEGINRESOLVE eine Konfliktlösungs-Session begonnen.
    begin
      dbms_wm.beginResolve(
        workspace    => 'PLANUNG_1'
      );
    end;
    /
    
  • Danach wird mit DBMS_WM.RESOLVECONFLICTS festgelegt, welche Version (aus dem Parent- oder aus dem Child-Workspace) beim einem Merge oder Refresh genommen werden soll. Mit einer WHERE-Klausel wird festgelegt, für welche Tabellenzeilen die Regel gelten soll. So können auch mehrere Regeln hinterlegt werden. Im folgenden soll für die MANAGER die Änderung aus dem Parent-Workspace (LIVE) und für alle anderen die Änderung aus dem Child-Workspace gelten. Da in der WHERE-Klausel nur Primärschlüsselspalten angesprochen werden dürfen, müssen wir hierfür mit etwas PL/SQL arbeiten ...
    begin
      for e in (select empno, job from emp) loop
        if e.job = 'MANAGER' then 
          dbms_wm.resolveConflicts (
            workspace    => 'PLANUNG_1',
            table_name   => 'EMP',
            where_clause => 'EMPNO = '||e.empno,
            keep         => 'PARENT'
          );
        else 
          dbms_wm.resolveConflicts (
            workspace    => 'PLANUNG_1',
            table_name   => 'EMP',
            where_clause => 'EMPNO = '||e.empno,
            keep         => 'CHILD'
          );
        end if;
      end loop;
    end;
    /
    
  • Schließlich wird die Konfliktlösungs-Session mit DBMW_WM.COMMITRESOLVE abgeschlossen. Achten Sie darauf, vorher ein "normales" COMMIT abzusetzen.
    commit
    /
    
    begin
      dbms_wm.commitResolve(
        workspace    => 'PLANUNG_1'
      );
    end;
    /
    
Anschließend wird die Merge- oder Refresh-Operation fehlerfrei durchlaufen.
begin
  dbms_wm.mergeworkspace(
    workspace    => 'PLANUNG_1'
  );
end;
/

PL/SQL-Prozedur erfolgreich abgeschlossen.
Nach der Merge-Operation befindet sich die folgende Version der Tabelle EMP im Workspace LIVE.
SQL> select * from emp where deptno = 20;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  1600           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  8925           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  6000           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  2200           20
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  6000           20
Im "echten Leben" kennt man die Konflikte natürlich nicht im Voraus. Doch der Workspace Manager bietet auch Möglichkeiten an, Workspaces auf Konflikte hin zu untersuchen. Wir starten nochmals in der Konfliktsituation.
begin
  dbms_wm.mergeworkspace(
    workspace    => 'PLANUNG_1'
  );
end;
/

begin
*
FEHLER in Zeile 1:
ORA-20055: conflicts detected for workspace: 'PLANUNG_1' in table: 'WM.EMP'
ORA-06512: in "WMSYS.LT", Zeile 6043
ORA-06512: in Zeile 2
Die Konfliktuntersuchung startet mit einem Aufruf von DBMS_WM.SETCONFLICTWORKSPACE. Danach dann die View EMP_CONF untersucht werden ...
begin
  dbms_wm.setConflictWorkspace('PLANUNG_1');
end;
/

SQL> select wm_workspace, empno, ename, job, deptno, sal, wm_deleted from emp_conf order by empno, wm_workspace;

WM_WORKSPACE      EMPNO ENAME      JOB           DEPTNO        SAL WM_
------------ ---------- ---------- --------- ---------- ---------- ---
BASE               7369 SMITH      CLERK             20        800 NO
LIVE               7369 SMITH      CLERK             20       2400 NO
PLANUNG_1          7369 SMITH      CLERK             20       1600 NO
BASE               7566 JONES      MANAGER           20       2975 NO
LIVE               7566 JONES      MANAGER           20       8925 NO
PLANUNG_1          7566 JONES      MANAGER           20       5950 NO
BASE               7788 SCOTT      ANALYST           20       3000 NO
LIVE               7788 SCOTT      ANALYST           20       9000 NO
PLANUNG_1          7788 SCOTT      ANALYST           20       6000 NO
BASE               7876 ADAMS      CLERK             20       1100 NO
LIVE               7876 ADAMS      CLERK             20       3300 NO
PLANUNG_1          7876 ADAMS      CLERK             20       2200 NO
BASE               7902 FORD       ANALYST           20       3000 NO
LIVE               7902 FORD       ANALYST           20       9000 NO
PLANUNG_1          7902 FORD       ANALYST           20       6000 NO

15 Zeilen ausgewählt.
Die View EMP_CONF enthält nun für jede Tabellenzeile, für die ein Konflikt besteht, drei Informationen: Mit BASE wird die gemeinsame Basis beider "Konfliktparteien" - in unserem Fall der Originalwert aus der Tabelle EMP angegeben. Zusätzlich sind die Versionsstände aus beiden Workspaces vorhanden. Auf Basis dieser View ließe sich nun auch eine Oberfläche für den Endanwender erstellen - dieser könnte dann jeweils anklicken, welche Version die Richtige sein soll.

Savepoints in einem Workspace

Savepoints im Workspace Manager sind prinzipiell das gleiche wie Savepoints im Rahmen einer "normalen" Datenbanktransaktion. Sie werden mit DBMS_WM.CREATESAVEPOINT erstellt und können beim Rollback eines Workspace verwendet werden. Anstelle der Prozedur RollbackWorkspace verwendet man dann DBMS_WM.ROLLBACKTOSP. Es werden dann nicht alle Änderungen des Workspace zurückgerollt, sondern nur die bis zum angegebenen Savepoint.
SQL> select * from emp where empno = 7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00   950           30

SQL> update emp set sal = 2000 where empno=7900;

SQL> commit;

SQL> select * from emp where empno = 7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00  2000           30


begin
  dbms_wm.createSavepoint(
    workspace      => 'PLANUNG_1',
    savepoint_name => 'SP_A',
    description    => '...',
    auto_commit    => true
  );
end;
/

SQL> update emp set sal = 5000 where empno=7900;

SQL> commit;

SQL> select * from emp where empno=7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00  5000           30

begin
  dbms_wm.rollbackToSp(
    workspace      => 'PLANUNG_1',
    savepoint_name => 'SP_A'
  );
end;
/

SQL> select * from emp where empno = 7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00  2000           30
Damit soll es für heute nun auch genug sein. Die verbleibenden Features werde ich dann in einem dritten Blog-Posting abdecken.
  • Gibt es ein Privilegienmodell für den Workspace Manager?
  • Gibt es eine Historisierung?
  • ...
Bis dahin verweise ich auf folgendes Material zum Weiterlesen ...
Today I'll continue the blog posting about "Oracle Workspace Manager". The first posting covered the very basic functionality like version-enabling one or more tables, creating a workspace and merging changes. This posting contains the Refresh operation, Conflict Management and Savepoints in a Workspace. Again, we'll use the well-known table EMP to illustrate the examples - first we'll version-enable (if not done already), then we'll create a new workspace and make some changes ....
begin
  dbms_wm.enableVersioning('EMP,DEPT');
end;
/

begin
  dbms_wm.createWorkspace('PLAN_1');
end;
/

begin
  dbms_wm.gotoWorkspace('PLAN_1');
end;
/

SQL> update emp set sal = sal * 2 where deptno = 20;

SQL> delete from emp where job = 'SALESMAN';

SQL> commit;

SQL> select * from emp order by deptno;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7934 MILLER     CLERK      7782 23.01.1982 00:00:00  1300           10
 7839 KING       PRESIDENT       17.11.1981 00:00:00  5000           10
 7782 CLARK      MANAGER    7839 09.06.1981 00:00:00  2450           10
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  6000           20
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  1600           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  5950           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  2200           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  6000           20
 7900 JAMES      CLERK      7698 03.12.1981 00:00:00   950           30
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30

10 rows selected.
Now we have the same state as in the last blog posting. But what, if there are changes in the parent workspace of PLAN_1? Other sessions might change the table in the LIVE workspace. And it would be the same situation, if we changed the table in PLAN_1 and PLAN_1 had child workspaces.

Propagating changes from Parent to Child Workspaces: Refresh

begin
  dbms_wm.gotoWorkspace('LIVE');
end;
/

SQL> update emp set job='SEN_CLERK' where deptno = 30 and job = 'CLERK';

SQL> select * from emp where deptno = 30;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7499 ALLEN      SALESMAN   7698 20.02.1981 00:00:00  1600   300     30
 7521 WARD       SALESMAN   7698 22.02.1981 00:00:00  1250   500     30
 7654 MARTIN     SALESMAN   7698 28.09.1981 00:00:00  1250  1400     30
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30
 7844 TURNER     SALESMAN   7698 08.09.1981 00:00:00  1500     0     30
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00   950           30

6 rows selected.
The changes in the LIVE workspace are not visible in the child workspace PLAN_1. And this is obvious, since the change has been done after the child workspace was created.
begin
  dbms_wm.gotoWorkspace('PLAN');
end;
/

SQL> select * from emp where deptno = 30;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30
 7900 JAMES      CLERK      7698 03.12.1981 00:00:00   950           30
The Refresh Operation (DBMS_WM.REFRESHWORKSPACE) propagates changes from the parent to a child workspace. Expressed otherwise, the simulation or planning scenario is being refreshed with the change of reality.
begin
  dbms_wm.refreshworkspace(
    workspace    => 'PLAN_1',
    auto_commit  => true,
    copy_data    => true
  );
end;
/

begin
  dbms_wm.gotoWorkspace('PLAN_1');
end;
/

SQL> select * from emp where deptno = 30;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7698 BLAKE      MANAGER    7839 01.05.1981 00:00:00  2850           30
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00   950           30
Now the change, which was done within the LIVE workspace, arrived in PLAN_1. The third parameter copy_data is very important.
  • false just updates the version information of the changed rows. The change is, so-to-say, being "pushed into the past - before the child workspaces were created". The effect of this is that the change is not only visible in the direct child workspace, but also in all subsequent child workspaces. So if PLAN_1 also had child workspaces the refresh operation would affect them as well.
  • true copies the changed rows to the child workspace. So the refresh operation only affects the given child workspaces. Subsequent child workspaces are not affected.

Conflict Management

Now we'll do the refresh operation again - but this time we'll (in the parent workspace) change some rows for which we know that they already have been changed in the child workspace. So in the LIVE workspace we have this situation:
begin
  dbms_wm.gotoWorkspace('LIVE');
end;
/

SQL> update emp set sal = sal * 3 where deptno = 20;

SQL> select * from emp where deptno = 20;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  2400           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  8925           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  9000           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  3300           20
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  9000           20
The table rows in PLAN_1 (the change was done above) looks like this ...
begin
  dbms_wm.gotoWorkspace('PLAN_1');
end;
/

SQL> select * from emp where deptno = 20;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  1600           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  5950           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  6000           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  2200           20
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  6000           20
The conflict is obvious - the same rows have been changed in LIVE as well as in PLAN_1. An attempt to refresh from LIVE to PLAN_1 ...
begin
  dbms_wm.refreshworkspace(
    workspace    => 'PLAN_1',
    auto_commit  => true,
    copy_data    => true
  );
end;
/

begin
*
ERROR in line 1:
ORA-20055: conflicts detected for workspace: 'PLAN_1' in table: 'WM.EMP'
ORA-06512: in "WMSYS.LT", line 5293
ORA-06512: in line 2
... will fail as well as the Merge operation from PLAN_1 to LIVE.
begin
  dbms_wm.mergeworkspace(
    workspace    => 'PLAN_1'
  );
end;
/

begin
*
ERROR in line 1:
ORA-20055: conflicts detected for workspace: 'PLAN_1' in table: 'WM.EMP'
ORA-06512: in "WMSYS.LT", line 6043
ORA-06512: in line 2
These conflicts must be resolved before the operation can succeed. For this, we have the procedures BEGINRESOLVE, RESOLVECONFLICTS and COMMITRESOLVE in DBMS_WM.
  • First, we'll start a conflict resolving session by calling DBMW_WM.BEGINRESOLVE.
    begin
      dbms_wm.beginResolve(
        workspace    => 'PLAN_1'
      );
    end;
    /
    
  • Then we'll use DBMS_WM.RESOLVECONFLICTS to decide, which version (Parent, Child or the common base) will succeed, when a conflict is being detected. DBMS_WNM.RESOLVECONFLICTS can be called multiple times. A SQL WHERE clause, given as the third parameter, determines the table row(s) for which this rule is to be applied. The following example will take the version from the parent workspace for all MANAGERs and the version from the child workspace for all other rows. But we cannot supply any WHERE clause we want: Workspace Manager forces us to use primary key columns in most cases. But with a bit of PL/SQL, this is an easy task ...
    begin
      for e in (select empno, job from emp) loop
        if e.job = 'MANAGER' then 
          dbms_wm.resolveConflicts (
            workspace    => 'PLAN_1',
            table_name   => 'EMP',
            where_clause => 'EMPNO = '||e.empno,
            keep         => 'PARENT'
          );
        else 
          dbms_wm.resolveConflicts (
            workspace    => 'PLAN_1',
            table_name   => 'EMP',
            where_clause => 'EMPNO = '||e.empno,
            keep         => 'CHILD'
          );
        end if;
      end loop;
    end;
    /
    
  • Finally we'll close the Conflict Resolving Session by calling DBMW_WM.COMMITRESOLVE. Make sure to issue an "normal" COMMIT beforehand.
    commit
    /
    
    begin
      dbms_wm.commitResolve(
        workspace    => 'PLAN_1'
      );
    end;
    /
    
Now the Merge and Refresh Operations will both succeed.
begin
  dbms_wm.mergeworkspace(
    workspace    => 'PLAN_1'
  );
end;
/

PL/SQL procedure successfully completed.
After a Merge Operation, LIVE contains the following version of EMP.
SQL> select * from emp where deptno = 20;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7369 SMITH      CLERK      7902 17.12.1980 00:00:00  1600           20
 7566 JONES      MANAGER    7839 02.04.1981 00:00:00  8925           20
 7788 SCOTT      ANALYST    7566 09.12.1982 00:00:00  6000           20
 7876 ADAMS      CLERK      7788 12.01.1983 00:00:00  2200           20
 7902 FORD       ANALYST    7566 03.12.1981 00:00:00  6000           20
In reality, of course, we don't know the conflicts in advance. The good news is that Workspace Manager provides Views which provide information about conflicts. We'll - again - start with the conflict situation.
begin
  dbms_wm.mergeworkspace(
    workspace    => 'PLAN_1'
  );
end;
/

begin
*
ERROR in line 1:
ORA-20055: conflicts detected for workspace: 'PLAN_1' in table: 'WM.EMP'
ORA-06512: in "WMSYS.LT", line 5293
ORA-06512: in line 2
First, we need to set the workspace, for which we want to see the conflicts: DBMS_WM.SETCONFLICTWORKSPACE. Having done this, we can examine the View EMP_CONF to see the conflicts, Workspace Manager has detected.
begin
  dbms_wm.setConflictWorkspace('PLAN_1');
end;
/

SQL> select wm_workspace, empno, ename, job, deptno, sal, wm_deleted from emp_conf order by empno, wm_workspace;

WM_WORKSPACE      EMPNO ENAME      JOB           DEPTNO        SAL WM_
------------ ---------- ---------- --------- ---------- ---------- ---
BASE               7369 SMITH      CLERK             20        800 NO
LIVE               7369 SMITH      CLERK             20       2400 NO
PLAN_1             7369 SMITH      CLERK             20       1600 NO
BASE               7566 JONES      MANAGER           20       2975 NO
LIVE               7566 JONES      MANAGER           20       8925 NO
PLAN_1             7566 JONES      MANAGER           20       5950 NO
BASE               7788 SCOTT      ANALYST           20       3000 NO
LIVE               7788 SCOTT      ANALYST           20       9000 NO
PLAN_1             7788 SCOTT      ANALYST           20       6000 NO
BASE               7876 ADAMS      CLERK             20       1100 NO
LIVE               7876 ADAMS      CLERK             20       3300 NO
PLAN_1             7876 ADAMS      CLERK             20       2200 NO
BASE               7902 FORD       ANALYST           20       3000 NO
LIVE               7902 FORD       ANALYST           20       9000 NO
PLAN_1             7902 FORD       ANALYST           20       6000 NO

15 rows selected.
EMP_CONF contains three rows for each conflict. BASE is the common base of both conflict workspaces - in our case this is the original value from the EMP table. The other two rows contain the version from both conflict workspaces - here LIVE and PLAN_1. Based on this information, we could easily generate a user interface (use APEX), in which the end user just clicks the version he wants to be the "right" one.

Savepoints in a Workspace

Savepoints in Workspace Manager are basically the same as Savepoints in "normal" database transactions. DBMS_WM.CREATESAVEPOINT creates a savepoint and DBMS_WM.ROLLBACKTOSP rolls back all changes, but only up to the given Savepoint. The following example illustrates the behaviour.
SQL> select * from emp where empno = 7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00   950           30

SQL> update emp set sal = 2000 where empno=7900;

SQL> commit;

SQL> select * from emp where empno = 7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00  2000           30


begin
  dbms_wm.createSavepoint(
    workspace      => 'PLAN_1',
    savepoint_name => 'SP_A',
    description    => '...',
    auto_commit    => true
  );
end;
/

SQL> update emp set sal = 5000 where empno=7900;

SQL> commit;

SQL> select * from emp where empno=7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00  5000           30

begin
  dbms_wm.rollbackToSp(
    workspace      => 'PLAN_1',
    savepoint_name => 'SP_A'
  );
end;
/

SQL> select * from emp where empno = 7900;

EMPNO ENAME      JOB         MGR HIREDATE              SAL  COMM DEPTNO
----- ---------- --------- ----- ------------------- ----- ----- ------
 7900 JAMES      SEN_CLERK  7698 03.12.1981 00:00:00  2000           30
For today, this should be enough information - so I think, I'll need a third posting to cover the remaining topics ...
  • Does Workspace Manager keep a history?
  • Is there are privilege model for Workspace Manager?
  • ...
Until then ... here's some material for further reading ...

Beliebte Postings