Dan Pasco · Jan 20, 2016 go to post

Caché supports binary relationships with cardinality 1:N (one to many, many to one).  A relationship presents two behaviors. One is to logically link the two objects togther, allowing one to be accessed from the other. When persisted, the relationship is only valid for as long as both objects exist in their respective extents. In this, a relationship is very much like a foreign key constraint in SQL. The other behavior is to maintain the relationship in memory when both of the related objects are present. Think of this as in-memory referential integrity.

The concept of an "ordered relationship" only is relevant when you consider that one object can be related to many others. From the perspective of the one object, the many objects could be ordered. But what defines that order? Is it the order in which the relationship with each of the many side objects was formed? Is it some implied order based on the state of the many side object? A key of some sort? What happens if the key is not defined in all of the many side objects? What if the ordered key value is changed while the object is in memory?

There are many questions that come up in the conversation about "ordered relationship". Often, the user assumes that the order in which many side objects were related to the one side object will define an order. That is not true nor can it ever be true. There is no defined order and any order implied cannot be assumed to hold over time.

The best approach is to not assume any order. If you require the related many side objects to be presented in a particular order then use a simple SQL select statement to retrieve those related objects and use an ORDER BY clause to explciitly define the order. This recommendation comes with the caveat that all of the related objects must have been persisted before executing the SQL statement. It also presumes that the many side of the relationship is a persistent class.

It is possible to prepare and execute the order relationship SQL statement in a manner that will honor the in-memory state of the related objects. The uncommitted state of the relationship will not be honored. Only relationships already formed and persisted are visible to SQL.

The SAMPLES database includes an example of a 1:N relationship - Sample.Company and Sample.Employee.

In the following example, I use dynamic SQL with %ObjectSelectMode = 1 (true). I open an instance of Sample.Employee and change its name to "State,Altered". Before I run the statement, I write the value of the Name property before changing it. Notice the output - it contains the altered name because of the basic rule of swizzling. If I were to also select the Name column in the SELECT statement, it would be the value as it current resides on disk, not as it resides in memory. I will leave that as an exercise for the reader.

LATEST:SAMPLES>kill

LATEST:SAMPLES>set employeeInMemory = ##class(Sample.Employee).%OpenId(193)

LATEST:SAMPLES>write employeeInMemory.Name

Gibbs,Greta A.

LATEST:SAMPLES>set employeeInMemory.Name = "State,Altered"

LATEST:SAMPLES>set sql = "select %ID As Employee from Sample.Employee where Company = ? order by SSN"                                     

LATEST:SAMPLES>set statement = ##class(%SQL.Statement).%New()

LATEST:SAMPLES>set statement.%ObjectSelectMode = 1

LATEST:SAMPLES>do statement.prepare(.sql)

LATEST:SAMPLES>set employees = statement.execute(10)

LATEST:SAMPLES>while employees.%Next() { set employee = employees.Employee write !,employee.%Id(),$c(9),employee.Name,?30,employee.SSN,! }

195 Beatty,Stuart T.          123-22-3131

200 Vanzetti,Kevin Q.         186-73-9108

170 Eagleman,Bart C.          247-83-1354

191 LaRocca,Tara W.           338-71-2352

164 Clay,Kristen S.           476-52-4746

193 State,Altered             523-94-2490

165 Campos,Edgar D.           558-21-6591

115 Davis,Angela G.           767-95-5164

155 Sato,Alfred L.            845-25-6807

Dan Pasco · Jan 21, 2016 go to post

Hi,

There is no capability to order the objects returned by the various iterator methods implemented by %Library.RelationshipObject. Your best alternative is to use SQL to select the related objects using a restriction similar to "WHERE Company = :companyID" and then to include explicit ORDER BY to define the desired ordering. Of course, this comes with some requirements - the set of related objects, Employee instance in this example, has to be saved to the Employee extent before you can use SQL to query them and also any changes made to those instances in memory will not be reflected by the query results. That is why I used the technique of querying using dynamic SQL and %ObjectSelectMode as it reduces the impact of in-memory changes.

It doesn't completely eliminate the "persistence" requirement for querying as  any newly related objects have to be saved before they can be queried. 

