I can provide a solution for this but it comes with warnings - SQL doesn't do well with polymorphic embedded objects. If that isn't an issue then consider using a factory pattern. You must define a base class that extends %SerialObject and it must be instantiable. Then, override %OnNew() to dispatch to the concrete class that you wish to instantiate. Perhaps pass in a type argument. That %OnNew() can return an OREF (or a %Status if an error occurs).

If you are interested in this model then I can provide a small demo.

-Dan

Yes, of course. JSON_TABLE in the FROM clause is just another table and can be used as such. If you have wish to use indexes on JSON values stored in columns then you can always define indexes on computed values. That has been possible even before JSON_TABLE. Keep in mind that dynamic data does not always follow expectations as a field in a dynamic object can be a literal, another object or an array of values.

I previously posted about ASQ. You can use ASQ to derive a computed value in your class definition.

I don't know if this works on Windows systems but for macOS and Linux (I use Ubuntu), I defined a file in my home folder named .iris_init with these contents:

:alias pp do ##class(%ASQ.SetUtils).pp($*)
:alias parse try { do ##class(%ASQ.SetUtils).pp(##class(%ASQ.Parser).parse("$*").toDao()) } catch e { w !,e.AsSQLMessage() }
:alias asq try { do ##class(%ASQ.SetUtils).pp($1.apply("$2")) } catch e { w !,e.AsSQLMessage() }
:alias find do find^%z($1,$2,$3,$4)

This file is automatically loaded each time I establish an IRIS session.

Hi,

When you say "purging" do you mean that you are deleting the class definition? Or do you simply mean that you are deleting the data - perhaps by invoking %DeleteExtent or %KillExtent?

IRIS has three types of Classes - datatypes (not instantiable), registered (instantiable), and dynamic (not registered but instantiable). Your question seems to involve only registered types. While any data can be serialized/deserialized, we define an interface for doing so - the so-called Storage Interface. Storage Definition is a class member that can be included in just about any class but we only recognize and process it when the class is either a serial or persistent class. A serial class is a class that extends %SerialObject (meaning that %SerialObject is in its primary superclass hierarchy) and a persistent class is a class that extends %Persistent (again, %Persistent is in its primary superclass hierarchy). For these classes, the class compiler will ensure there is a fully defined Storage Definition. As with all class members, the Storage Definition has a type class. This type class is often referred to as the Storage Class - it is a class that implements the Storage Interface. The Storage Definition defines the format of the serialized object. For serial classes, this serialized object value is not directly stored anywhere. Rather, the serialized value is expected to be stored as a value embedded inside of another object. For persistent classes, an object is serialized, perhaps into multiple nodes, and stored in a global at a location defined by the IDKEY index. Persistent classes can also use globals for storing other things such as indexes, counters, metadata and so on. The set of all globals used by a persistent class is referred to as the Extent Set which is, by default, registered with the Extent Manager.

When you remove the Storage Definition from a serial or persistent class, then it is possible that any existing data serialized using the removed Storage Definition will not be able to be deserialized as it might not be compatible with a newly defined Storage Definition. We only recommend removing a Storage Definition from a class if there are no serialized/stored instances of the class present.

When you delete a persistent class, you can also delete the data and the Extent Set by adding the /deleteextent qualifier (or the 'e' flag) to the command. You can also delete the data by invoking %DeleteExtent (logical delete) or %KillExtent (physical delete - no constraints are enforced). That only deletes the data and leaves the Extent Set registered with the Extent Manager. You can delete the Extent Set from the Extent Registry by using utilities we provide. Start here: https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic....

I am happy to help if you wish (I wrote much of the core serialization/deserialization/storage code) but I will need to know more about the types of classes you are using and the issues you have.

-Dan

We posted a sample project to the community github account. There are java and dotnet versions of the project. Take a look and we would appreciate any feedback you may want to offer.

https://github.com/intersystems-community/samples-dynamicgateway-java

https://github.com/intersystems-community/samples-dynamicgateway-dotnet

Please note that there are some deficiencies when working with parameterized Java types when using the new External Java Server. Some of these were discovered during the refactoring of the %Net.Remote.Java.Test code. Some have already been addressed but not yet included in a release.

I came up with this quick and dirty example. I did discover a problem when I executed this using the Database Console in IntelliJ - evidently the IRIS Server requires statement results to be only ResultSets! (I've reported that problem). If the result of executing the insert statements is not added to the %sqlcontext then this works through client connections, otherwise an error is reported. I'll include the result of executing this from the command line and then displaying the results. Also, the results from executing this from a client with the insert results suppressed.

DROP TABLE IF EXISTS demo_person;
DROP PROCEDURE IF EXISTS multi_results;

CREATE TABLE demo_person (name VARCHAR(20), age INT, home_city VARCHAR(40));

CREATE PROCEDURE multi_results() RESULT SETS
    LANGUAGE OBJECTSCRIPT
{
    set ins = $system.SQL.Prepare("insert into demo_person(name, age, home_city) VALUES(?,?,?)")
    do $system.SQL.Execute("TRUNCATE TABLE demo_person")
    do %sqlcontext.AddResultSet(ins.execute("Dan", 25, "Miami"))
    do %sqlcontext.AddResultSet(ins.execute("Jorge", 32, "Tampa"))
    do %sqlcontext.AddResultSet(ins.execute("Enrico", 29, "Turin"))
    do %sqlcontext.AddResultSet(ins.execute("Alexy", 21, "London"))
    do %sqlcontext.AddResultSet($system.SQL.Execute("SELECT name, age, home_city FROM demo_person ORDER BY age DESC"))
};

CALL multi_results();
USER>set result = $system.SQL.Execute("call multi_results()")

USER>do result.%Display()

Dumping result #1
1 Row Affected

Dumping result #2
1 Row Affected

Dumping result #3
1 Row Affected

Dumping result #4
1 Row Affected

Dumping result #5
name    age    home_city
Jorge    32    Tampa
Enrico    29    Turin
Dan    25    Miami
Alexy    21    London

4 Rows(s) Affected

The results when not adding the insert results as displayed by a database console:

name age home_city
Jorge 32 Tampa
Enrico 29 Turin
Dan 25 Miami
Alexy 21 London

Along with a co-worker, I worked on this for three days. The good news is that I have something to show. The not-so-good news is that we discovered a bug and a deficiency. The better news is that the next version of JDBC to be released will include these fixes and enhancements. Also good news is that most of the old %Net.Remote.Java.Test class works with minor refactoring.

Mo Cheng will be adding information on how to leverage a more backward compatible Java Gateway. The legacy Java Gateway used generated classes and the new Dynamic Gateway ($system.external) does not. I'll leave that description for Mo to provide.

For now - pending review - I am ready to share a new class (actually a full project). I am just waiting for a place for this to be posted. In the meantime, I could post code snips here. Let me know.

In general, a <LIST> error indicates a malformed $list value.

USER>set bad = "something wrong"   

USER>write $li(bad,1)

WRITE $LI(bad,1)
^
<LIST>
USER>set good = $listbuild("something right")

USER>write $li(good,1)
something right

In %SerializeObject, this typically is caused by an expectation that a value is a valid list but it is not. In a more complex class that extends %SerialObject, one that contains at least one property whose type class is a swizzlable (persistent, serial, stream), $list(value, n) is used to extract the object value (id or serial value) from a value that is expected to be an OID. While OID's an be more complex, the typical OID format is $list(<serialized object value>, <most specific type class>). <serialized object value> can be a simple ID and <most specific type class> (MSTC) is the class of which the object is an instance but not an instance of any subclass of MSTC.

That isn't the only possibility but it is the most likely. I can't be more specific without seeing the full class definition.

-Dan

Enrico,

For some reason I didn't get an email alert that you had replied. I'm sure it is buried in my "group" mail somehwere - that's my fault and I'll try to get that sorted.

%Net.Remote.DotNet.Test - I can't help much with that but I certainly can with %Net.Remote.Java.Test. I'll take a look at that now.

$system.external.* is just a helper to get a gateway connection to an external language server such as the Java Server. I refer to the "gateway" as the connection. Maybe that doesn't work for every one. Sorry :(

Once you have a gateway connection you can interact with the external Server using methods supported by that connection. Some of those methods are still unknown to me so I'll have to do some research.

Stay tuned!

Dan

Hi Enrico,

I am listening! $system.external does not replace the old Java Gateway but the old Java and JDBC Gateways should still be available. I am not intimately familiar with either of those although I did some of the refactoring work with them both. I am not aware of any user code changes that are necessary to keep using them.

If you want to use the new External Java Server then you will necessarily need to write new code. I am happy to provide demos showing how this can be done. I can also assist you with implementing your own project to get you started. Just let me know how you wish to proceed.

As for leveraging existing Java libraries, you should be able to do many things without writing new Java code but not everything. If you have some examples of libraries you have attempted to use but were not successful, please provide information so that I can attempt to replicate your issues.

As always, I am here, listening, and happy to help!!

-Dan

Hi,

Using the new(ish) External Java Server and Gateway connection, this worked for me. I copied your class, refactored it for the Apache PDFBox version 3.0.0 and then called it from the command line. I did receive some warnings that have nothing to do with PDFBox, they are related to the larger project that I placed this code into. The call worked just fine. I don't know exactly when the new external language server/gateway support was added but it is documented in 2021.1: https://docs.intersystems.com/iris20211/csp/docbook/DocBook.UI.Page.cls?KEY=BEXTSERV_intro. I'm pasting the IRIS Session commands I used:

USER>set java = $system.external.getJavaGateway()

USER>do java.addToPath("<path to my project/target/intersystems-demo-1.0-SNAPSHOT.jar")

USER>set text = java.invoke("demo.intersystems.utils.PdfToText","getText","/home/danp/Downloads/DM32.2-2017-00157-ambiguity-in-JSON-array-constructor.pdf")

USER>zw text
text="ISO/IEC JTC1/SC32 WG3:CMH-023"_$c(10)_"ANSI INCITS DM32.2-2017-00157"_$c(10)_"- 1 of 7 -"_$c(10)_"Title: Ambiguity in <JSON array constructor>"_$c(10)_"Author: <<snipped>>

I even have a nice way to return the text as an instance of %Stream.Object if that is interesting to you.

HTH,

Dan

I don't recommend using the "embedded extent" model. It might be possible to achieve this model but it isn't simple to do and it does produce some odd behavior. I could, reluctantly, describe how this is done. It may not apply to this case anyway. The default serialization of a LIST collection is as an embedded list and we do not support - currently - a serialized list as a child table.

This is an example of a MANY to MANY relationship. IRIS does not support this relationship type. Rather, we recommend using the Associative Entity model where there is a third class/table that maintains the relationship. The structure of the AE class/table is quite simple, consisting of two properties/columns, one referencing of the related classes and the other referencing the other related classes.

If restructuring the user class is not possible then the %OnDelete recommendation is good. Perhaps it would be better to use a FOREACH=ROW/OBJECT, ONAFTER=DELETE trigger to iterate over the collection and delete each referenced item. Triggers have the added advantage of working with both Object and SQL filing operations. Keep in mind that collections use OID format when crafting the delete.

Interesting question. There is no good option to manipulate the ObjectRegistry but you do have options to access stored data. My recommended option you've already taken off the table.

The FOREACH=ROW/OBJECT trigger is by far the best and easiest solution if you require access to the version of an object that is currently stored. There are several reasons why this is the best option, perhaps the most important being that ROW/OBJECT triggers are consistently applied between Objects and SQL. Of course, the same restrictions exist for triggers that exist for %OnBeforeSave - we don't recommend modifying objects/rows.

Another option is to use <property>GetStored. This may not work for every situation but it does allow code to retrieve the value of a property directly from storage. I believe this is restricted to what we call "default storage". That isn't an entirely true statement but it is for common storage types (SQL Mapped storage being the other common type).

USER>zw ^demo.person.1(1)
^demo.person.1(1)=$lb("Doe, John","123 Main Street","Cambridge","MA","02142","[{""type"":""mobile"",""country_code"":""1"",""number"":""999-999-9999""},{""type"":""work"",""country_code"":""1"",""number"":""888-888-8888""}]")

USER>set person = ##class(demo.intersystems.Person).%OpenId(1)

USER>write person.name
Doe, John
USER>set person.name = "Richard, Maurice"

USER>write person.nameGetStored(person.%Id())
Doe, John
USER>write person.name
Richard, Maurice

Of course, you can always use SQL to retrieve stored values.

For the intrepid explorer, there is another option. This code isn't supported as the %Load* api's aren't supposed to be public. I'm sure most people have examined generated code and have discovered these methods so I'm divulging any big secrets here. First, the results and then the code.

USER>set person = ##class(demo.intersystems.Person).%OpenId(1)

USER>set person.name = "Enono, William"

USER>set sperson = ##class(demo.intersystems.Person).GetStored(person.%Id())

USER>write sperson.name
Leavitt, Timothy
USER>write person.name
Enono, William

Fair warning - this code isn't supported, it is considered to be user-implemented code. I also didn't rigorously test this code. I added this class method to my demo.intersystems.Person class:

ClassMethod GetStored(id As %Integer) As demo.intersystems.Person
{
    try {
        set obj = ..%New()
        set cur = obj.%Concurrency
        set obj.%Concurrency = 0
        $$$THROWONERROR(sc,obj.%LoadInit(,,1))
        $$$THROWONERROR(sc, obj.%LoadData(id))
        set obj.%Concurrency = cur
        do obj.%SetModified(0)
    } catch e {
        set obj = $$$NULLOREF
    }
    return obj
}

The object referenced by the oref returned by this code is not assigned an id. If you were to save this object it would create a new stored object and it would be assigned its own ID. Exercise caution.