Rechercher

Article
· Mar 2, 2023 5m read

Tutorial - Working with %Query #2

My previous article introduced you to SQL based Basic Class Query
where a clever wizard created all the required code for you and your essential
contribution was an SQL statement. 

Now we enter the real Custom Class Query that provides more freedom but
requires a deeper understanding of the mechanic behind the scene.
The full code example is again on GitHub

Some things haven't changed:

  • demo data are the same
  • consumption of the query is also unchanged
  • all handling of ODBC / JDBC protocol is still generated.

So what is different?

  • You need a header QUERY statement to declare your input parameters but
    • also the record layout for your output. ROWSPEC
  • you have to provide an Execute method to initialize your query and consume your input parameters
  • a Close method to clean up your environment
  • and a Fetch method that does the Job.
    • it is called row by row until you set AtEnd=1. (it is passed by reference)
    • and you return variable Row (also passed by reference) as $LB() structure
    • so it is obvious that the resulting Row is shorter than MAXSTRING
    • and this is our challenge to present your stream

so we take our choice: 
as before we use the Studio's Query Wizard

we have 3 parameters: idfrom (first ID), isto (last ID), maxtxt (maxim text from stream)

and new the layout of our output (ROWSPEC), similar to above

for our Stream, we need to overwrite %String defaults to match ODBC / JDBC
The type needs to be %String(EXTERNALSQLTYPE = "LONGVARCHAR", MAXLEN = "")​​​
This is the generated code framework:

Query Q1(
    idfrom As %Integer = 1,
    idto As %Integer = 0,
    maxtxt As %Integer = 25) As %Query
    (ROWSPEC = "ID:%Integer,City:%String,
               Name:%String,Age:%Integer,
               Stream:%String(EXTERNALSQLTYPE=""LONGVARCHAR"", MAXLEN = """")")
{
}
ClassMethod Q1Execute(
    ByRef qHandle As %Binary,
    idfrom As %Integer = 1,
    idto As %Integer = 0,
    maxtxt As %Integer = 25) As %Status
{
    Quit $$$OK
}
ClassMethod Q1Close(ByRef qHandle As %Binary) As %Status [ PlaceAfter = Q1Execute ]
{
    Quit $$$OK
}
ClassMethod Q1Fetch(
    ByRef qHandle As %Binary,
    ByRef Row As %List,
    ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = Q1Execute ]
{
    Quit $$$OK
}

we still need to add CONTAINID=1 and   [ SqlName = Q1, SqlProc ]

  • Query Q1() is the descriptor used by the interface support
  • qHandle is the common data structure for all 3 methods
    • whatever you pass along to the next row for processing needs to be stored there.
    • e.g. first ID, last ID, actual ID, ...... 
    • In past this was typically a subscripted variable or some oref
    • As a personal experiment, I tried a JSON object and it worked fine
  • The query takes 3 simple input parameters:
    • idfrom = first ID to show
    • idto = last ID to show - missing shos al higher IDs available
    • maxtxt = maximum text from the beginning of the stream. Default = 25
  •  
    ClassMethod Q1Execute(
        ByRef qHandle As %Binary,
        idfrom As %Integer = 1,
        idto As %Integer = 0,
        maxtxt As %Integer = 25) As %Status
    {
      set qHandle={}
      set qHandle.id=0
      set qHandle.idfrom=idfrom
      set qHandle.idto=idto
      set qHandle.obj=0
      set qHandle.stream=0
      set qHandle.maxtxt=maxtxt
      Quit $$$OK
    }
  • Q1Fetch is the biggest working bloc
    • I used object access in this example to keep it more readable
    • Accessing the Globals for Date and Stream directly was remarkably faster but really hard to read and to follow. 
    • The more important point is that you can do whatever you like, not just collect or select data.
    • Many management routines use it to display Processes, actual Users,  ... whatever can be presented as a table. 
    • The point is to compose the $LB() for Row and return it and once you are done,
    • Set AtEnd=1, and the query terminates.
    • In this example, the major challenge is to skip nonexisting objects and to skip no existing streams
    • to avoid empty result lines.
  • /// that's where the music plays
    /// called for evey row delivered
    ClassMethod Q1Fetch(
    	ByRef qHandle As %Binary,
    	ByRef Row As %List,
    	ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = Q1Execute ]
    {
      /// first access
      if qHandle.id<qHandle.idfrom set qHandle.id=qHandle.idfrom
      ///
    nextrec
      if qHandle.idto,qHandle.idto<qHandle.id set AtEnd=1
      if qHandle.id>^rcc.TUD set AtEnd=1
      if AtEnd quit $$$OK
      if 'qHandle.obj {
        set obj=##class(rcc.TU).%OpenId(qHandle.id)
          ,qHandle.obj=obj
          ,qHandle.stream=0
      } 
      if 'obj set qHandle.id=qHandle.id+1 goto nextrec
      if 'qHandle.stream set qHandle.stream=qHandle.obj.Stream
      set text=qHandle.stream.Read(qHandle.maxtxt)
      set Row=$lb(qHandle.id,qHandle.obj.City,qHandle.obj.Name,qHandle.obj.Age,text)
    /// row completed
      set qHandle.id=qHandle.id+1
      set qHandle.stream=0
      set qHandle.obj=0
      Quit $$$OK
    }
  • and here is a short test
    [SQL]USER>>call rcc.Q1(4,7)
    8.      call rcc.Q1(4,7)
    
    Dumping result #1
    ID      City    Name    Age     Stream
    4       Newton  Evans   61
    5       Hialeah Zemaiti 47      Resellers of premise-base
    6       Elmhurs Jenkins 29      Enabling individuals and
    7       Islip   Drabek  61      Building shareholder valu
     
    4 Rows(s) Affected
    statement prepare time(s)/globals/lines/disk: 0.0003s/11/685/0ms
              execute time(s)/globals/lines/disk: 0.0010s/18/2156/0ms
                              cached query class: %sqlcq.USER.cls59
    ---------------------------------------------------------------------------

Getting just the beginning of our stream is not always sufficient.

Follow me on to the next chapter for the extension of this example that
will show and control more result lines.

Just a reminder:
All test data are generated using the System method %Populate
So your output will be different. I suggest you run our tests also with other parameters
than the shown examples to get the full power of this tool.

The full code example is again available on GitHub

 

The full code example is again available on GitHub  

The Video is available now. See section Custom Class Query 

For immediate access, you can use Demo Server WebTerminal  
and the related system Management Portal on Demo Server SMP

I hope you liked it so far and I can count on your votes.

1 Comment
Discussion (1)1
Log in or sign up to continue
Article
· Mar 2, 2023 5m read

Tutorial - Working with %Query #1

    

The title of the contest subject is not quite precise but addresses the %Library.classes involved.
What is meant is officially named Class Query and is dating back to the early days of Caché.
CLASS is used because it is embedded in a COS class.
Though there is a precise description in the official documentation it remains rather abstract.
My tutorial should guide you step-by-step through a simple example in COS to make it tangible to you

  • All code examples are available on GitHub
  • All examples can be exercised from Terminal, Console, or WebTerminal.
  • I use a personal command ZZQ homed in %ZLANGC00.INT for test and demo which runs the SQL Shell  >>>  "DO $system.SQL.Shell()"
  • SQL Shell runs interactive and allows simple debugging of your code
  • Finally, I use Studio as it includes a very comfortable wizard for ClassQueries

Intro

For any demo or tutorial, some test data are required.
My simple table/class design just has 4 columns:

  • Id
  • City
  • Name
  • Age
  • Stream

While the first 4 are rather normal data types easily mapped to SQL
Stream provides the challenge to use a Class Query for display.
The idea in background: If this is a patient record then some big
stream documents might be directly attached to it.
The class is defined as [Final] to keep the Global more readable.

The demo content is generated using %Populate Utility.
Therefore your data content will look different than here
Except for the Stream that is not serviced by %Populate.
Here I generate some text that is randomly split into sections
using || (double pipe) as a segment separator.  
(! it is not a tutoriral on %Populate Utility!)

In order to simulate  missing content I removed
the stream for ID=4 and also the whole ID=3 .

It's simply
USER>Do ##class(rcc.TU).Populate(8)
USER>kill ^rcc.TUD(3)                   ;; make a gap
USER>set $LI(^rcc.TUD(4),4)=""  ;; no stream

So this is the demo Table / Class

USER>zwrite ^rcc.TUD
^rcc.TUD=8
^rcc.TUD(1)=$lb("Bensonhurst","Kovalev",16,"1")
^rcc.TUD(2)=$lb("Queensbury","Yeats",15,"2")
^rcc.TUD(4)=$lb("Newton","Evans",61,"")
^rcc.TUD(5)=$lb("Hialeah","Zemaitis",47,"5")
^rcc.TUD(6)=$lb("Elmhurst","Jenkins",29,"6")
^rcc.TUD(7)=$lb("Islip","Drabek",61,"7")
^rcc.TUD(8)=$lb("Islip","Kovalev",88,"8")


The case %SQLquery

You create an empty class frame  (rcc.TU0) and let the wizard add a Query

And it guides you through all the required parameters:
We first create a Basic Class Query 

and add our input parameters  (I have just 1)

and

that's the result

That's not so impressive yet and you need to add some more parameters + your Query!
you may do it just by typing or using Studio's Inspector which knows all quotes and brackets

  •  CONTAINID defines the column of your ID in the resulting row
  • SqlProc indicates that your query may be used as Procedure  (which we will do)
  • SqlName = Q0  assigns a name within your class package (mostly simpler)
  • most important: your SQL statement applying your input parameters as host variables

so it looks like this:

WHAT IS THIS GOOD FOR ?

  • This is, of course, the most simple statement. Normally  you would use it for rather complex SQL statements that are frozen now and as SQLprocedure available anywhere internal or from some external ODBC or JDBC client 
  • all ODBC/JDBC protocol is precompiled for your query. 
  • It looks like embedded SQL but you are not caught in your classmethod.
  • internal it is available for %ResultSet, or  %SQL.Statement or $system.SQL.Shell()
  • either direct using CALL rcc.Q0(4)  or as sub-select  SELECT Id, name FROM rcc.Q0(99) where AGE > 21 

And it looks like this:

  • 
    USER>do $system.SQL.Shell()
    SQL Command Line Shell
    ----------------------------------------------------
    The command prefix is currently set to: <<nothing>>.
    Enter q to quit, ? for help.
    [SQL]USER>>call rcc.q0(4)
    3.      call rcc.q0(4)
    
    Dumping result #1
    ID      Age     City    Name    Stream
    1       16      Bensonhurst     Kovalev "1%Stream.GlobalCharacter
                                                                       ^rcc.TUS"
    2       15      Queensbury      Yeats   "2%Stream.GlobalCharacter
                                                                       ^rcc.TUS"
    4       61      Newton  Evans
    5       47      Hialeah Zemaitis        "5%Stream.GlobalCharacter
                                                       ^rcc.TUS"
    4 Rows(s) Affected
    statement prepare time(s)/globals/lines/disk: 0.0003s/11/583/0ms
              execute time(s)/globals/lines/disk: 0.0006s/4/1515/0ms
                              cached query class: %sqlcq.USER.cls77
    ---------------------------------------------------------------------------
  • [SQL]USER>>SELECT Id, age, name FROM rcc.Q0(99) where AGE > 21 
    6.      SELECT Id, age, name FROM rcc.Q0(99) where AGE > 21 
     
    ID      Age     Name
    4       61      Evans
    5       47      Zemaitis
    6       29      Jenkins
    7       61      Drabek
    8       88      Kovalev
     
    5 Rows(s) Affected
    statement prepare time(s)/globals/lines/disk: 0.0726s/45969/214801/0ms
              execute time(s)/globals/lines/disk: 0.0016s/123/2486/0ms
                              cached query class: %sqlcq.USER.cls81
    ---------------------------------------------------------------------------

It's immediately obvious to you that instead of the Stream content you get a mystic StreamReference

Follow me on to the next chapter of a custom code-based Query.

Just a reminder:
All test data are generated using the System method %Populate
So your output will be different. I suggest you run our tests also with other parameters
than the shown examples to get the full power of this tool.

The full code example is again available on GitHub  

The Video is available now.

For immediate access, you can use Demo Server WebTerminal  
and the related system Management Portal on Demo Server SMP

I hope you liked it so far and I can count on your votes.

4 Comments
Discussion (4)1
Log in or sign up to continue
Question
· Mar 2, 2023

How Does InterSystems FHIR profile validation works?

Hi folks!

Examining FHIR profile validation with InterSystems FHIR server. FHIR profiles is a very useful feature of FHIR standard that helps an organization or solution to establish constraints to a very disperse FHIR standards that are relevant to a particular business solution. Learn more on FHIR profiles.

I created a very simple FHIR profile with the following JSON:

 
Spoiler

As you can see in "differential" section it makes mandatory fields of id, name and gender.

I managed to successfully submit the profile via the POST request to:

localhost:52773/fhir/r4/StructureDefinition

Then I submitted the following test Patient profile, where I omitted the "id" field and included the FHIR profile link in the meta section to:

localhost:52773/fhir/r4/Patient

 

{

"resourceType": "Patient",

"meta": {

"profile": [

"http://example.org/fhir/StructureDefinition/TutorialPatient"
]

},

"text": {

"div": "‹div xmlns=\"http://ww.w3.org/1999/xhtml\"><h1>Elon Musk</hi>/div>",

"status": "generated"
},

"name": [

{

"use": "official",

"given": [

"Elon"
],

"family": "Ramesh"
}

],

"gender": "male",

"birthDate": "1997-09-08",

"telecom": [

{

"value": "9876543210",

"use": "mobile",

"system": "phone"
},

{

"system": "email",

"value": "elon.musk@gmai.com"
}

]

}

And instead of the expected error I'm getting the successfully created patient.

What am i doing wrong? How are the FHIR validation profiles supposed to be used in InterSystems FHIR server?

9 Comments
Discussion (9)2
Log in or sign up to continue
Question
· Feb 24, 2023

IHE Inbound CCD Result Categorisation Customisation

Hi,

I'm currently working on an IHE implementation, and we've hit some issues around categorising inbound CCD results (i.e. mapping lab and rad results to their relevant SDA types).

I know that this is handled by the \CSP\xslt\SDA3\CDA-Support-Files\Import\Section-Modules\DiagnosticResults.xsl out of the box which can be overridden to use different logic, but I was wondering whether there's a standard/best practice approach for handling different sets of logic depending on the supplier of the CCD? We're going to be onboarding multiple data sources, so we're likely going to end up with lists of thousands of procedure codes, each mapped to a different specialty (e.g. Rad, Cell Path, Micro etc.), so I just wanted to get some thoughts on whether there's a common approach to make this as reusable (and painless) as possible.

This may end up being more of an XSLT-related question as I assume it'll likely involve referencing an external list of values from within a particular stylesheet, or is there maybe a way to call out to some objectscript to use a LUT?

Any advice or ideas would be great.

Thanks!

2 Comments
Discussion (2)3
Log in or sign up to continue
Article
· Feb 22, 2023 4m read

Export to JSON - relationships and inheritance

Why I've decided to write this

Once again I had a challenge that costed me some time and a lot of testing to reach the best solution. And now that I've managed to solve it, I'd like to share a little bit of my knowledge.
 

What happened?

In a namespace there were a lot of similar classes, so to make them simpler there were a superclass with comon properties. Also, there are relationships between them. I had to export one of them to JSON, but I couldn't change the superclasses, or I would break down the flow of many other integrations.

What made it all difficult was the problem that my JSON couldn't have the properties of the superclass. Ouch! I could export and take them off one by one, but.. what if someone changes the superclass?

And even worse... what happens with the relationships? If we export a relationship, we export another whole object, with all of its properties, but I couldn't have them either in the JSON.

 

A light in the end of the tunnel

Luckily, there is always a light in the end of the tunnel, and my light is the XData.

The solution is very simple: lets call the class that I had to export ClassToExport, the class with the relationship RelatedClass and the superclass SuperClass.

We'll have:

Class project.SuperClass Extends %Persistent {
    Property CommonProperty As %String;
}
Class project.ClassToExport Extends project.SuperClass {
    Property PropertyToExport As %String;
    Relationship RelationshipToExport As project.RelatedClass [ Cardinality = many, Inverse = RelatedProperty ];
}
Class project.RelatedClass Extends project.SuperClass {
    Property DontExportThis As %String;
    Property ExportThis As %String;
    Relationship RelatedProperty As project.ClassToExport [ Cardinality = one, Inverse = RelationshipToExport ];
}

 

In ClassToExport, I write the XData: there must be a name and a tag <Mapping> with the tags <Property>. The tag <Mapping> carries the xml namespace, xmlns="http://intersystems.com/jsonmapping", and the tags <Property> carry the properties described in %JSON.MappingProperty¹ (of the official documentation).

 

The magic trick is that everything that is not specified in the mapping will be ignored. So, if we change ClassToExport to:

Class project.ClassToExport Extends project.SuperClass {
    Property PropertyToExport As %String;
    Relationship RelationshipToExport As project.RelatedClass [ Cardinality = many, Inverse = RelatedProperty ];
    XData MappingJSON {
        <Mapping xmlns = "http://intersystems.com/jsonmapping">
            <Property Name = "PropertyToExport" FieldName = "Property-JSON"/>
            <Property Name = "RelationshipToExport" FieldName = "RelatedClassJSON"/>
        </Mapping>
    }
}

we'll have in the JSON something like:

{
   "Property-JSON":"value",
   "RelatedClassJSON": [
      {"CommonProperty":"value", "DontExportThis":"value", "ExportThis":"value"},
      {"CommonProperty":"value", "DontExportThis":"value", "ExportThis":"value"}
   ]
}

So the names of the ClassToExport are ready, and only the properties we want are in the JSON, but the RelatedClass still has work to do.

 

Then, we change RelatedClass with an XData with the same name to arrange their properties:

Class project.RelatedClass Extends project.SuperClass {
    Property DontExportThis As %String;
    Property ExportThis As %String;
    Relationship RelatedProperty As project.ClassToExport [ Cardinality = one, Inverse = RelationshipToExport ];
    XData MappingJSON
    {
        <Mapping xmlns = "http://intersystems.com/jsonmapping">
            <Property Name = "DontExportThis" Include="None"/>
            <Property Name = "ExportThis" Include="INOUT"/>
            <Property Name = "CommonProperty" Include="INOUT"/>
        </Mapping>
    }
    
    
}

 

so we'll have in the JSON somehting like:

{
   "Property-JSON":"value",
   "RelatedClassJSON": [
      {"CommonProperty":"value", "ExportThis":"value"},
      {"CommonProperty":"value", "ExportThis":"value"}
   ]
}

which is what we want.

It is interesting to observe that for the property "DontExportThis", I specified a tag with INclude="None". This is the same of not putting any ta at all for that property.

 

¹ read also %JSON.PropertyParameters to understant what each property does.
 

Thank you for reading and I hope the article was useful!

Feel free to ask me for doubts or to get in touch if you think I can help in some specific case. I'll be happy to help!

Discussion (0)1
Log in or sign up to continue