The only other possiblity is for you to iterate over the set of related objects and build your own sorted array.

Hope this helps!

Dan

Dan Pasco · Mar 14, 2016 go to post

A couple of small points. %SQL.Statement is the newest and the recommended approach. As was stated previously, in many cases it will perform better. From a pure SQL perspective, a class query is defined as an SQL-Invokable-Routine (SIR). A SIR can be either a procedure or a function. The SQL standard further states that there is an implicit procedure for every function. Functions can be invoked by other SQL statements - SELECT for example, while procedures can be invoked by a CALL statement. The %SQL.Statement %PrepareClassQuery call will prepare a CALL statement for a class query. Of course, the class query must be projected as a SQLPROC (an unfortunate choice of keyword name). 

But, a class query is a function.

There is a new feature (as of 2015.1) called "table-valued function" that allows a class query projected as a SQLPROC to be invoked directly from a SELECT statement in the FROM clause (you can try this in SMP/Explorer/SQL/SAMPLES):

select * from sample.sp_sample_by_name('N')

This option not only allows you to fully encapsulate any hidden (and perhaps unsupported) direct method calls but also allows the statement author to select a subset of available columns, order the results or further restrict the results:

select name,dob from sample.sp_sample_by_name('N')  where DOB > '01/01/1950' order by DOB desc
 

This feature is not restricted to %SQLQuery class queries, %Query queries are also supported.

As a side note, the hidden and unsupported direct class query method, Func, is what we generate to support the SIR-Function interface.

Dan Pasco · May 26, 2017 go to post

I don't understand why it is not recommended. Can you elaborate?

Dan Pasco · May 26, 2017 go to post

Triggers and computed properties (compute triggers) are certainly better than callbacks. Now that we have unified triggers callbacks should be avoided - IMO - in favor of using triggers. However, initial expression when there is some outside influence such as a clock or other value source involved, when the property is not read-only protected, etc. makes using initialexpression a bad idea in some cases. In cases where there is no opportunity for the user to interact with a new instance prior to it being saved then initialexpression is acceptable. If there is opportunity for interaction between instantiation and save (serialization) then initialexpression can be unreliable. The %%INSERT/%%UPDATE compute triggers avoid that. 

Dan Pasco · May 26, 2017 go to post

Yes, of course. The SQLCOMPUTECODE can invoke a classmethod but not an instance method unless you provide the instance. If you wish to pass arguments that correspond to other properties then just use curly-brace syntax to refer to them:

sqlcomputecode = { set {*} = ..MyComputeMethod({property1},{property2}) }

and the compiler will work out the details.

Also, we have proposed syntactical changes that would make this much nicer. The idea is that a class definition is composed of some set of members, members being things like properties, methods, indices, and so on. Each member type, excepting methods, can also have methods. The idea for the syntax is to embed the method definition with the member. This was just an idea and is not an active development project. But - something like this is what we envision:

property foo as %String [ sqlcomputed ] {

    method Compute(someParameter as %String) as %String {

        your code goes here, set the return value and return it

    }

}

Just an idea...

Dan Pasco · Jun 16, 2017 go to post

I am not a fan of parent/children relationships. Think about what this really means. Kyle's comment about the data being nested beneath the parent object in the global is just the default physical storage model. The user has control over storage and can change that structure. But consider the logical behavior that PARENT cardinality brings. First, a relationship is like a foreign key - it is constrained by a referential constraint and it is subject to referential actions. Since this is a foreign key referencing the IDKEY of the parent class there is no need to define an ONUPDATE action (IDKEY values cannot be changed) but ONDELETE must be CASCADE. Why? Because PARENT cardinality establishes an EXISTENCE dependency on the parent. You can't change that unless you change the cardinality to ONE. Secondly, the IDKEY of the child class implicitly includes the parent relationship. This establishes an IDENTIFICATION dependency on the child class. You can't identify (the ID/OID) an instance of the child class unless you know the ID of the parent class. Furthermore, once a parent value is assigned and saved, you cannot change the value of the parent property. To do so would identify a different object and it isn't allowed. So - PARENT defines existence and identification dependencies.

To summarize PARENT/CHILDREN behavior:

1. Existence dependency;

2. Identification dependency;

