Logo Oracle Deutschland März 2017
JSON in 12.2: JSON Generierung, neues Data Guide Konzept, Objekttypen
Ulrike Schwinn

JSON Daten werden häufig in NoSQL oder anderen speziellen Datenbanken gespeichert bzw. damit in Verbindung gebracht. Diese erlauben zwar die Speicherung und den Zugriff der Daten, aber weisen kein vergleichbares Konsistenzmodell, Transaktionsmodell und andere Standardfunktionalitäten von relationalen Datenbanken auf. "All your Data" in der Oracle Datenbank gilt immer noch auch und gerade in der aktuellen Datenbankversion Oracle Database 12c Release 2: Sei es im Bereich XML, bei Texten mit linguistischen Suchanforderungen oder auch für Geoinformationen (Spatial) und im semantischen Netzwerk Umfeld um nur einige Beispiele zu nennen.

So wurden mit 12c (12.1.0.2) dann auch JSON Zugriffe möglich. Die Verwendung ist dabei ganz einfach: Man definiert eine Datenbankspalte mit einem beliebigen Datentyp für Textstrings (wie zum Beispiel VARCHAR2 oder CLOB). (Hinweis: VARCHAR2 Felder können übrigens seit 12c bis zu 32k groß werden, sofern die Möglichkeit der Erweiterung der Datentypen aktiviert wurde.) Mit der Bedingung IS JSON kann zusätzlich der Inhalt validiert werden - auf Wohlgeformtheit oder auf die Art der Syntaxverwendung (STRICT oder LAX). Die Zugriffe erfolgen dann mit Standardmitteln und neu eingeführten SQL/JSON Funktionen. Liegen die JSON Daten in einem DMP Format außerhalb der Datenbank vor, kann sogar über die EXTERNAL TABLE Funktion zugegriffen werden. Es ist keine zusätzliche Installation von Software erforderlich; eine JSON Unterstützung ist in allen Cloud und Softwareinstallation der Datenbank sofort verfügbar. Mehr über die Basisfunktionen in der Oracle Datenbank können Sie im Community Beitrag "JSON in der Datenbank mit Basisfunktionen" nachlesen.

In Oracle 12.2 gibt es jetzt auch Lösungen zu folgenden Fragestellungen: Wie kann man beispielsweise JSON mit Datenbankmitteln generieren? Wie lassen sich JSON Inhalte in der Datenbank mit PL/SQL Mitteln manipulieren. Wie kann man schnell und einfach relationale Views erzeugen? Wie lässt sich umfassend in JSON suchen? Für hohe Performance Anforderungen ist zusätzlich zu den bestehenden In-Memory Zugriffen ein spezielles optimiertes JSON Dokument Handling in 12.2. implementiert worden.

Folgende Abschnitte geben einen Einblick in ausgewählte neue Funktionen der Oracle Database 12.2 und illustrieren die Verwendung an kleinen Code Beispielen. Um ein vollständiges Bild zu bekommen, empfehle ich die Links im letzten Abschnitt.

JSON Daten generieren mit JSON/SQL Operatoren

Kennt man die entsprechenden Werkzeuge, lassen sich auch schon vor 12.2 JSON Daten aus der Datenbank generieren. Arbeitet man beispielsweise mit APEX kann man JSON Dokumente mit der Funktion APEX_JSON erzeugen. Eine andere Möglichkeit bietet das Linemode Werkzeug SQLcl. Mit dem Format Kommando SET SQLFORMAT JSON wird beispielsweise automatisch eine Ausgabe im JSON Format angezeigt. In 12.2 gibt es nun eine dritte Möglichkeit unter Verwendung reiner SQL Mittel und den neuen SQL/JSON Operatoren wie JSON_ARRAY, JSON_ARRAYAGG, JSON_OBJECT und JSON_OBJECTAGG. Schauen wir uns ein paar einfache Beispiele dazu an. Mit JSON_ARRAY wird ein zugehöriges JSON Feld ausgegeben, JSON_OBJECT hingegen erzeugt ein "Key:Value" Paar aus jeder referenzierten Spalte.
SQL> select JSON_ARRAY(empno, ename, mgr, deptno) emp
     from scott.emp where deptno=10; 

