How get properties of a class, sorted by order in storage
Hello
I want to get the property of a class, sorted by order in storage.
I know we can use
Set definition = ##class(%Dictionary.ClassDefinition).%OpenId(className)
Set listProperty = definition.Properties
For ii = 1:1:listProperty.Count(){
write listProperty.GetAt(ii).NameBut using GetAt sorts the results alphabetically.
Example :
Class Test.classExtends (%SerialObject, %XML.Adaptor, %JSON.Adaptor)
{
Property tiers As%String;Property journal As%String;
}listProperty.GetAt(1).Name = "journal" and listProperty.GetAt(2).Name = "tiers"
But I would like it to be instead: listProperty.GetAt(1).Name = "tiers" and listProperty.GetAt(2).Name = "journal"
Do you have a solution, please?
Corentin
Comments
Hello @Corentin Blondeau
Here is the code to retrieve the properties in the same order they are defined in the class.
ClassMethod GetPropOnDefinedSequence(pClass As%String = "", Output pProperties)
{
For {
Setproperty=$$$comMemberNext(pClass,$$$cCLASSproperty,property)
Ifproperty=""QuitIfproperty="%%OID"!(property="%Concurrency") continueSet pProperties(
+$$$defMemberKeyGet(pClass,$$$cCLASSproperty,property,$$$cPROPsequencenumber),
property)=""
}
Return$$$OK
} The properties assigned to the pProperties parameter
properties(2,"City")=""
properties(3,"IsActive")=""
properties(4,"State")=""
Previous version of my reply was for the storage order for data stored. Sorry.
An easy and simple (assuming standard storage strategy) way to get those informations (the slot numbers, where a property is stored) could be achieved with a simple classmethod
Class DC.PropInfo [ Abstract ]
{
/// Info about properties of a class:/// - list of all properties stored in a list/// - slot number for a given property/// /// 1) add this class to your class definition/// class some.class extends (%Persistent, DC.StorageInfo) or/// class some.class extends (%SerialClass, DC.StorageInfo)/// /// 2) then use it as follows/// write ##class(some.class).PropInfo() --> list of property names/// write ##class(some.class).PropInfo("Age") --> slot number for the Age property/// /// write ##class(ClassMethod PropInfo(name = "") As%String [ CodeMode = objectgenerator ]
{
set sto=%compiledclass.Storages, prp=0, list=""if sto.Count()=1 {
set dat=sto.GetAt(1).Data
for i=1:1:dat.Count() if dat.GetAt(i).Structure="listnode"set prp=dat.GetAt(i) quitif prp {
if%compiledclass.ClassType="serial" { set list="", i=1 } else { set list=$lb(""), i=2 }
for i=i:1:prp.Values.Count() set list=list_$lb(prp.Values.GetAt(i).Value)
}
do%code.WriteLine(" if name="""" quit """_$lts(list)_"""")
do%code.WriteLine(" quit $lf($lfs("""_$lts(list)_"""),name)")
}
if list=""write !,"*** No properties found! ***"quit$$$OK
}
}
Two test classes
Class DC.TestPerson Extends (%Persistent, DC.PropInfo)
{
Property Name As%String;Property Addr As DC.Address;Property Salary As%Numeric;Property Expertise As list Of %String;
}
Class DC.Address Extends (%SerialObject,DC.PropInfo)
{
Property Street As%String;Property City As%String;
}Now you can do things like:
write##class(DC.TestPerson).PropInfo() --> ,Name,Addr,Salary,Expertise
write##class(DC.TestPerson).PropInfo("Salary") --> 4// you can use the slot number for direct data access:write$list(^DC.TestPersonD(id),slotnumber) gives you the same value aswrite##class(DC.TestPerson).SalaryGetStored(id)
// the same game for serial classeswrite##class(DC.Address).PropInfo() --> Street,City
write##class(DC.Address).PropInfo("City") --> 2// in case you have an instanceset pers=##class(DC.TestPerson).%OpenId(id)
write pers.PropInfo() --> ,Name,Addr,Salary,Expertise
write pers.Addr.PropInfo() --> Street,City
// etc.Thanks for your responses !!!
I found a way to do it with SQL :
Set sqlQuery = "SELECT * FROM %Dictionary.PropertyDefinition WHERE parent = '"_ className _"' ORDER BY SequenceNumber"Set resultSet = ##class(%SQL.Statement).%New()
Set status = resultSet.%Prepare(sqlQuery)
$$$ThrowOnError(status)
Set tResult = resultSet.%Execute()
While tResult.%Next() {
Set Name = tResult.%Get("Name")
...
}Corentin
Never, ever, concatenate parameters to an SQL Query!
Fixed for you:
Set sqlQuery = "SELECT * FROM %Dictionary.PropertyDefinition WHERE parent = ? ORDER BY SequenceNumber"Set resultSet = ##class(%SQL.Statement).%New()
Set status = resultSet.%Prepare(sqlQuery)
$$$ThrowOnError(status)
Set tResult = resultSet.%Execute(className)
While tResult.%Next() {
Set Name = tResult.%Get("Name")
...
}Back to your initial question, what is your definition of "sorted by Storage"?
If you need the ($list) position of properties within the global, then your query does not answer your question.
I'm writing this in case other community members read this question/answer.
In response to my good friend Enrico's comment about "storage":
SELECT cc.Name as class_name,cs.Name as storage_name,cd.Name as data_node_name,cd.structure as structure, cd.subscript as subscript,cd.attribute asattribute, cv.name as value_name, cv.value asvalueFROM %Dictionary.CompiledClass cc
JOIN %Dictionary.CompiledStorage cs on cc.%ID = cs.parent
JOIN %Dictionary.CompiledStorageData cd on cs.%ID = cd.parent
LEFTOUTERJOIN %Dictionary.CompiledStorageDataValue cv on cd.%ID = cv.parent
WHERE cc.name = 'User.Person'And a general comment about dynamic SQL. In the early days of Caché, someone made the decision to return a status value from methods to indicate success/failure. We didn't have try/catch at the time so it made some sense. Now we have try/catch (regrettably, no finally) and dynamic SQL follows a pattern that I started using many years ago - the traditional %<capital case> methods return a %Status value and a non-percent, lower camelCase method throws an exception. For dynamic SQL, that is prepare() and execute().
The primary benefit in using the exception-throwing method interface is that you cannot completely ignore errors unless you use a specific pattern to do so.
LATEST:USER>zw sql
sql=6
sql(1)="SELECT cc.Name as class_name,cs.Name as storage_name,cd.Name as data_node_name,cd.structure as structure, cd.subscript as subscript,cd.attribute as attribute, cv.name as value_name, cv.value as value"
sql(2)=" FROM %Dictionary.CompiledClass cc"
sql(3)=" JOIN %Dictionary.CompiledStorage cs on cc.%ID = cs.parent"
sql(4)=" JOIN %Dictionary.CompiledStorageData cd on cs.%ID = cd.parent"
sql(5)=" LEFT OUTER JOIN %Dictionary.CompiledStorageDataValue cv on cd.%ID = cv.parent"
sql(6)=" WHERE cc.name = 'User.Person'"
LATEST:USER>do statement.prepare(.sql)
LATEST:USER>set result = statement.execute()
LATEST:USER>do result.%Display()
class_name storage_name data_node_name structure subscript attribute value_name value
User.Person Default DescriptionNode node d description
User.Person Default PersonDefaultData listnode 1 name
User.Person Default PersonDefaultData listnode 2 dob
User.Person Default PersonDefaultData listnode 3 address
User.Person Default PersonDefaultData listnode 4 counter
5 Rows(s) Affected
I think, Enrico was more aiming for the point that “concatenation is the gateway for SQL injection.”
Perhaps I should have been more clear. I was responding to Enrico's comment about sequencing properties as they are stored in a global.
Back to your initial question, what is your definition of "sorted by Storage"?
If you need the ($list) position of properties within the global, then your query does not answer your question. I'm writing this in case other community members read this question/answer.
And there, my friend, we differ completely. ;-)
"someone made the decision to return a status value from methods to indicate success/failure."
A brilliant idea! (Joe D. ?)
In my code I don't want to catch anything, I want to know if a certain method/function did what it supposed to do and if not: report tat to me in a polite fashion (%Status).
@Herman Slagman - Perhaps we will meet again and we can have a vigorous debate! Your turn to buy ;)
It is an interesting exercise, but what is the point of it ?
💡 This question is considered a Key Question. More details here.
I think the easiest way is by defining the storage
.png)