3. Physical storage default is nested (artificially creating an implicit ONDELETE CASCADE behavior at the physical level - IMO this is not a good thing!)

And think about other ways to achieve the same thing using ONE/MANY or even a simple foreign key:

1. Existence dependency - make the relationship in the child class (or property if using fkey) REQUIRED;

2. Identification dependency - define the IDKEY and add the relationship with cardinality = ONE to the IDKEY;

3. Physical storage model nested - I don't recommend this model for ONE/MANY. 

The advantages of ONE/MANY:

1. The user has control over dependencies;

2. More ONDELETE/ONUPDATE options;

3. The option to base the IDKEY on a positive integer value, allowing the use of bitmap indices;

What does a relationship do for you that a foreign key does not? One thing. It maintains the relationship in memory when either side of the relationship is in memory (swizzled). This can be a good thing. It can also be a bad thing if there are a large number of related objects in the n-cardinality side.

Dan Pasco · Jun 20, 2017 go to post

Hi Peter!!

Your comment is correct of course. But think about the behavior and not the physical model. Everything that PARENT/CHILDREN relationships offers can be easily replicated using ONE/MANY with the only differences being the default storage model and the composite IDKEY that is required by PARENT/CHILDREN. With ONE/MANY you have the advantage of using system assigned integer ID's (frees you up to use bitmap indices - think product/invoice line perhaps) and you have more flexibility with referential actions. PARENT/CHILDREN requires ONDELETE=CASCADE. With ONE/MANY you have the option for ONDELETE=NOACTION/CASCADE... And, of course, if you with to establish existence dependency between the parent class and the child then simply add a REQUIRED constraint to the relationship whose cardinality = ONE.

Hope to see you at Global Summit!

Dan