EMP
--------------------------------------------------------------------------------
[7782,"CLARK",7839,10]
[7839,"KING",10]
[7934,"MILLER",7782,10]

SQL> select JSON_OBJECT('empid' is empno,'empname' is ename, 'Managerid' is mgr, 'Deptid' is deptno) emp
     from scott.emp where deptno=10;

EMP
--------------------------------------------------------------------------------
{"empid":7782,"empname":"CLARK","Managerid":7839,"Deptid":10}
{"empid":7839,"empname":"KING","Managerid":null,"Deptid":10}
{"empid":7934,"empname":"MILLER","Managerid":7782,"Deptid":10}

SQL> select JSON_OBJECT(object_type is object_name,'Status' is STATUS) from USER_OBJECTS
     where OBJECT_TYPE like 'PACK%';

JSON_OBJECT(OBJECT_TYPEISOBJECT_NAME,'STATUS'ISSTATUS)
--------------------------------------------------------------------------------
{"PACKAGE":"PACK1","Status":"VALID"}
{"PACKAGE":"STOCK_TRACK_PACK","Status":"VALID"}
{"PACKAGE BODY":"STOCK_TRACK_PACK","Status":"VALID"}
Interessant ist auch die Möglichkeit ein "Key:Value" Paar bzgl. einer bestimmten Spalte zu aggregieren. Um die reine Funktionsweise aufzuzeigen, bleiben wir bei der einfachen EMP Tabelle. Wir haben 14 Angestellte, mit 6 verschiedenen Managern. Im nächsten Beispiel geben wir pro Manager die entsprechenden Angestellten mit Berufsbezeichnung als "Key:Value" Paar aus. Das Ergebnis sieht dann folgendermassen aus.
SQL> select json_objectagg (ename, job) employees_per_manager from emp
     group by mgr;

EMPLOYEES
--------------------------------------------------------------------------------
{"SCOTT":"ANALYST","FORD":"ANALYST"}
{"ALLEN":"SALESMAN","JAMES":"CLERK","TURNER":"SALESMAN","MARTIN":"SALESMAN","WARD":"SALESMAN"}
{"MILLER":"CLERK"}
{"ADAMS":"CLERK"}
{"JONES":"MANAGER","CLARK":"MANAGER","BLAKE":"MANAGER"}
{"SMITH":"CLERK"}
{"KING":"PRESIDENT"}
Natürlich lassen sich diese Operatoren auch miteinander kombinieren und ergänzen. Weitere Beispiele sind im JSON Handbuch, in Artikeln im Internet oder auch auf Github zu finden (siehe auch Informationen unten).

PL/SQL Integration mit JSON Datenstrukturen

Eine neue PL/SQL API in 12.2 ermöglicht einen Zugriff auf JSON Dokumente, die in der Datenbank gespeichert sind. Damit ist es möglich "Key:Value" Paare hinzuzufügen, zu verändern oder zu löschen. Man kann durch ein JSON Dokument navigieren vergleichbar mit dem XML Document Object Model (DOM). Die neuen Objekttypen mit ihren zugehörigen Funktionen können bei der Umsetzung hilfreich sein: JSON_ELEMENT_T, JSON_OBJECT_T und JSON_ARRAY_T. JSON_OBJECT_T und JSON_ARRAY_T sind eine Erweiterung des Objekttyps JSON_ELEMENT_T. Ein DESCRIBE auf die drei Objekttypen gibt erste Hinweise auf die Verwendungsmöglichkeiten. Der Basistyp JSON_ELEMENT_T liefert beispielsweise eine PARSE Funktion um die interne Repräsentation für die weitere Verarbeitung zur Verfügung zu stellen.
SQL> desc json_element_t
 JSON_ELEMENT_T is NOT FINAL
 JSON_ELEMENT_T is NOT INSTANTIABLE
 Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 DOM                                                SYS.JDOM_T
