Yes, that is true today but dispatching directly to labels in a class runtime is very fragile.
- Log in to post comments
Yes, that is true today but dispatching directly to labels in a class runtime is very fragile.
I really enjoy fluent interfaces made possible by using classes and objects.
DEV>write {}.%Set("a","1").%Set("b",2).%Set("c","thought it would be 3!").%ToJSON()
{"a":"1","b":2,"c":"thought it would be 3!"}Have you considered using $xecute?
Version:1.0 StartHTML:00000092 EndHTML:00001371 StartFragment:00000172 EndFragment:00001337 CachÙ Studio clip Class User.Exec [ Abstract ]{ClassMethod test(STATUS = 1) As %String{set greeting = $xecute("(STATUS) { return $S(STATUS:""HELLO"", 1:""GOODBYE"") }",STATUS)return greeting}}and here is this code in action:
PANTHER:XEP:DEV>w ##class(Exec).test() HELLO PANTHER:XEP:DEV>w ##class(Exec).test(0) GOODBYE
I should update the CSV and CSV2CLASS utility procedures to take advantage of table-valued functions. Then you could select JSON_ARRAYAGG() from CSVTVF(<arguments go here>). Of course you would have to supply the constructor for the objects, probably using JSON_OBJECT.
If the default array projection for PET.ImageStudy.Injection is used then there will be an SQL table projected from the array of objects. By default, the name of this table will be PET.ImageStudy_Injection. This projected table will define a column referencing the PET.ImageStudy table. I believe this column will be named ImageStudy. There will also be a column named Injection_RadioTracer in this table and that column will be a reference to the RadioTracer table.
If the array, Injection, in the PET.ImageStudy class is not stored using defaults then this becomes a more complex problem - still solvable but more difficult.
Let me know if array storage defaults are used and then I can help compose a query - or perhaps you can do it with the information I provided above. You can verify this information using the System Explorer->SQL to view the table definitions.
-Dan
If you have flexibility with the schema definition then there are certainly other models that would make a query such as this simpler.
With an array collection that is stored as a serialized list you will have to either write a procedure, define some computed properties, or you will have to use some of the InterSystems collection extensions that may not work very well with collections of objects.
Our projection of serial types (classes that extend %SerialObject) uses a "flattening model" and we don't use that for serial collections stored as "listnode" structures. We do use it in the specific case of an array of serial objects stored using a "subnode" structure - the default.
I know this all sounds a bit scary but it doesn't need to be. When you get back to this we can walk through the options.
Nicely done!
There is a more general way to describe this situation. First consider what is actually happening. When you open an Object (%Open/%OpenId) you are copying data from persistent storage into memory. Then some other actor is modifying the data in persistent storage so the in-memory data is no longer the same as the data in persistent storage. If you were to then copy the in-memory data back to persistent storage (%Save), assuming the same identifier is used, then the modifications from the other actor would be overwritten.
There are two ways that Objects/SQL Persistence will allow this to happen (I call the action performed by the other actor an underwrite). First is the case you present with your example (same process performs underwrite and overwrite). Second is when there are no or insufficient concurrency controls in place. The second case is easily resolved simply by employing proper concurrency control mechanisms to prevent under/over writes. The first case is more sinister because normal pessimistic concurrency mechanisms won't guard against this case. This case occurs when a process holds data in memory and then updates the data in persistent storage either using direct global set/kill or by using SQL. Optimistic concurrency controls will guard against only the SQL update case, direct global set/kill will go undetected unless those set/kill actions also update the version number of the data.
%Reload() was not intended to solve this problem. %Reload() is automatically invoked when the concurrency setting of an object in memory is updated from no-retained lock (value less than 3) to a retained lock (value 3 or 4). In this case, data is automatically sync'ed with data on disk to ensure that the now-lock protected data is the same in-memory and in persistent storage. However, %Reload() can be invoked directly to perform this same action should user code somehow detect it is necessary.
If the reason you want to %Reload() the data is because the persistent storage was modified by SQL then I have the same question as Fab asked above. Why would you use SQL to update the data? I can think of a few reasons why you might want to do that but there are other ways to accomplish the same task without having to worry about the state of data in memory vs. the state of data in persistent storage.
If you were to use optimistic concurrency controls (VERSIONPROPERTY) then %Save()/UPDATE will fail if the version numbers don't match and then the user code can go into a state where conflicts can be resolved.
There is an interesting dynamic SQL feature that will guard against the case where some objects may be in memory and the user code needs to SQL to update data. I am happy to provide an example of this technique if someone is interested.
-Dan
We should never report an error in this way. The Extent Manager can hold stale extent definitions that, over time, can become a bit unwieldy but <FUNCTION> errors are certainly never normal. All registered extents can be rebuilt from compiled class information. I agree with Kyle in that this should be brought to the attention of support so we can do a proper analysis of why this is happening.
As for the "unwieldy" comment, look into the /deleteextent qualifier. This can be optionally specified when deleting a class definition. It causes the Extent Manager information (the registered extent) to be deleted along with the class definition. It helps keep the Extent Manager a bit cleaner. It also causes the compiled class's extent (the data!!) to be deleted so be careful.
Sorry! I didn't mean "we should never" comment to refer to how you reported the problem!! Not at all!
I wrote the code that is reporting the <FUNCTION> error. What I meant was that my code is wrong, there is a bug in my code and this is a problem that I need to address! ( I think that I have already in a later version.)
I am sorry that I wasn't more clear about this. I do recommend that you open an issue through the WRC however. That way you can be updated as to the resolution of the issue and we can use it internally to track the resolution.
Be careful. %Extends returns 1 | 0 but it is checking the entire SUPER value of your class, not just the primary superclass. Take a simple class that extends both MyLibrary.ParentClass and %Library.Populate - %Extends will return 1 (true) for both MyLibrary.ParentClass and %Library.Populate.
There is another method - %IsA(superclass) that returns 1 (true) only if the superclass is the primary super class (forms a %IsA() relationship with your current class).
Amir,
If you want the results of your query to be usable by the outside world then you can easily do this by generating a custom query (subclass %SQL.CustomQuery), compile and execute it. I probably have some examples of doing this somewhere. %SQL.CustomQuery is a bit more convenient than defining a query in a class - it basically accomplishes the same thing. %SQL.CustomQuery will generate all of the relevant metadata from your property definitions and you do not have to be concerned with ROWSPEC, etc. It is fully compatible with SQL and can even be used as a TVF in other queries.
-Dan
System assigned id's are projected to SQL as an integer as previously mentioned. The name of the column projected to SQL from a system-assigned id is 'ID' by default but it isn't guaranteed to always be 'ID'.
As for controlling how system-assigned ID's are defined - both name and type - I suggest using IDENTITY. I think your code will be happier. You can even name it 'ID' if you wish.
Class User.Person Extends (%Persistent, %Populate)
{
Property ID As %BigInt [ Identity ];
Property Name As %String;
}
And a sample run:
USER>set ^User.PersonD = 5983658923640
USER>d ##class(Person).Populate(50)
USER>do $system.SQL.Execute("select id,name from person where id = 5983658923646").%Display()
ID Name
5983658923646 Young,Imelda B.
1 Rows(s) AffectedThe reason it works when you specify a different dialect is because the type mappings are different for different dialects.
Hi Keith,
%BuildIndices/%PurgeIndices are generated for any class that primarily extends %Library.Persistent. When such a class defines one or more indexes and that class uses SQL Storage then the class must also define an index map in the storage definition. Without that the index simply won't be maintained - it doesn't exist as far as SQL is concerned. Your display in the original posting is a view of the SQL table definition. The SQL table definition is a projection of the class definition to SQL.
Your first example shows an index that is defined as the IDKEY index - that is the index definition that corresponds to the SQL Master Map (data map). I'm not surprised that it doesn't show up in the indexes display of the SQL Manager.
If WRC isn't an option then a good place to start is to work through just one of your classes. If you can choose one simple example perhaps we can suggest corrections for you.
-Dan
Fair enough. Let's make this more fun! First of all, we need to understand the IDKEY index. InterSystems is somewhat unique in the market with this one. Most SQL vendors support indexes and most support "clustering" data with indexes to further improve the performance of certain queries. Think of our IDKEY index as the "Master Data Index" (sometimes referred to as the "Master Map" or "Data Map"). The "Master Data Index" (MDI) is simply the index that has every column "clustered" with it - all data values are present in the IDKEY index structure. Every table (persistent class) has an IDKEY index and every column (property in Object terms) is stored with it.
Any other index - not the MDI - is simply there to improve query performance. Each non-MDI index has an index key and perhaps some data clustered with it. InterSystems is not different from most other SQL implementations. You can create a new table and add indexes to it using standard DDL statements. We, like most vendors, have some non-standard syntax to support features unique to our product but it is mostly familiar syntax. You can also define a class directly using one of our IDE's or your favorite editor. And all of this will just work - no special effort required and your experience will be similar to that of using any other SQL.
Your example is different. We support low-level storage mapping to legacy data structures. There are two additional storage classes that support this, one of which is used by your example - %CacheSQLStorage. This storage class was introduced specifically to provide a bridge from legacy applications to Caché/InterSystems IRIS. Mapping legacy data structures can be a complicated task and it can be confusing. And this is where you are - you have an application that is using our support for bridging existing data structures to our DBMS.
And none of that helps you with your problem! So let's try this. Let's define your class without using the legacy data mapping. I'll choose some simply data types just to illustrate.
Class User.Patient Extends %Persistent
{
Index AcctIndex On accountNo;
Property accountNo As %String;
Property citySt As %String;
Property dob As %Date;
Property name As %String;
Property patientNo As %Integer;
Property rel2Guar As %String;
Property sex As %String;
Property ssn As %String;
Property street1 As %String;
Property street2 As %String;
Property telephone As %String;
Property zip As %String;
}
That's it - no special work involved, just a quick and easy class definition with an index on accountNo defined. This is what it looks like in the SQL Manager:

This is an example without the legacy global mapping. %BuildIndices is automatically generated and there is nothing else you would need to do to define, populate and maintain this index - other than to build it initially if it was added to a previously populated extent.
In my next comment (this one is getting long), I'll convert the default storage structure into a mapped storage structure using %CacheSQLStorage.
In reviewing the storage definition you present above, I realize that the list of properties has little to do with the SQL Map definitions it includes. So I will continue with my example, using the properties your example defines (they are present in the storage definition only for the purpose of computing statistics and other storage specific metadata).
Without altering the class definition, other than to add a couple of keywords to make life a bit simpler in the class header and the conversion of the storage to use SQL Mapped Storage (%CacheSQLStorage in your version), and adding %Populate - our test data generator - I came up with this example data storage:
USER>d ##class(Patient).Populate(10)
USER>zw ^User.PatientD
^User.PatientD=10
^User.PatientD(1)=":J5201:Z5211:58985:Isaacs,Michael A.:501759566:H2536:A8788:377-96-6394:J7857:G3137:R4692:42233"
^User.PatientD(2)=":S4498:Z4308:62077:Cooke,Mario C.:832248338:Z9867:V1891:859-50-1555:I9221:H3938:W7632:25909"
^User.PatientD(3)=":J8016:S3895:53889:Quilty,Nellie J.:150706592:J3845:B6691:998-18-8294:B482:D5191:R7961:59453"
^User.PatientD(4)=":H8837:T289:43380:Quince,Samantha U.:115923507:F6623:S6188:407-88-9788:R8672:Y1441:A9501:60822"
^User.PatientD(5)=":N1854:W4497:55465:North,Susan A.:784860058:Z8257:E2831:187-93-1523:T3167:U4316:A9955:34073"
^User.PatientD(6)=":A4324:Z7427:61318:Lopez,Will F.:133068033:K7869:R5254:302-73-2490:B2970:F1889:P8421:26050"
^User.PatientD(7)=":N4346:Y671:65137:Moon,Milhouse Z.:459189579:E6160:R3362:507-37-8752:L8401:R7909:F4245:60716"
^User.PatientD(8)=":N4328:V1682:47890:Zweifelhofer,Terry V.:360767849:B8856:E145:466-23-4632:K4269:X2839:S1937:49318"
^User.PatientD(9)=":M7370:B6086:49261:Rotterman,Lawrence S.:353537548:S6329:B9164:874-34-2035:D4140:U7504:N1456:66241"
^User.PatientD(10)=":W995:B5004:50613:Ironhorse,Barb I.:809117324:S6518:V1966:873-92-8543:Z9470:H6976:G2259:40210"
USER>zw ^User.PatientI
^User.PatientI("A"," A4324",6)=""
^User.PatientI("A"," H8837",4)=""
^User.PatientI("A"," J5201",1)=""
^User.PatientI("A"," J8016",3)=""
^User.PatientI("A"," M7370",9)=""
^User.PatientI("A"," N1854",5)=""
^User.PatientI("A"," N4328",8)=""
^User.PatientI("A"," N4346",7)=""
^User.PatientI("A"," S4498",2)=""
^User.PatientI("A"," W995",10)=""
And this is the full definition of the class - with the index map defined as a new SQL Map Definition in the storage. I highlighted a few things to show the connections between the logical class definition and the corresponding items in the storage definition.
Class User.Patient Extends (%Persistent, %Populate) [ SqlRowIdName = Patient, StorageStrategy = SQLStorage ] { Index AcctIndex On accountNo; Property accountNo As %String; Property citySt As %String; Property dob As %Date; Property name As %String; Property patientNo As %Integer; Property rel2Guar As %String; Property sex As %String; Property ssn As %String; Property street1 As %String; Property street2 As %String; Property telephone As %String; Property zip As %String; Storage SQLStorage { <SqlIdExpression>$i(^User.PatientD)</SqlIdExpression> <SQLMap name="DataMasterMap"> <Data name="accountNo"> <Delimiter>":"</Delimiter> <Piece>2</Piece> </Data> <Data name="citySt"> <Delimiter>":"</Delimiter> <Piece>3</Piece> </Data> <Data name="dob"> <Delimiter>":"</Delimiter> <Piece>4</Piece> </Data> <Data name="name"> <Delimiter>":"</Delimiter> <Piece>5</Piece> </Data> <Data name="patientNo"> <Delimiter>":"</Delimiter> <Piece>6</Piece> </Data> <Data name="rel2Guar"> <Delimiter>":"</Delimiter> <Piece>7</Piece> </Data> <Data name="sex"> <Delimiter>":"</Delimiter> <Piece>8</Piece> </Data> <Data name="ssn"> <Delimiter>":"</Delimiter> <Piece>9</Piece> </Data> <Data name="street1"> <Delimiter>":"</Delimiter> <Piece>10</Piece> </Data> <Data name="street2"> <Delimiter>":"</Delimiter> <Piece>11</Piece> </Data> <Data name="telephone"> <Delimiter>":"</Delimiter> <Piece>12</Piece> </Data> <Data name="zip"> <Delimiter>":"</Delimiter> <Piece>13</Piece> </Data> <Global>^User.PatientD</Global> <Structure>list</Structure> <Subscript name="1"> <Expression>{Patient}</Expression> </Subscript> <Type>data</Type> </SQLMap> <SQLMap name="IndexNName"> <BlockCount>-4</BlockCount> <Global>^User.PatientI</Global> <Structure>list</Structure> <Subscript name="1"> <Expression>"A"</Expression> </Subscript> <Subscript name="2"> <Expression>$$SQLUPPER({accountNo})</Expression> </Subscript> <Subscript name="3"> <Expression>{Patient}</Expression> </Subscript> <Type>index</Type> </SQLMap> <StreamLocation>^User.PatientS</StreamLocation> <Type>%Storage.SQL</Type> } }
and this is how it appears in the SQL Manager display of the indexes:

.
Hi,
Yes, SIR :) SQL Invokable routine. A procedure is invoked by CALL and a function is invoked using the function syntax. The primary difference between the two is in which statements you use to invoke the SIR. CALL invokes procedures, other DML statements can be used to invoke functions. The standard states that for every SQL Function there is an implicit procedure. That means that every SQL Function can also be invoked by CALL. The reverse is not necessarily true. IRIS SQL (and Caché SQL) does not enforce that restriction as even a procedure can be invoked using function syntax, as long as it returns a value. The issue with us doing that is that we do not restrict output-directed arguments and we do not restrict a Result Set Sequence (RSS) - strict SQL does not allow either in an SQL Function. Just be careful - invoking such a procedure (with output directed parameters and/or returnresultsets) as a function will ignore all but the return value.
That said, where the SQL Function is located in a DML statement matters. In order for an SQL Function to be referenced in the FROM clause of a SELECT statement it must implement the Table-Valued Function (TVF) interface. Queries do that (I don't recall in which version we added that capability) and %SQL.CustomQuery does. Refer to the class documentation for %SQL.CustomQuery for more information. If you still have questions then let me know!
Defining a class method as a stored procedure will not work as it does not implement the TVF interface.
-Dan
Perhaps this link will help - https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_from.
Look specifically for the paragraph on table-valued functions.
What isn't especially clear in this document is how to implement a custom query. The simplest and most portable way to implement a custom query is to extend %SQL.CustomQuery. Another way is to define a class query whose type is %Query, define a ROWSPEC, and implement the various functions. It is much simpler to extend %SQL.CustomQuery. There should be good documentation in that class.
There was a bug in 2017.1 that caused some problems with the seamless projection of a %SQL.CustomQuery subclass as an SQL function. That bug has since been fixed but I do not know if it is fixed in 2017.2.2.
If you need more information, please let me know.
-Dan
I believe the correct way to report an error from an SQL-Invokable Routine (procedure or function) is to throw an exception as Eduard demonstrated. Attempting to set local variables or retrieve them using an SQL function/procedure might produce invalid results.
As for reporting a nested error - I don't know.
A side note - it is not necessary to extend %RegisteredObject when a class contains only class methods. Not extending %RegisteredObject will result in smaller runtime footprints.
Hi John,
Great question! There is a lot of misinformation floating around about the general topic of CALCULATED, TRANSIENT, SQLCOMPUTED, etc. I'll try to clear the air and lay a clean foundation. First, let's discuss CALCULATED and TRANSIENT.
Each property defined in a class is accessed by a "getter" and a "setter", implemented as <property>Get and <property>Set methods respectively. In most common cases these accessor methods are trivial and are implemented at a very low level in Caché simply retrieve or store a value in the instance memory allocated to the property. In fact, the <property>Get and <property>Set method generators depend on instance memory. If no instance memory is allocated then the method generators have no clue where to retrieve the property's value from or where to set (store) the property's value. The allocation of instance memory is controlled by the CALCULATED keyword. If CALCULATED is true then no instance memory is allocated and the only way to access the property value is to override the accessor methods. Commonly, only the <property>Get method is overridden. We refer to such a property (CALCULATED with overridden Get method) as a "derived property". A CALCULATED property is not implicitly settable but, in theory, such a property can be set if the <property>Set method is implemented. I don't know offhand if the theory works in practice.
Now on to TRANSIENT. TRANSIENT simply means that the property is not stored when the containing object is saved. For a subclass of %SerialObject it means the property is not part of the serialized state produced by %GetSerial. There is instance memory allocated for a TRANSIENT property, assuming the user hasn't also defined it as CALCULATED. BTW, CALCULATED implies TRANSIENT but the reverse is not true. Since instance memory is allocated to a TRANSIENT, NOT CALCULATED property, the system does generate accessors (Get and Set) and such a property is implicitly settable.
Users can override the Get/Set accessors to inject custom behaviors for any property, CALCULATED or not, but doing so directly causes problems for SQL. (Property accessor methods run in an instance context and SQL does not.) Caché provides a mechanism for deriving a property value that can be used outside of an instance context. That mechanism is triggered by SQLCOMPUTED and SQLCOMPUTECODE. I won't go into details of how to define SQLCOMPUTECODE here.
If a property is defined as CALCULATED and SQLCOMPUTED then we refer to that property as "always computed", meaning that every <property>Get reference will cause the SQLCOMPUTECODE to be executed (more on that in a bit) to derive the current value of the property.
If the property is defined as NOT CALCULATED and SQLCOMPUTED then we refer to that property as "triggered computed" and its value will only be derived when certain conditions are true. First, if the current value of the property is NULL then the SQLCOMPUTECODE is called to derive the current value. Second, if the value of any property in the SQLCOMPUTEONCHANGE list of properties is changed then the SQLCOMPUTECODE is called to derive an updated value. Finally, there are two special tokens that can be specified in SQLCOMPUTEONCHANGE - %%INSERT and %%UPDATE. These tokens will trigger the SQLCOMPUTECODE to derive an updated value for the property when inserting (%%INSERT) a new instance into the extent of the class (or a new row in the SQL table) or updating (%%UPDATE) and existing instance or row.
That's the foundation.
Determinism. The function used to derive a value is only deterministic if that function returns the same value each time it is invoked with the same inputs. (if a = b then f(a) = f(b)). A property that is NOT CALCULATED + TRANSIENT is implicitly non-deterministic in an instance context. A CALCULATED property can be deterministic but it depends on the implementation of the function to derive the value. A property whose value is derived by overriding Get, Set or both is implicitly non-deterministic in an SQL context. (this concept is similar to third and fourth normal forms in relational systems). A simple non-deterministic function is now() implemented in Caché as "return $h". A more complex example would be a function that returns a value retrieved from another table. That function is non-deterministic because the value retrieved can be updated at any time, that is, passing the same input argument to the function multiple times can produce different results.
And the short answer to your question - if you define derived properties in your class for the purpose of simplifying SQL queries then make certain you understand the implications of non-deterministic derivation. Not all non-deterministic derived values are bad. Take Age() as an example. If Age is computed based on current time then it is non-deterministic. That makes it a bad candidate for a NOT CALCULATED property but showing Age in a query is a good thing.
HTH,
Dan
Very interesting question. The callbacks we provide as part of Caché Objects are limited to Objects and are often quite useful. However, Caché Persistent classes also project to SQL as tables. Users can perform CRUD operations using either Objects or SQL. Callbacks are not processed by SQL so any work you do in a callback must either be reproduced in some form in SQL or the user must recognize that accessing persistent data through SQL might produce different results from performing an equivalent access using Caché Objects.
Rubens Silva's answer is accurate - %OnAddToSaveSet provides more flexibility when you need to modify the object or the graph of objects to be serialized.
But we have better choices these days. Caché Objects provides two mechanism for triggering behavior. SQL recognizes both of these mechanisms. The first is a compute trigger. You can define a property as SQLCOMPUTED. The value of the property can be defined to be computed whenever the value of a property defined in the SQLCOMPUTEONCHANGE list of properties is changed. We support two meta-properties to trigger a compute on insert and update - %%INSERT and %%UPDATE.
The second mechanism is a TRIGGER. TRIGGERs were one the exclusive domain of SQL but we since added a new FOREACH keyword value of ROW/OBJECT. When FOREACH = ROW/OBJECT the trigger will be pulled at the defined time for each filing event using SQL or Objects. FOREACH = ROW indicates that the trigger is invoked only when SQL is used. There is no FOREACH value for invoking a trigger only when Objects is used.
We refer to this feature as "unified triggers". This feature first appears in Caché 2014.1.
-Dan
Very interesting question. Rubens Silva's answer is accurate - %OnAddToSaveSet provides more flexibility when you need to modify the object or the graph of objects to be serialized.
The callbacks we provide as part of Caché Objects are limited to Objects and are often quite useful. However, Caché Persistent classes also project to SQL as tables. Users can perform CRUD operations using either Objects or SQL. Callbacks are not processed by SQL so any work you do in a callback must either be reproduced in some form in SQL or the user must recognize that accessing persistent data through SQL might produce different results from performing an equivalent access using Caché Objects.
But we have better choices these days. Caché Objects provides two mechanisms for triggering behavior. SQL recognizes both of these mechanisms. The first is a compute trigger. You can define a property as SQLCOMPUTED. The value of the property can be defined to be computed whenever the value of a property defined in the SQLCOMPUTEONCHANGE list of properties is changed. We support two meta-properties to trigger a compute on insert and update - %%INSERT and %%UPDATE. If SQLCOMPUTEONCHANGE includes %%INSERT then the value of the property is computed when the object is about to be inserted into the database. This computation will override the current value of the property. Similarly, if %%UPDATE is specified in SQLCOMPUTEONCHANGE then the property's value is computed just prior to saving an updated version of the object.
Compute triggers have been present in Caché for a very long time and %%INSERT/%%UPDATE were available in 2008.1.
The second mechanism is a TRIGGER. TRIGGERs were one the exclusive domain of SQL but we since added a new FOREACH keyword value of ROW/OBJECT. When FOREACH = ROW/OBJECT the trigger will be pulled at the defined time for each filing event using SQL or Objects. FOREACH = ROW indicates that the trigger is invoked only when SQL is used. There is no FOREACH value for invoking a trigger only when Objects is used.
We refer to this feature as "unified triggers". This feature first appears in Caché 2014.1.
-Dan
As a follow-up, I should have provided an example!
Class User.TriggerCompute Extends (%Persistent, %Populate)
{
Property foo As %String;
Property CreationDate As %TimeStamp [ SqlComputeCode = { set {*}=$zdt($h,3) }, SqlComputed, SqlComputeOnChange = %%INSERT ];
}
and I populated this class at random intervals. Here are five sample rows:
select top 5 * from triggercompute order by creationdate desc
| ID | CreationDate | foo |
|---|---|---|
| 11 | 2017-05-26 08:40:15 | Y1407 |
| 10 | 2017-05-26 08:40:10 | J9793 |
| 9 | 2017-05-26 08:40:03 | Y5011 |
| 8 | 2017-05-26 08:39:57 | E51 |
| 7 | 2017-05-26 08:39:55 | L3468 |
5 row(s) affected
Caché Objects persistent classes project to SQL as tables. Caché Objects relationships are binary - a relationship must have a defined inverse relationship in the related class. We only support relationship/inverse relationship cardinality pairs of one:many and parent:children (a special case of one:many where the parent is implicitly required and is also implicitly part of the IDKEY). A Caché Objects n-cardinality relationship (children, many) is transient in the container and there is no projection to SQL. A 1-cardinality relationship is stored and projects to SQL as a column that is constrained by a foreign key constraint. A simple query can be used to retrieve the contents of the unprojected n-cardinality relationship. Consider two tables, A and B, related by properties b and a. A.b is of type B and specifies cardinality = many. B.a is of type A and specifies cardinality = one.
The value of A.b can be determined by this query:
select %ID from B where B.a = ?
and specify '?' as the A.%ID value. In fact, the internal relationship implementation uses a very similar query to populate A.b on %Open.
Hi Chris,
Good question! This is a poorly understood area and there is much to say about it. I'll try to be brief. Feel free to follow up.
Your new property falls into the category of properties commonly referred to as 'derived columns' in other products. We support two different flavors - 'always computed' and 'triggered computed'. An 'always computed' property is indicated by the CALCULATED keyword. A 'triggered computed' property is 'NOT CALCULATED' although this is the default if you don't specify it. A triggered computed property can be TRANSIENT or stored (not transient - again, the default) and its value is triggered to recompute if the value of any property specified in the SQLCOMPUTEONCHANGE list of properties changes. In an object context, you need to be a aware that the value of a TRANSIENT property is directly settable unless you take precautions to prevent that.
The advantage to using SQLCOMPUTED/SQLCOMPUTECODE to derive a property value is that SQL and Objects seamlessly (almost) recognize the derived value and invoke the computation at the proper time. Overriding Get/Set methods can be used to derive property values but SQL will not recognize this as you have already discovered.
SQL does not operate in an instance context. In other words, there is no oref available to SQL so SQLCOMPUTECODE must run in a class context and not in an instance (object) context. No oref. The way you have implemented your code is correct and, sorry to say, there isn't a cleaner way that I know about.
There is one other thing you should know. When you open the object you may not actually get a new oref if that object was previously open in your process. That goes for any call to %Open([d] - we cache the OID's with their active orefs and always return the existing oref from %Open if one is present in process memory. What does that mean? It means that you may see data in memory that has not committed to storage - Objects sees the data because of the in-memory context but SQL won't because it runs in an on-disk (storage) context. By opening objects you may produce a computed value that is different from the same data that is stored. There are ways you can avoid this problem, one being to store the derived value and trigger it to recompute on INSERT and UPDATE. This is easily defined by adding %%INSERT and %%UPDATE to the SQLCOMPUTEONCHANGE list of trigger properties.
Not so brief.
-Dan
Errors can be encountered during the normal execution of any code and the cause of those errors is not always known or predictable. Caché has three primary error reporting mechanisms two are passive, meaning the user code is responsible for checking some error indicator and one is active, meaning the error triggers a change in the code path. The passive error reporting, %Status and SQLCODE, both require the user code to check for error conditions after executing some code. The code author must know before hand that these error indicators can be changed by the code that is being executed.
The third mechanism is exceptions. ($ZE with ZTRAP, etc. exist of course). With exceptions, the user code only needs to know that an exception could be thrown and to properly structure the code to deal with that possibility. If Caché user code is not already properly structured then $ZTRAP errors can cause problems so structuring code to deal with exceptions should not be considered a hardship.
The advantage of exceptions over the two passive types is that all three types of errors - %Status, SQLCODE, and exception - can be integrated into a single error handler. The passive checks can be used to construct exceptions using exception class constructors - CreateFromStatus and CreateFromSQLCODE - which can then be thrown. The error handling code lives in the CATCH block and can use the exception instance without further need to parser of otherwise process the exception in order to identify the type of exception, the exact error code and name, error context values, and so on. There is no need to litter your code with a lot of error handling code, detecting different types of errors, formatting, parsing, and so on. Just do it all in one place.
There are also exception methods to cast an exception as %Status or SQLCODE error values should there be a contractual obligation to report errors using one of the passive methods.
Another advantage to using exceptions is that there is virtually no runtime overhead. Also, unless you must return a %Status value (it happens), your code is free to return a useful value from a function, improving code readability and efficiency.
Yes, this should work but you are missing a step - you must execute the statement. Dynamic SQL allows for a statement to be prepared once and executed many times. A prepared statement is executed, returning a statement result. You can provide different parameter values for each execution.
set result = tStatement.%Execute()
Alternatively, you can do this in a single comment -
set result = $system.SQL.Execute("UPDATE table Set Status = 'Completed' WHERE ID in (1,2,3,4)")
-DanThe ID is assigned by %SaveData() and %OnBeforeSave is invoked before %SaveData is called. For a new object the ID value will not be reliable until after %SaveData has returned. I do not know whether or not this is documented explicitly.
I defined this simple class:
Class Community.IdBeforeSave Extends (%Persistent, %Populate){Parameter USEEXTENTSET = 1;Parameter DEFAULTGLOBAL = "^C.I";Property Name As %String;Property DOB As %Date;Property SSN As %String;/// This callback method is invoked by the <METHOD>%Save</METHOD> method to /// provide notification that the object is being saved. It is called after /// the object's data has been successfully written to disk./// /// <P><VAR>insert</VAR> will be set to 1 if this object is being saved for the first time./// /// <P>If this method returns an error then the call to <METHOD>%Save</METHOD> will fail.Method %OnAfterSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ]{set id = ..%Id()set ^C.S($increment(^C.S)) = "OnAfterSave, insert = "_$select(insert:"'true'",1:"'false'") _ ", ID = " _ $select(id'="":id,1:"<null>")Quit $$$OK}/// This callback method is invoked by the <METHOD>%Save</METHOD> method to /// provide notification that the object is being saved. It is called before /// any data is written to disk./// /// <P><VAR>insert</VAR> will be set to 1 if this object is being saved for the first time./// /// <P>If this method returns an error then the call to <METHOD>%Save</METHOD> will fail.Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ]{set id = ..%Id()set ^C.S($increment(^C.S)) = "OnBeforeSave, insert = "_$select(insert:"'true'",1:"'false'") _ ", ID = " _ $select(id'="":id,1:"<null>")Quit $$$OK}and then populated it with 5 objects. This is the result:
LATEST:USER>zw ^C.S ^C.S=10 ^C.S(1)="OnBeforeSave, insert = 'true', ID = <null>" ^C.S(2)="OnAfterSave, insert = 'true', ID = 1" ^C.S(3)="OnBeforeSave, insert = 'true', ID = <null>" ^C.S(4)="OnAfterSave, insert = 'true', ID = 2" ^C.S(5)="OnBeforeSave, insert = 'true', ID = <null>" ^C.S(6)="OnAfterSave, insert = 'true', ID = 3" ^C.S(7)="OnBeforeSave, insert = 'true', ID = <null>" ^C.S(8)="OnAfterSave, insert = 'true', ID = 4" ^C.S(9)="OnBeforeSave, insert = 'true', ID = <null>" ^C.S(10)="OnAfterSave, insert = 'true', ID = 5"
HTH,
Dan
To set a property whose type class is a serializable class (extends %Library.SwizzleObject - streams, serial classes, persistent classes) you need to call the property's SetObject or SetObjectId method if you wish to set the value to a serialized value (id value often). I think that in your example, tvar1 is holds an ID value of the referenced ZenCrm.Relationtypes class? If so, then this should work:
do RelationMatrix.RelationAIDSetObjectId(tvar)