Dan Pasco · Jun 28, 2017 go to post
Class User.DQ Extends %SQL.CustomQuery{Property id As %Integer;Property lastName As %String;Property firstName As %String;Property age As %Integer;Property ptr As %Integer [ Private ];Property atEnd As %Boolean [ Private ];Parameter SQLNAME As String = "DQ";Method %OpenCursor() [ Private ]{try {// Let's set up some temporary dataset ^CacheTempDQ(1) = $listBuild("Smith","John",42)set ^CacheTempDQ(2) = $listBuild("Jones","Quincy",80)set ^CacheTempDQ(3) = $listBuild("Wilson","George",71)set ^CacheTempDQ(4) = $listBuild("Garcia","Andrew",32)set ^CacheTempDQ(5) = $listBuild("Smoak","Felicity",24)set ^CacheTempDQ(6) = $listBuild("Partridge","Audrey",32)set ^CacheTempDQ(9) = $listBuild("Williams","Andrie",92)set ^CacheTempDQ(10) = $listBuild("Orr","Robert",62)set ^CacheTempDQ(11) = $listBuild("Moon","Lila",21)set ^CacheTempDQ(120) = $listBuild("Lola","Elleoh",67)set ..ptr = ""set ..atEnd = 0set ..%SQLCODE = 0set ..%Message = ""catch exception {set ..%SQLCODE = exception.AsSQLCODE()set ..%Message = exception.AsSQLMessage()}}Method %CloseCursor() [ PlaceAfter = %Next, Private ]{try {set ..ptr = ""set ..atEnd = 0kill ^CacheTempDQset ..%SQLCODE = 0set ..%Message = ""catch exception {set ..%SQLCODE = exception.AsSQLCODE()set ..%Message = exception.AsSQLMessage()}}Method %FetchCursor(ByRef sc As %Library.Status = {$$$OK}) As %Library.Integer{try {if '..atEnd {set ..ptr = $order(^CacheTempDQ(..ptr),1,data)if ..ptr {set link = ""set ..lastName = $list(data,1)set ..firstName = $list(data,2)set ..age = $list(data,3)else {set ..atEnd = 1set ..%SQLCODE = 100}}catch exception {set ..%SQLCODE = exception.AsSQLCODE()set ..%Message = exception.AsSQLMessage()}return '..atEnd}}

Then execute this statement:

select * from DQ()

or 

select * from DQ() where lastName %STARTSWITH 'S' order by age DESC

 
 
id lastName firstName age
  Smith John 42
  Smoak Felicity 24

2 row(s) affected

Dan Pasco · Aug 1, 2017 go to post

It really depends on what you are doing. For retrieving all rows from a result set the increase we measured when we did initial testing showed 4-7x improvement. Dynamic SQL is a more fully featured implementation. It was designed to be consistent with the SQL-CLI standard. It can do so much more than our other dynamic mechanisms.

Dan Pasco · Aug 1, 2017 go to post

Are you saying that %Library.ResultSet performs better than %SQL.Statement when executing an equivalent query and retrieving the result? If so, please provide timing tests showing the difference.

Dan Pasco · Aug 1, 2017 go to post

For many simple statements and common operations, they will perform nearly the same. Both employ a generated code container with an embedded SQL statement that is compiled using the same SQL query processor. It is the interface to the compiled embedded SQL query where we see differences. The new(er - implemented several years ago) dynamic SQL uses objects to scope versions, you can have multiple instances of the same query open at the same time, you can return multiple result sets from a stored procedure using CALL, you can retrieve output-directed parameters from a CALL-ed procedure, you can execute DDL statements, and so on. With %ObjectSelectMode active, you can retrieve columns that are directly swizzled, enabling access to in-memory versions of objects that may not have been saved to disk. There are many reasons to use %SQL.Statement.

Dan Pasco · Aug 3, 2017 go to post

Can you elaborate on your comment regarding status and exception mixtures with %SQL.Statement? I think the interface is completely normal - and consistent with the SQL/CLI Standard. We do report %Status from some calls but the primary error handling is through the very normal SQLCODE/%Message properties.

Dan Pasco · Aug 7, 2017 go to post

%Library.ResultSet combines the statement (prepared) and the result into a single instance. You must consume the entire result - or discard it - before you can execute the prepared statement again. %Library.ResultSet also copies the data from the underlying embedded SQL memory into the result set object - perhaps more than once. %SQL.Statement is the prepared statement and it can be executed as many times as you wish, each producing an independent result. The SQL statement result shares memory with SQL SELECT statements so data does not have to be copied in most cases.

%SQL.Statement and %SQL.StatementResult do provide status values where appropriate. However, the convention of "always return a %Status value" destroys our ability to implement true functions that return a usable return value. With the convention of always returning a %Status value, the error case is the primary check. With exceptions, errors become, well, the exception and code can be written in a more direct manner. Most modern programming languages use try/catch.

Since you like macros, you might investigate $$$THROWONERROR. This macro allows the COS programmer to combine %Status values and exceptions using try/catch, writing the error handling only once - in a CATCH block.

Dan Pasco · Aug 7, 2017 go to post

Here are two snippets. The first is as close as I can come to Amir's original example and the second is a more radical version that embraces try/catch and exceptions.

/// Always return a %StatusClassMethod SomeMethod2() As %Status{    Set tSC = $System.Status.OK()    Try    {        Set oRS = ##class(%SQL.Statement).%New()        Set tSC = oRS.%Prepare("Select Name,DOB,Home_City from Sample.Person where Name %STARTSWITH ?")        Quit:$System.Status.IsError(tSC)        Set result = oRS.%Execute("A")        if result.%SQLCODE '< 0 {            While result.%Next()            {                //Do something...            }else {            throw ##class(%Exception.SQL).CreateFromSQLCODE(result.%SQLCODE,result.%Message)        }    }    Catch (oException)    {        Set tSC = oException.AsStatus()    }    Quit tSC}

Snippet #2 - embrace exceptions

/// Always throw an exception, return something useful to the callerClassMethod SomeMethod3() as %String{
    try {
        set oRS = ##class(%SQL.Statement).%New()
        do oRS.prepare("Select Name,DOB,Home_City from Sample.Person where Name %STARTSWITH ?")
        set result = oRS.execute("A")
        while result.%Next(){
            //Do something...
        }         if result.%SQLCODE < 0 {
            throw ##class(%Exception.SQL).CreateFromSQLCODE(result.%SQLCODE,result.%Message)
        }
        set response = "something useful"
    }catch (oException){
            // process the exception - perhaps eat it and return or re-throw
            // often, we can place error/exception logging code here
        throw oException
    }
    return response}

Of course, it would be nice if there were a "next()" method that throws an exception. Even the prepare() and execute() are not "officially" documented.

HTH,

Dan

Dan Pasco · Aug 7, 2017 go to post

Yes, that was placed there for consistency with %Library.ResultSet.

Thanks for pointing that out!

Dan Pasco · Aug 7, 2017 go to post

I see two primary advantages of using exceptions over other error handling mechanisms. First is that exceptions can integrate all of the other error reporting mechanisms, allowing for consolidation of error handling code as well as the ability to report errors reported by one mechanism as an error using a different mechanism - %Status reported as SQLCODE and so on. The second is performance. Exceptions, using try/catch, are basically zero-cost for success, overhead encountered only when an exception is actually thrown. 

An additional advantage that is more subjective is code "cleanliness". Code written using try/catch/throw doesn't have to continually check for errors unless there functions not using some other error protocol are referenced.

To your question, I do not always add try/catch to a method if the catch {} block simply re-throws the exception. I only catch an exception if I need to do something to the exception before re-throwing it (or not throwing it at all) to the caller.

-Dan

PS: 

There is a macro, $$$THROWONERROR, that helps clean up calls to functions that return %Status. This macro is a convenient way to replace this pattern:

    set status = ..StatusReturningMethod()
    ​if $$$ISERR(status) { throw ##class(%Exception.StatusException).CreateFromStatus(status) }

with

    $$$THROWONERROR(status,..StatusReturningMethod())

If the status is not returned by the code, perhaps it is returned as a by-reference parameter value, then there is another macro that can help with the throw:

     $$$ThrowStatus(status)
Dan Pasco · Aug 7, 2017 go to post

Please keep in mind that you are not supposed to be checking the local variable, SQLCODE. You should be checking the %SQLCODE property of either the prepared statement or the %SQL.StatementResult instance returned by %Execute().

And, the undocumented but fully supported methods of prepare() and execute() do throw exceptions now - no status code unless you want a status code. Just catch any thrown exception and process it as you wish.

Dan Pasco · Aug 14, 2017 go to post

This should work. I assume the code you included resides in a routine named 'XJSON' and in a function named 'fromFile'? I can guess that the class referenced is %DynamicAbstractObject and that class should certainly exist in versions 2016.2 and later. In version 2016.1, this class was known as %Library.AbstractObject.

In 2016.1, the JSON syntax was much different. The correct expression for that version is:

 set obj = [].$fromJSON(stream)
Dan Pasco · Aug 14, 2017 go to post

sorry - not that the JSON syntax is different - the class and method names were different! 

Dan Pasco · Aug 15, 2017 go to post

SYSTEM is not a valid filter. That keyword allows the class compiler to organize classes into compile 'groups' so that a class that may depend on another class at compile time is guaranteed to have the dependency resolved.

It isn't an indicator that the class originated in %SYS/%CACHELIB.

Dan Pasco · Oct 18, 2017 go to post

I have to correct the record - execute() does not throw an exception, it returns a statement result. Sorry for the confusion.

Dan Pasco · Nov 27, 2017 go to post

Renaming a persistent class is complicated a few things:

  • Renaming the globals used by the extent of the class. There is no requirement that you change the global names. If you do not rename the globals and the extent is of type %Library.CacheStorage and MANAGEDEXTENT is true then we will have registered the globals as being "used by" the old class. An attempt to compile the new class could produce an error in that case. This is easily avoided - refer to ##class(%ExtentMgr.Util).DeleteExtentDefinition().
  • Subextents and %%CLASSNAME, if your class is not final and subclasses do exist then instances of the subclasses will have a non-null %%CLASSNAME value containing references to old class names. This value is not only in the data global but it will also be present in index globals. Even if you do not rename the globals you will have to update these values. The simplest way to deal with the values in the index globals is to rebuild the indices of the new class.
  • The structure of the global nodes as defined by the DATA members in the STORAGE definition. If you retain the previous DATA members from the old class then you will not have to alter the global nodes and the merge command can be used.
  • If PARENT/CHILDREN relationships exist and hierarchical storage is used then the children classes will have to be refactored as well.
  • All properties whose type class is a renamed class will have to be updated. If OID format values are used then these OID values will have to be updated as well.
  • Streams are referenced by the container object and can be stored in that container as a simple ID, an OID or as a SOID (includes storage location). Any stream reference stored as an SOID (CLASSNAME=2) will have to be refactored. The default is CLASSNAME = 0. If CLASSNAME = 0 then a simple merge will work for the stream global, assuming no other classes that are not also refactored share the same stream global.

-Dan

Dan Pasco · Jan 30, 2018 go to post

Correct. There are many reasons why a class can be added to the set. Super classes, member type classes (property type, method return type, argument types, etc.) and so on.

Dan Pasco · Jan 30, 2018 go to post

This method call is not part of any release but, as others have mentioned, you can use %Net.Http to submit a request and receive a response. I can help you write that request. How does this look?

USER>set matrix = ##class(<your classname goes here>).GetJSON("https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&origins=40.6655101,-73.89188969999998&destinations=40.6905615%2C-73.9976592%7C40.6905615%2C-73.9976592%7C40.6905615%2C-73.9976592%7C40.6905615%2C-73.9976592%7C40.6905615%2C-73.9976592%7C40.6905615%2C-73.9976592%7C40.659569%2C-73.933783%7C40.729029%2C-73.851524%7C40.6860072%2C-73.6334271%7C40.598566%2C-73.7527626%7C40.659569%2C-73.933783%7C40.729029%2C-73.851524%7C40.6860072%2C-73.6334271%7C40.598566%2C-73.7527626&key=<you api key goes here>",{"SSLConfiguration":"<your SSL Configuration goes here>"})

USER>write matrix.%ToJSON()

{"httpStatus":"200","message":"HTTP/1.1 200 OK","content":{"destination_addresses":["67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","585 Schenectady Ave, Brooklyn, NY 11203, USA","66-0-66-26 103rd St, Rego Park, NY 11374, USA","931-947 N Village Ave, Rockville Centre, NY 11570, USA","300-448 Beach 19th St, Far Rockaway, NY 11691, USA","585 Schenectady Ave, Brooklyn, NY 11203, USA","66-0-66-26 103rd St, Rego Park, NY 11374, USA","931-947 N Village Ave, Rockville Centre, NY 11570, USA","300-448 Beach 19th St, Far Rockaway, NY 11691, USA"],"origin_addresses":["566 Vermont St, Brooklyn, NY 11207, USA"],"rows":[{"elements":[{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"3.4 mi","value":5426},"duration":{"text":"19 mins","value":1136},"status":"OK"},{"distance":{"text":"8.5 mi","value":13740},"duration":{"text":"23 mins","value":1399},"status":"OK"},{"distance":{"text":"15.9 mi","value":25546},"duration":{"text":"29 mins","value":1755},"status":"OK"},{"distance":{"text":"13.2 mi","value":21296},"duration":{"text":"34 mins","value":2040},"status":"OK"},{"distance":{"text":"3.4 mi","value":5426},"duration":{"text":"19 mins","value":1136},"status":"OK"},{"distance":{"text":"8.5 mi","value":13740},"duration":{"text":"23 mins","value":1399},"status":"OK"},{"distance":{"text":"15.9 mi","value":25546},"duration":{"text":"29 mins","value":1755},"status":"OK"},{"distance":{"text":"13.2 mi","value":21296},"duration":{"text":"34 mins","value":2040},"status":"OK"}]}],"status":"OK"}}

And with a little work, this can also be invoked as an SQL function:

Row count: 1 Performance: 0.398 seconds  54 global references 2349 lines executed 0 disk read latency (ms)  Cached Query: %sqlcq.pSYS.cls4  Last update: 2018-01-30 07:09:30.073

Print

 
Expression_1
[{"httpStatus":"200","message":"HTTP/1.1 200 OK","content":{"destination_addresses":["67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","67-89 Pacific St, Brooklyn, NY 11201, USA","585 Schenectady Ave, Brooklyn, NY 11203, USA","66-0-66-26 103rd St, Rego Park, NY 11374, USA","931-947 N Village Ave, Rockville Centre, NY 11570, USA","300-448 Beach 19th St, Far Rockaway, NY 11691, USA","585 Schenectady Ave, Brooklyn, NY 11203, USA","66-0-66-26 103rd St, Rego Park, NY 11374, USA","931-947 N Village Ave, Rockville Centre, NY 11570, USA","300-448 Beach 19th St, Far Rockaway, NY 11691, USA"],"origin_addresses":["566 Vermont St, Brooklyn, NY 11207, USA"],"rows":[{"elements":[{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"6.7 mi","value":10754},"duration":{"text":"37 mins","value":2201},"status":"OK"},{"distance":{"text":"3.4 mi","value":5426},"duration":{"text":"19 mins","value":1136},"status":"OK"},{"distance":{"text":"8.5 mi","value":13740},"duration":{"text":"23 mins","value":1399},"status":"OK"},{"distance":{"text":"15.9 mi","value":25546},"duration":{"text":"29 mins","value":1755},"status":"OK"},{"distance":{"text":"13.2 mi","value":21296},"duration":{"text":"34 mins","value":2040},"status":"OK"},{"distance":{"text":"3.4 mi","value":5426},"duration":{"text":"19 mins","value":1136},"status":"OK"},{"distance":{"text":"8.5 mi","value":13740},"duration":{"text":"23 mins","value":1399},"status":"OK"},{"distance":{"text":"15.9 mi","value":25546},"duration":{"text":"29 mins","value":1755},"status":"OK"},{"distance":{"text":"13.2 mi","value":21296},"duration":{"text":"34 mins","value":2040},"status":"OK"}]}],"status":"OK"}}]

1 row(s) affected

Dan Pasco · Feb 13, 2018 go to post

There is a short answer to this and a long explanation. The short answer is that the warning is accurate and you might experience problems if you use the same name for two class members. When defining classes myself, I keep method names and property names distinct, mostly by using nouns and verbs. For indices, I prefix the name with a letter indicating the index type.

The reason why you should avoid non-unique names is simple. Most member types inherit methods from what I refer to as the member super tree. The member super tree has an abstract top node with two subnodes, the member super class and the declared type class. The member super class for properties is often called the "property class" and the declared type class is the class the author defines as the type. Methods inherited from the member super tree are implemented in the resolved member runtime as a composite name formed from the member name and the method name. For a property named 'Foo', you might see methods in the class runtime named 'FooSet' and 'FooGet'.

If two members share a name it is possible that a composite method name will conflict between the two member types. We try to avoid this when implementing our member super classes and our type classes but it is still possible to encounter runtime method name conflicts.

Dan Pasco · Feb 13, 2018 go to post

At one time - in the pre-2.0 days of Caché Objects - we did discuss this but decided to settle on the implementation we currently have. The landscape is much more complex these days and there is a greater chance of a composite method name collision or parameter name collision. Not too long ago, I had to redesign the index constraint projection (primary key, unique) and referential constraint projection (foreign key) because we had name collisions.

Dan Pasco · Apr 24, 2018 go to post

I often see user classes that extend either %RegisteredObject or %Persistent that implement only class methods with no properties at all. This produces larger-than-needed runtimes.

For classes that implement only classmethods (static) there is no reason to inherit an instantiable interface such as that provided by %RegisteredObject.

It isn't necessary but it is good practice to define such classes to be abstract.

Using a simple helper class such as the one used by the various $system.<class> classes implemented by ISC it is possible to provide help at the command prompt for all methods implemented in the class.

I tend to lean toward classes-only but I do have requirements that can only be met by using routines. I'm not a fan of the ##class() syntax and, outside of instantiation, there aren't very good alternatives to that syntax. 

Dan Pasco · Apr 24, 2018 go to post

Not necessarily "faster" at execution time but the routine generated by compiling a class that unnecessarily extends %RegisteredObject or %Persistent will be much larger than the routine generated by a class that implements only static members and is abstract. That's less space consumed. The class descriptor/dispatch table will be smaller.

Dan Pasco · Apr 25, 2018 go to post

It seems logical that a class method call will be slightly slower than a direct routine function call but I'm not sure there is much difference. I've never conducted any benchmarks in a pure environment. It would be interesting for someone to test this.