Question
· Jun 12, 2018

How to navigate to parent of Serial Object?

I have a serial object:

Class EmbedObj Extends %SerialObject

which is stored as a  property of another object

Class ContainerObj Extends %Persistent

Property InnerObj As EmbedObj;

Property Foobar As %String;

Question:

From within the context of an instance of EmbedObj, how can I navigate to the containing instance of "ContainterObj" and find that value of its Foobar property?

Harder Question:  Is there a way I can do this as part of SQLComputeCode? (my EmbedObj has a Calculated property which now needs to depend on the value of property Foobar of the containing object).

Thanks in advance for any help on how to walk this relationship via Object or SQL access.

Discussion (8)2
Log in or sign up to continue

And if so?

Class dc.EmbedObj Extends %SerialObject
{

Property pOID As %ObjectIdentity Private ];

Property As %String;

Property As %String CalculatedSqlComputeCode = {{*}=##class(dc.EmbedObj).Calcb({pOID})}, SqlComputed ];

ClassMethod Calcb(pOIDAs %String
{
  r=""
  q:pOID="" r

  s $lb(id,cls)=$lfs(pOID,"@")

  p=$system.OBJ.OpenId(cls,id)
  q:'$IsObject(pr

  cls="dc.ContainerObj" {
    r="b"_p.Foobar
  }elseif cls="dc.blablablaContainerObj" {
    r="b"_p.qwe
  }
  r
}
}

Class dc.ContainerObj Extends %Persistent
{

Property Foobar As %String;

Property InnerObj As dc.EmbedObj;

Trigger NewTrigger1 [ Event = INSERT, Foreach = row/object, Time = AFTER ]
{
  oid
  oid={%%ID}_"@"_{%%CLASSNAMEQ}
  &sql(update dc.ContainerObj set InnerObj_pOID=:oid where %ID=:{id})
}

/// d ##class(dc.ContainerObj).Test()
ClassMethod Test()
{
  ..%KillExtent()
  
  t=..%New()
  t.Foobar="foobar1"
  t.InnerObj.a="a1"
  t.%Save()

  &sql(insert into dc.ContainerObj(Foobar,InnerObj_avalues('foobar2','a2'))
  
  ##class(%SQL.Statement).%ExecDirect(,"select * from dc.ContainerObj").%Display()
}
}

I have been trying this approach but I am hitting a wall. It appears that because I am inserting the value into the embedded object in the container, it is seeing this as an additional UPDATE and calling the TRIGGER again, resulting in a <FRAMESTACK>.  Any ideas on how to prevent the <FRAMESTACK> error? 

Here is my simplified code:
 

Trigger TUpdateFoobar [ Event = INSERT/UPDATE, Foreach = row/object, Time = AFTER ]
{
  Try {
    // get value of Foobar
    NEW fb
    SET fb= {Foobar}

    // INSERT value into embedded Primary Environments
    &sql(UPDATE ContainerObj (InnerObj_Foobar)
     VALUES (:fb)
     WHERE %ID=:{ID})

Catch tError {
Do LOG^%ETN
Throw tError
}
}

Vitaliy,

Thank you very much for the working sample - I really appreciate it!!  In the end I decided to go with the approach of just projecting the property from the container into the embedded obj because it ended up being better for my particular project.  If I have to revisit this in the future because I need more than just that one property, I will definitely try out your example!

Thanks again,

Ben

Hi Ben,

from the hacker's toolbox:

Parameter MAXSTACK = 15;
Trigger TUpdateFoobar [ Event = INSERT/UPDATE, Foreach = row/object, Time = AFTER ]
{
  Try {
    // get value of Foobar
    NEW fb
    SET fb= {Foobar}
    IF $STACK > ..#MAXSTACK {
     // just for debugging
     wwrite "***",$STACK
         // INSERT value into embedded Primary Environments
         &sql(UPDATE ContainerObj (InnerObj_Foobar) 
              VALUES (:fb)
              WHERE %ID=:{ID})
    }
} Catch tError {
Do LOG^%ETN
Throw tError
}
}

You have to find out by "hacking"  a suitable value for MAXSTACK related to your application.
Inside the looping Trigger, there is no chance to identify the first run of the loop.

If you know a %variable (e.g. %BenMax) you may use it as well.
You only have to make sure it is initialized outside your trigger.  

IF $i(%BenMax) > ..#MAXSTACK {

With SQL you can initial %BenMax by a static (independent of row) WHERE Condition 

 INSERT ..... WHERE my.BENMAX()=1 and ....

The ClassMethod BENMAX may look like this:

ClassMethod BenMax(As %Integer = 1 ) As %Integer [ SqlName = BENMAX ]
{       set %BenMax=$s(n:n,1:1) quit 1    }

Thank you all ... sometimes all it takes is a good night sleep :)

I figured out to avoid recursively updating the value via the trigger .... don't update it if I don't need to :)

Here is my code - it now works like a charm!

Trigger TUpdateFoobar [ Event = INSERT/UPDATE, Foreach = row/object, Time = AFTER ] 

  {

    Try { 

      // get value of Foobar 

      NEW fb 

      SET fb= {Foobar} 

      // INSERT value into embedded foobar, *only* if it differs from Container value

      If (fb'={InnerObj_Foobar}) {   

        &sql(UPDATE ContainerObj (InnerObj_Foobar) 

            VALUES (:fb) 

            WHERE %ID=:{ID})           

        }        

    } Catch tError {             

      Do LOG^%ETN             

      Throw tError        

    }

  }

It's not the prettiest, but I think the simplest solution would be to avoid navigating to the parent object entirely:

  • Add a Foobar property to EmbedObj.
  • Via a row/object trigger in ContainerObj, propagate changes to Foobar to EmbedObj_Foobar.
    • As an initial step for data population in existing records, run SQL: update ContainerObj set EmbedObj_Foobar = Foobar
  • Base your SQLComputeCode on the copy of Foobar in the serial class.