METHOD
------
 STATIC FUNCTION PARSE RETURNS JSON_ELEMENT_T
 Argument Name                  Type                    In/Out Default?
 ------------------------------ ----------------------- ------ --------
 JSN                            VARCHAR2                IN
Darüberhinaus existiert ein spezielles Error Handling, PUT und GET Methoden, Clone und Rename/Remove Funktionen, um nur einige Beispiele zu nennen. Da die Erläuterung der einzelnen Methoden und Konstruktoren zu weit führen würde, möchte ich an einem einfachen Beispiel die generelle Vorgehensweise demonstrieren. Wir verwenden dazu die Beispielumgebung aus dem Community Beitrag "JSON in der Datenbank mit Basisfunktionen". In der Spalte JSON_DOCUMENT, in der die JSON Daten gespeichert sind, wird der Wert des Keys User verändert (von 'CJOHNSON' in den Wert 'USCHWINN).
declare
     cursor getDocuments 
     is
     select JSON_DOCUMENT
     from JSON_TAB j
     where json_value(json_document,'$.User') = 'CJOHNSON' for UPDATE;   
     V_RESULT            JSON_ELEMENT_T;    
     V_DOCUMENT_OBJECT   JSON_OBJECT_T;
     V_NEW_DOCUMENT      VARCHAR2(4000);
begin
    for doc in getdocuments loop 
--  setup of the internal representation     
    V_RESULT := JSON_ELEMENT_T.parse(doc.json_DOCUMENT);
    V_DOCUMENT_OBJECT := treat(V_RESULT as JSON_OBJECT_T);
 
    if (V_Document_OBJECT.get_String('User') = 'CJOHNSON') then
       V_document_object.put('User','USCHWINN');
       end if;
-- transformation to a string 
   V_NEW_DOCUMENT := V_DOCUMENT_OBJECT.to_string();

-- update operation
    update %TABLE_NAME%
       set JSON_DOCUMENT = V_NEW_DOCUMENT
     where current of getDocuments;
  commit;
end loop;
end;
/
Eine genaue Erläuterung findet sich im Database PL/SQL Packages and Types Reference im Kapitel JSON Data Structures. Weitere Beispiele sind auf Github zu finden.

View Erzeugen und Textsuche ganz einfach mit der neuen Struktur Data Guide

Hat man mit großen und unübersichtlichen JSON Daten zu tun, stellt sich schnell die Frage, wie man darin performant nach einem Wert oder Eintrag suchen kann. Auch die Möglichkeit eine relationale Sichtweise auf JSON Dokumente zur Verfügung zu stellen - wie im Falle von XML - ist eine übliche Fragestellung. Die Basisfunktionen in 12.1 liefern schon erste Möglichkeiten diese Anforderungen zu lösen. Allerdings ist die Handhabung etwas unhandlich. Eine Vereinfachung und weitere Funktionen verspricht nun das neue Konzept in 12.2. Die Metadaten eines JSON Dokuments können nun im Data Dictionary gespeichert werden. Optional wird ein zugehöriger Index zur Volltextsuche zur Verfügung gestellt. Die neue Struktur hat den treffenden Namen Data Guide. Damit ist es nun möglich schnell und einfach mit einem Befehl relationale Views zu erzeugen, eine performante Suche im Text zu gestalten und sogar automatisch virtuelle Spalten zu einer View hinzuzufügen sobald sich der Inhalt des JSON Dokuments erweitert.

Starten wir mit dem Erzeugen eines Data Guides. Auch hier nehmen wir die Tabelle JSON_TAB als Grundlage. Je nach Verwendung der Syntax gibt es unterschiedliche Varianten ein Data Guide Konstrukt zu erzeugen - beispielsweise mit oder ohne Text Index. Wir entscheiden uns hier für die einfachste Form der Syntax (erstes Beispiel), die einige Defaulteinstellungen mit sich bringt - wie zum Beispiel das automatische Erzeugen eines Textindex. Ein paar weitere Beispiele sind ebenfalls gelistet.
create search index JSON_TAB_GUIDE 
  on JSON_TAB (JSON_DOCUMENT) for json;

-- ohne Oracle Text Index (auch Search Index)
create search index JSON_TAB_GUIDE ON JSON_TAB (JSON_DOCUMENT) for json
PARAMETERS ('DATAGUIDE ON SEARCH_ON NONE');

-- mit Search Index und Synchronisierung bei DML nach COMMIT
CREATE SEARCH INDEX JSON_TAB_GUIDE ON JSON_TAB (JSON_DOCUMENT) for json 
PARAMETERS ('DATAGUIDE ON SYNC (ON COMMIT)')

-- ohne Data Guide nur Search Index
create search index JSON_TAB_GUIDE ON JSON_TAB (JSON_DOCUMENT) for json
PARAMETERS ('DATAGUIDE OFF SEARCH_ON TEXT')
Es dauert eine kleine Weile, bis die Strukturen analysiert und die zugehörigen neuen Objekte im Data Dictionary angelegt worden sind. Kontrolliert man USER_OBJECTS wird man feststellen, dass eine ganze Reihe neuer zusätzlicher Objekte wie Indizes, Tabellen und LOBs erzeugt wurden. Die Oracle Text Experten werden auch einen entsprechenden Eintarg in CTX_USER_INDEXES finden.
SQL> select object_name, object_type from user_objects where created>= sysdate-1/24;

OBJECT_NAME                    OBJECT_TYPE            
------------------------------ -----------------------
DR$JSON_TAB_GUIDE$I            TABLE                  
JSON_TAB_GUIDE                 INDEX                 
SYS_IL0000098698C00006$$       INDEX                  
...
DR$JSON_TAB_GUIDE$SNI          INDEX                   
DR$JSON_TAB_GUIDE$STI          INDEX                  

30 rows selected.
Genaue Informationen über die Syntax findet man übrigens im Oracle Text Reference Guide. So kann man beispielsweise auf das Erzeugen eines Text Index verzichten, die Synchronisierung beeinflussen u.v.m. Kennt man sich zusätzlich etwas mit Oracle Text Funktionen aus, kann man einige Parallelen in der Syntax und der weiteren Verwendung erkennen.

Wo liegen nun die Strukturen und was genau wird gespeichert? Die Strukturen sind beispielsweise in USER_JSON_DATAGUIDES zu finden oder lassen sich über die neue Funktion DBMS_JSON.GET_INDEX_DATAGUIDE ausgeben.
SQL> set pagesize 100 long 10000
SQL> select dbms_json.get_index_dataguide (
        OWNER     => 'SCOTT',
        TABLENAME => 'JSON_TAB',
        JCOLNAME  => 'JSON_DOCUMENT',
        FORMAT    => dbms_json.format_hierarchical,
        PRETTY    => dbms_json.pretty
     ) data_guide
    from dual;


DATA_GUIDE
--------------------------------------------------------------------------------
{
  "type" : "object",
  "properties" :
  {
    "User" :
    {
      "type" : "string",
      "o:length" : 8,
      "o:preferred_column_name" : "JSON_DOCUMENT$User"
    },
    "PONumber" :
    {
      "type" : "number",
      "o:length" : 8,
      "o:preferred_column_name" : "JSON_DOCUMENT$PONumber"
    },
    "LineItems" :
    {
      "type" : "array",
      "o:length" : 1024,
      "o:preferred_column_name" : "JSON_DOCUMENT$LineItems",
      "items" :
      {
        "properties" :
        {
          "Part" :
          {
            "type" : "object",
            "o:length" : 256,
            "o:preferred_column_name" : "JSON_DOCUMENT$Part",
            "properties" :
            {
              "UPCCode" :
              {
                "type" : "number",
                "o:length" : 16,
                "o:preferred_column_name" : "JSON_DOCUMENT$UPCCode"
              },
              "UnitPrice" :
              {
                "type" : "number",
                "o:length" : 8,
                "o:preferred_column_name" : "JSON_DOCUMENT$UnitPrice"
              },
              "Description" :
              {
                "type" : "string",
                "o:length" : 128,
                "o:preferred_column_name" : "JSON_DOCUMENT$Description"
              }
            }
          },
          "Quantity" :
          {
            "type" : "number",
            "o:length" : 4,
            "o:preferred_column_name" : "JSON_DOCUMENT$Quantity"
          },
          "ItemNumber" :
          {
            "type" : "number",
            "o:length" : 1,
            "o:preferred_column_name" : "JSON_DOCUMENT$ItemNumber"
          }
        }
      }
    },
    "Reference" :
    {
      "type" : "string",
      "o:length" : 32,
      "o:preferred_column_name" : "JSON_DOCUMENT$Reference"
    },
    "Requestor" :
    {
      "type" : "string",
      "o:length" : 32,
      "o:preferred_column_name" : "JSON_DOCUMENT$Requestor"
    },
    "CostCenter" :
    {
      "type" : "string",
      "o:length" : 4,
      "o:preferred_column_name" : "JSON_DOCUMENT$CostCenter"
    },
    "AllowPartialShipment" :
    {
      "type" : "boolean",
      "o:length" : 4,
      "o:preferred_column_name" : "JSON_DOCUMENT$AllowPartialShipment"
    },
    "ShippingInstructions" :
    {
      "type" : "object",
      "o:length" : 256,
      "o:preferred_column_name" : "JSON_DOCUMENT$ShippingInstructions",
      "properties" :
      {
        "name" :
        {
          "type" : "string",
          "o:length" : 32,
          "o:preferred_column_name" : "JSON_DOCUMENT$name"
        },
        "Phone" :
        {
          "type" : "array",
          "o:length" : 128,
          "o:preferred_column_name" : "JSON_DOCUMENT$Phone",
          "items" :
          {
            "properties" :
            {
              "type" :
              {
                "type" : "string",
                "o:length" : 8,
                "o:preferred_column_name" : "JSON_DOCUMENT$type"
              },
              "number" :
              {
                "type" : "string",
                "o:length" : 16,
                "o:preferred_column_name" : "JSON_DOCUMENT$number"
              }
            }
          }
        },
        "Address" :
        {
          "oneOf" :
          [
            {
              "type" : "string",
              "o:length" : 1,
              "o:preferred_column_name" : "JSON_DOCUMENT$Address"
            },
            {
              "type" : "object",
              "o:length" : 256,
              "o:preferred_column_name" : "JSON_DOCUMENT$Address_1",
              "properties" :
              {
                "city" :
                {
                  "type" : "string",
                  "o:length" : 32,
                  "o:preferred_column_name" : "JSON_DOCUMENT$city"
                },
                "state" :
                {
                  "type" : "string",
                  "o:length" : 2,
                  "o:preferred_column_name" : "JSON_DOCUMENT$state"
                },
                "county" :
                {
                  "type" : "string",
                  "o:length" : 8,
                  "o:preferred_column_name" : "JSON_DOCUMENT$county"
                },
                "street" :
                {
                  "type" : "string",
                  "o:length" : 64,
                  "o:preferred_column_name" : "JSON_DOCUMENT$street"
                },
                "country" :
                {
                  "type" : "string",
                  "o:length" : 32,
                  "o:preferred_column_name" : "JSON_DOCUMENT$country"
                },
                "zipCode" :
                {
                  "type" : "number",
                  "o:length" : 8,
                  "o:preferred_column_name" : "JSON_DOCUMENT$zipCode"
                },
                "postcode" :
                {
                  "type" : "string",
                  "o:length" : 8,
                  "o:preferred_column_name" : "JSON_DOCUMENT$postcode"
                },
                "province" :
                {
                  "type" : "string",
                  "o:length" : 2,
                  "o:preferred_column_name" : "JSON_DOCUMENT$province"
                }
              }
            }
          ]
        }
      }
    },
    "Special Instructions" :
    {
      "type" : "string",
      "o:length" : 32,
      "o:preferred_column_name" : "JSON_DOCUMENT$SpecialInstructions"
    }
  }
}

Das Ganze sieht schon sehr vielversprechend aus: Es werden die einzelnen Elemente, Datentypen und mögliche Spaltennamen (hier "JSON_DOCUMENT$User") ausgegeben. Das Package DBMS_JSON kann dann im nächsten Schritt zur Erzeugung einer Standard View (hier: JSON_PO_VIEW) dienen.
begin
  dbms_json.create_view_on_path (
    viewName   => 'JSON_PO_VIEW',
    tableName  => 'JSON_TAB',
    jcolname   => 'JSON_DOCUMENT',
    path       => '$');
end;
Ein Blick auf die Objekte des Users beweist, dass wir damit die neue View JSON_PO_VIEW erzeugt haben. Die Spaltennamen lassen sich anschliessend mit der Prozedur DBMS_JSON.RENAME_COLUMN umbennen.
SQL> desc JSON_PO_VIEW
  Name                                      Null?    Type
 ----------------------------------------- -------- ----------------------------
 ID                                        NOT NULL NUMBER
 JSON_DOCUMENT$User                                 VARCHAR2(8)
 JSON_DOCUMENT$PONumber                             NUMBER
 JSON_DOCUMENT$Reference                            VARCHAR2(32)
 JSON_DOCUMENT$Requestor                            VARCHAR2(32)
 JSON_DOCUMENT$CostCenter                           VARCHAR2(4)
 JSON_DOCUMENT$AllowPartialShipment                 VARCHAR2(4)
 JSON_DOCUMENT$name                                 VARCHAR2(32)
 JSON_DOCUMENT$Address                              VARCHAR2(1)
 JSON_DOCUMENT$city                                 VARCHAR2(32)
 JSON_DOCUMENT$state                                VARCHAR2(2)
 JSON_DOCUMENT$county                               VARCHAR2(8)
 JSON_DOCUMENT$street                               VARCHAR2(64)
 JSON_DOCUMENT$country                              VARCHAR2(32)
 JSON_DOCUMENT$zipCode                              NUMBER
 JSON_DOCUMENT$postcode                             VARCHAR2(8)
 JSON_DOCUMENT$province                             VARCHAR2(2)
 JSON_DOCUMENT$SpecialInstructions                  VARCHAR2(32)
 JSON_DOCUMENT$UPCCode                              NUMBER
 JSON_DOCUMENT$UnitPrice                            NUMBER
 JSON_DOCUMENT$Description                          VARCHAR2(128)
 JSON_DOCUMENT$Quantity                             NUMBER
 JSON_DOCUMENT$ItemNumber                           NUMBER
 JSON_DOCUMENT$type                                 VARCHAR2(8)
 JSON_DOCUMENT$number                               VARCHAR2(16)
Zum Abschluss möchte ich noch auf die Verwendung des neuen Textindex eingehen. Ein typisches Beispiel liefert eine Ad-hoc Abfrage mit JSON_EXISTS. Der Ausführungsplan bestätigt dabei die Verwendung des Textindex (siehe Operation DOMAIN INDEX).

Das letzte Beispiel zeigt eine Volltextsuche mit einem Oracle Text Operator. Die Aufgabe besteht darin die Dokumente zu finden, bei denen der Wert von Requestor auf "Sarah", "Sarath" oder einem ähnlichen Namen lautet. Hier bietet sich der Einsatz des Oracle Text Operators FUZZY oder SOUNDEX an. Das Ergebnis sieht dann folgendermaßen aus.

Möchte man mehr über die Möglichkeiten der Oracle Text Abfragen wissen, eignet sich die Lektüre des Oracle Text Handbuchs.

Hinweis zur Lizenzierung

Die JSON Funktionen in der Datenbank sind in allen Editionen (Standard und Enterprise) und Cloudangeboten der Datenbank ab Oracle Database 12c verfügbar. Es ist keine zusätzliche Lizenzierung erforderlich.

Weitere Informationen

 

Zurück zum Anfang des Artikels

Zurück zur Community-Seite