Project

General

Profile

Feature #6584

implement schema hint which generates a POJO in addition to a DMO interface

Added by Eric Faulhaber almost 2 years ago. Updated almost 2 years ago.

Status:
Review
Priority:
Normal
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD

om20220709_6584a.diff.zip (8.51 KB) Ovidiu Maxiniuc, 07/08/2022 08:00 PM

History

#1 Updated by Eric Faulhaber almost 2 years ago

The idea is to generate a POJO equivalent of a DMO interface during schema conversion, with no binary dependencies on FWD or on a converted application, for use in scenarios where having such dependencies is not possible or is undesirable (e.g., on the remote side of RESTful APIs).

Requirements:

  • define a schema (i.e., table) hint which tells schema conversion to generate the POJO equivalent of a DMO interface;
    • table hint specifies package (or an alias representing the package) for the POJO (presumably separate from any package generated by application conversion);
    • do we default to a pojo subdirectory of the DMO interface's package?
  • no dependencies on FWD or application conversion artifacts;
  • uses only Java primitives and J2SE classes (e.g., String, BigDecimal) -- same data type mappings used between Record and BaseRecord in FWD;
  • implements Serializable (Externalizable? more work...), as the POJO must be able to be sent over the network;
  • FWD runtime support to marshall/unmarshall into associated DMO implementation class, without reflection (would only be used within the FWD server, as this implies a dependency on FWD; e.g., on the server side of RESTful APIs).

These POJOs would be compiled and jarred separately from application code (again, to break any dependency on application code). Although this is more about deployment, it is included here for completeness.

#2 Updated by Ovidiu Maxiniuc almost 2 years ago

Question: is it necessary for these POJOs that the private data to be independent fields?

I think, if this is not a requirement, we could store the private data in an array, the same way we do for FWD's BaseRecord: the data array contains only Java primitives (auto-wrapped). Same semantics. The interface is maintained but we get a both:
  • a performance boost if we use System.arraycopy() or maybe even sharing the data array if the invoker chooses so (at his own risk);
  • simplified code for constructing the new objects: instead of N assignments from POJO/DMOImpl to DMOImpl/POJO, a simple System.arraycopy() will do the work.

#3 Updated by Greg Shah almost 2 years ago

I know of no limit in this area. An array should be fine.

#4 Updated by Eric Faulhaber almost 2 years ago

Ovidiu Maxiniuc wrote:

Question: is it necessary for these POJOs that the private data to be independent fields?

I don't think this necessarily is a requirement, but doing otherwise may be "unnatural" to developers. Consider, if we use the same data array structure underneath, that the implementation of the getters and setters will be hard-coded to the positions of "fields" in the array, and this code will be visible to developers (yes, we do this today with the DMO implementation classes, but those generally are not used directly by external code, and there is no source code). Also, this would preclude the use of field-level reflection; I cannot predict how important this might be to any given developer.

These things probably are not show-stoppers, but users might find them odd at best and restrictive at worst. Even so, the performance aspect of data copy may be worth it.

I think, if this is not a requirement, we could store the private data in an array, the same way we do for FWD's BaseRecord: the data array contains only Java primitives (auto-wrapped). Same semantics. The interface is maintained but we get a both:
  • a performance boost if we use System.arraycopy() or maybe even sharing the data array if the invoker chooses so (at his own risk);
  • simplified code for constructing the new objects: instead of N assignments from POJO/DMOImpl to DMOImpl/POJO, a simple System.arraycopy() will do the work.

I do like this part; I also was thinking along these lines, but I haven't convinced myself yet. I'm not sure we want to go as far as sharing the data array directly. You note, "if the invoker chooses so...". How do you envision this working/looking?

Note that arraycopy() will work nicely for everything immutable (most types), but what about LOBs?

#6 Updated by Ovidiu Maxiniuc almost 2 years ago

Eric Faulhaber wrote:

I don't think this necessarily is a requirement, but doing otherwise may be "unnatural" to developers.

Not if they are aware that this is generated code.

Consider, if we use the same data array structure underneath, that the implementation of the getters and setters will be hard-coded to the positions of "fields" in the array, and this code will be visible to developers (yes, we do this today with the DMO implementation classes, but those generally are not used directly by external code, and there is no source code).

This is nothing else than converting identifiers to memory addresses. Any compiler does this. In stead of accessing fields from the whole heap, FWD limits the addresses of the 'variables' into the data array. Any programmer knows this and should not be something unexpected.

Also, this would preclude the use of field-level reflection; I cannot predict how important this might be to any given developer.

This is true if they want to access the private fields directly. They have this visibility level for a reason. If reflection is used to invoke the accessors (public getters and setters we generate), there should be no problem.

[...]

I do like this part; I also was thinking along these lines, but I haven't convinced myself yet. I'm not sure we want to go as far as sharing the data array directly. You note, "if the invoker chooses so...". How do you envision this working/looking?

In C/C++ there is a nice keyword, const. A function/method returning such object is sure that the receiver cannot alter it (immutable externally). This was my thinking. So, if the caller obtains a record from FWD and his only need is to serialize it (REST/JSON) but not alter it in any way, it should use the shared version of the POJO. The POJO object will also have the advantage to be 'entangled' with the original DMO, any change in DMO will immediately be reflected in POJO. However, the other way around might not fully work because of the internal maintenance the FWD does: a direct change in data will not be saved to FWD persistence because FWD will not be aware of it.
"if the invoker chooses so..." means that we may have two methods (or a parameter) which will return a mutable or immutable POJO object.

Note that arraycopy() will work nicely for everything immutable (most types), but what about LOBs?

Yes, I did think about them. Probably the LOB container will have to be duplicated each time, although it is probably safe - I think - to use shared data, at first.

#7 Updated by Eric Faulhaber almost 2 years ago

  • Assignee set to Ovidiu Maxiniuc

All this sounds good. How long do you think it will take to implement?

#8 Updated by Ovidiu Maxiniuc almost 2 years ago

A basic initial solution could be ready in 2-3 days.

#9 Updated by Constantin Asofiei almost 2 years ago

Ovidiu, on FWD side, I'm using the declared fields to determine the getter/setter, in PojoSerializer. If you are removing the individual fields associated with the getter/setter, there will be no way to determine via the reflection the POJO properties.

I understand that this is a performance/compatibility concern with the FWD Record implementation, but the goal of the POJO is to be completely decoupled from FWD, even in implementation details. What happens if at some point, for some reason, we change the FWD BaseRecord implementation?

#10 Updated by Ovidiu Maxiniuc almost 2 years ago

Constantin Asofiei wrote:

Ovidiu, on FWD side, I'm using the declared fields to determine the getter/setter, in PojoSerializer. If you are removing the individual fields associated with the getter/setter, there will be no way to determine via the reflection the POJO properties.

Using the (private) declared fields to work with the POJO object implies you have a strong coupling with the objects and their implementation. It seems to me that PojoSerializer assumes the objects are beans instead, although neither these objects have the constraints of property names having the same name with the field storing it. The POJO (and beans, as well) should be accessed only via their public interface.

I understand that this is a performance/compatibility concern with the FWD Record implementation, but the goal of the POJO is to be completely decoupled from FWD, even in implementation details. What happens if at some point, for some reason, we change the FWD BaseRecord implementation?

If we change the BaseRecord implementation, we must take into consideration the POJO<->DMO converter and adjust accordingly.

I will take into consideration your note when starting to implement this feature.

#11 Updated by Constantin Asofiei almost 2 years ago

Eric/Ovidiu: on the client-side, jackson is used to serialize to JSON via ObjectMapper. This requires class fields and getter/setter (more a bean-style). Also, it's a pain to debug a Record instance in FWD (determining which value in the data array belongs to which property...), and adding this pain to the client-side, too, I don't see a reason for it.

On FWD side, the methods which convert this POJO to a Record and back can just call the getter/setter from the POJO.

#12 Updated by Eric Faulhaber almost 2 years ago

Constantin Asofiei wrote:

Eric/Ovidiu: on the client-side, jackson is used to serialize to JSON via ObjectMapper. This requires class fields and getter/setter (more a bean-style). Also, it's a pain to debug a Record instance in FWD (determining which value in the data array belongs to which property...), and adding this pain to the client-side, too, I don't see a reason for it.

On FWD side, the methods which convert this POJO to a Record and back can just call the getter/setter from the POJO.

I do expect that Jackson will be a pretty common library for this kind of work. Constantin, are you saying Jackson does not have a simple mode which uses the public getters/setters, instead of the internal fields, to do its work? If so, this to me is a pretty convincing argument to implement the POJO as a traditional bean, having recently been forced to write a custom Jackson deserializer for DMOs, instead of being able to use the default, easy mode. My case was more about the BDT wrappers, but if the internal structure of the data storage breaks the common Jackson [de-]serialization use case, that would be a problem, IMO.

#13 Updated by Constantin Asofiei almost 2 years ago

Eric Faulhaber wrote:

I do expect that Jackson will be a pretty common library for this kind of work. Constantin, are you saying Jackson does not have a simple mode which uses the public getters/setters, instead of the internal fields, to do its work?

AFAIK Jackson does introspection for fields and from that it generates the JSON (via getter/setter etc).

#14 Updated by Eric Faulhaber almost 2 years ago

OK, let's go with the bean approach for the POJOs. They will just work with Jackson and they will not confuse developers of client/remote projects. For the tables marked by hint to generate POJOs, we can add default methods to the DMO interfaces to handle the POJO serialization/deserialization.

#15 Updated by Ovidiu Maxiniuc almost 2 years ago

  • Status changed from New to WIP
  • % Done changed from 0 to 30
I started the implementation using the bean approach. For the moment, the POJOs are always created - I will add the configuration flags at the end. As I write this note, the conversion is able to:
  • create the POJO classes in a pojo package sibling to dmo;
  • create the properties as private fields (all ABL types, including blob, clob, handle, raw, recid, etc) using the types from Record class. With the exception of clob, which is assumed to be a java.sql.Clob instead of com.goldencode.p2j.util.clob. In fact this is the only FWD type present in Record 's data; The type mapping is the following:
    typeMapping.put("blob",        "byte[]")
    typeMapping.put("character",   "String")
    typeMapping.put("clob",        "Clob")
    typeMapping.put("comhandle",   "String")
    typeMapping.put("date",        "Date")
    typeMapping.put("datetime",    "Timestamp")
    typeMapping.put("datetimetz",  "OffsetDateTime")
    typeMapping.put("decimal",     "BigDecimal")
    typeMapping.put("int64",       "Long")
    typeMapping.put("integer",     "Integer")
    typeMapping.put("handle",      "Long")
    typeMapping.put("logical",     "Boolean")
    typeMapping.put("object",      "Long")
    typeMapping.put("raw",         "byte[]")
    typeMapping.put("recid",       "Integer")
    typeMapping.put("rowid",       "Long")
    
  • create simple and indexed getters for all these fields;
  • added only necessary import statements for used data type;
  • all the above are accompanied by javadocs.

Here is an example of how the book permanent table looks as a

The generated code is, of course, fully compilable.

#16 Updated by Greg Shah almost 2 years ago

That looks really good!

Perhaps we should consider generating these for every DMO. Is there a reason to limit it to those that are marked? It seems like a useful feature in general for the long term.

#17 Updated by Ovidiu Maxiniuc almost 2 years ago

  • % Done changed from 30 to 60

I added the setters and performed more class polishing, like the initialization of extent fields as Java arrays of the specified type.

I have a few questions now:
  • now all scalar fields (including equivalent of ABL int, for example) are initialized to null (the equivalent of ? in ABL) which is not semantically equivalent to neither Java nor ABL. Is it necessary to initialize the fields to their default ABL value? (Example: int/Integer to 0, character/String to "");
  • how do we handle the initial values. Should I make the best effort to initialize the properties to respective values (including the dynamic ones like now and today)? This might require pieces of FWD runtime.
  • some of the data types used for the fields are immutable (Integer, String, OffsetDateTime, BigDecimal), others are not (Timestamp, Date, byte[]). What should be the semantics of assignment (for scalar values) in case of the latter? Do we simply use the assignment and allow them to be altered externally? I am thinking we should make a copy of these, instead;
  • how do we implement the assignment of extent/array fields? just assign the whole array (which may corrupt the POJO by assigning an array of the wrong size). I am thinking the right thing to do is to make the array/extent properties final and to copy each element on the respective position on bulk assignment. Then we need to decide how to proceed if the size is not a match.

Here is an example of how the the dame table looks now as a

The generated code is, again, fully compilable.

At this moment, the POJOs are always created. I was thinking of a switch in p2j.cfg.xml for turning their generation on or off (as opposed to individual hints) and maybe specifying the path where they are generated, although the current package seems fair.

#18 Updated by Ovidiu Maxiniuc almost 2 years ago

Added the change-set.

#19 Updated by Greg Shah almost 2 years ago

I was thinking of a switch in p2j.cfg.xml for turning their generation on or off (as opposed to individual hints)

This seems reasonable and simple enough.

maybe specifying the path where they are generated, although the current package seems fair.

I agree the default name is good. Go ahead and add the override just to be complete.

#20 Updated by Ovidiu Maxiniuc almost 2 years ago

I have implemented the marshalling methods between POJO and DMO. Here are the important highlights:
  • the Record has two new methods:
       public Object asPOJO();
       public static DataModelObject fromPOJO(Object pojo, Class<? extends DataModelObject> dmoType);
    
    They are here for the moment but they can eventually be moved in a separate (static) class which has access to BaseRecord.data;
  • the copy process in both directions uses direct copy from/to data array to private members of the POJO. Because the location is really unaware of the structure of the POJO object, the reflection is used to discover its fields. Based on the fact that these fields match the fields names from DMO, the index in dmo is used to get/set the information from/to data array. In case of extents, the information is copied in bulk, using the System.arrayCopy;
  • the asPOJO() does not require the POJO class because it is read from DMO annotation. The annotation is created only if the POJO class was generated. This also assures the flexibility of storing the POJO in any package, as long as the right path is written in @Table annotation;
  • fromPOJO method need the DMO interface, for the moment. The POJO class has no information about the DMO. I intend to add a reverse lookup support in DmoMetadataManager and avoid the extra parameter;
  • the fromPOJO() method returns now a DMO object, to match the 2nd parameter (the DMO interface), but internally, it is a local Record variable. Maybe it should be returned as such and the cast from the end dropped.

#21 Updated by Eric Faulhaber almost 2 years ago

Ovidiu Maxiniuc wrote:

I added the setters and performed more class polishing, like the initialization of extent fields as Java arrays of the specified type.

I have a few questions now:
  • now all scalar fields (including equivalent of ABL int, for example) are initialized to null (the equivalent of ? in ABL) which is not semantically equivalent to neither Java nor ABL. Is it necessary to initialize the fields to their default ABL value? (Example: int/Integer to 0, character/String to "");
  • how do we handle the initial values. Should I make the best effort to initialize the properties to respective values (including the dynamic ones like now and today)? This might require pieces of FWD runtime.

I think it is OK to make the POJOs "dumb", leaving it up to the developer to initialize the properties for their purposes when they instantiate a new POJO directly. POJOs instantiated automatically from DMOs will have the values of the DMO, so this is taken care of for us in this case.

You and Constantin have written more REST client code than I have. What do you both think?

  • some of the data types used for the fields are immutable (Integer, String, OffsetDateTime, BigDecimal), others are not (Timestamp, Date, byte[]). What should be the semantics of assignment (for scalar values) in case of the latter? Do we simply use the assignment and allow them to be altered externally? I am thinking we should make a copy of these, instead;

I am inclined to keep it simple, as you currently have it implemented, and let the developer be responsible for data safety, but I am open to other opinions.

  • how do we implement the assignment of extent/array fields? just assign the whole array (which may corrupt the POJO by assigning an array of the wrong size). I am thinking the right thing to do is to make the array/extent properties final and to copy each element on the respective position on bulk assignment. Then we need to decide how to proceed if the size is not a match.

Again, I think we leave it the way you have it, but detect a problem when unmarshalling into a DMO on the server side, as the DMO has more knowledge of valid data, mandatory data, etc.

Here is an example of how the the dame table looks now as a {{collapse(POJO:)
[...]
}}

The generated code is, again, fully compilable.

At this moment, the POJOs are always created. I was thinking of a switch in p2j.cfg.xml for turning their generation on or off (as opposed to individual hints) and maybe specifying the path where they are generated, although the current package seems fair.

I think it will be a tiny (often 0) percentage of the overall schema for which a customer will need these POJOs, so I'd rather not generate them for all tables. I'd like to stay with the hints idea. I also like the package path. The one concern I have is that the simple POJO name is the same as the simple DMO name. Won't this be messy in the server-side code, as one of them will need to be fully qualified?

#22 Updated by Eric Faulhaber almost 2 years ago

Ovidiu Maxiniuc wrote:

I have implemented the marshalling methods between POJO and DMO. Here are the important highlights:
  • the Record has two new methods:
    [...]They are here for the moment but they can eventually be moved in a separate (static) class which has access to BaseRecord.data;
  • the copy process in both directions uses direct copy from/to data array to private members of the POJO. Because the location is really unaware of the structure of the POJO object, the reflection is used to discover its fields. Based on the fact that these fields match the fields names from DMO, the index in dmo is used to get/set the information from/to data array. In case of extents, the information is copied in bulk, using the System.arrayCopy;
  • the asPOJO() does not require the POJO class because it is read from DMO annotation. The annotation is created only if the POJO class was generated. This also assures the flexibility of storing the POJO in any package, as long as the right path is written in @Table annotation;
  • fromPOJO method need the DMO interface, for the moment. The POJO class has no information about the DMO. I intend to add a reverse lookup support in DmoMetadataManager and avoid the extra parameter;
  • the fromPOJO() method returns now a DMO object, to match the 2nd parameter (the DMO interface), but internally, it is a local Record variable. Maybe it should be returned as such and the cast from the end dropped.

I thought we had discussed not using reflection and instead having default implementations of the marshalling methods directly generated into the DMO interface. At conversion time, we have full awareness of the mapping between POJO getters/setters and how fields are organized in the DMO's BaseRecord.data array. This approach is perhaps not as hidden away and elegant, but seems like it would avoid extra infrastructure. Is there a reason you moved away from this design? I am not saying rewrite what you've done, I just want to understand your choice.

One other question, about the data type mappings: I don't understand how java.sql.Clob can work within a POJO, as it requires a database connection and a transaction. This won't exist on the client side, as far as I understand.

#23 Updated by Eric Faulhaber almost 2 years ago

Eric Faulhaber wrote:

I thought we had discussed not using reflection and instead having default implementations of the marshalling methods directly generated into the DMO interface. At conversion time, we have full awareness of the mapping between POJO getters/setters and how fields are organized in the DMO's BaseRecord.data array. This approach is perhaps not as hidden away and elegant, but seems like it would avoid extra infrastructure. Is there a reason you moved away from this design? I am not saying rewrite what you've done, I just want to understand your choice.

To partially answer my own question, avoiding the duplication of error handling code in every DMO interface for POJO unmarshalling is one reason to do this in a central place.

#24 Updated by Greg Shah almost 2 years ago

Even if we want to allow for only a subset of the DMOs to get POJOs, we should make it easy to generate POJOs for all DMOs. The global flag is useful for that. Otherwise, someone will have to generate hints for every DMO, which is just a huge churn for what will likely be a common case.

It seems to me that if you are planning to use POJOs, you are trying to move development into Java and will want there to be a pure Java representation of all your tables. In my opinion, picking out a subset now and then being forced to rerun conversion to get more later is not the approach most people would choose in that case.

#25 Updated by Ovidiu Maxiniuc almost 2 years ago

Support for converting to/from POJOs was committed as r14064.

There are no hints (for the moment). The only way to enable POJO generation is to add

<parameter name="generate-pojos" value="true" />
in the desired namespace in p2j.cfg.xml.

To configure the path where they are generated use the global

<parameter name="pojoroot"                value="com.goldencode.testcases.pojoSpecialPackage" />

Here is one simple example of how the two new APIs can be used:

         foo.create();
         foo.setOptIons1(new character("one"));
         foo.setOptIons10(new character("ten"));
         foo.setPrices(0, new decimal(1.00));
         foo.setPrices(9, new decimal(10.0));

         // toPOJO() method
         Object pojo = ((BufferImpl) foo).buffer().getCurrentRecord().toPOJO();

         // Record.fromPOJO() method
         DataModelObject dmo = Record.fromPOJO(pojo1);

#26 Updated by Constantin Asofiei almost 2 years ago

Ovidiu, please post the POJO for a table which has extent fields, blob, clob, etc (all data types).

#27 Updated by Ovidiu Maxiniuc almost 2 years ago

Constantin, here it is. First the .df content:

To force it to extreme, I used a partial denormalization using

<?xml version="1.0"?>

<!-- UAST hints for p2j_test schema -->
<hints>
   <schema>
      <table name="tm-all">
         <custom-extent>
            <field name="e-1"/>
            <field name="e-2"/>
            <!-- <field name="e-3"/> this extent field remains normalized -->
            <!-- <field name="e-4"/> this extent field also remains normalized -->
         </custom-extent>
      </table>
   </schema>
</hints>

Here is the generated DMO interface:

Here is the generated POJO class:

For completeness, the in-memory DMO class looks like this:

#28 Updated by Constantin Asofiei almost 2 years ago

I don't think is OK to be using the SQL types:
  • date/datetime/datetime-tz should all map to java.util.GregorianCalendar
  • clob should map to java.lang.String - I don't even think you can transport this via the network (i.e. serialized).

#29 Updated by Ovidiu Maxiniuc almost 2 years ago

  • % Done changed from 60 to 90

This was my initial solution, where I tried to keep the same data types from FWD Record (see #6584-15).
I will add some conversion wrappers from java.sql.* types to GregorianCalendar and from Clob to java.lang.String.
Are there other concerns about the types or generally about the implementation itself?

#30 Updated by Eric Faulhaber almost 2 years ago

Ovidiu Maxiniuc wrote:

Are there other concerns about the types or generally about the implementation itself?

Did you post the runtime changes? There seem at least to be changes to the Record class, and I assume some changes in DmoClass or elsewhere to generate the to/from POJO methods in the DMO implementation class. How are errors handled during unmarshalling a POJO, if the POJO user has made mistakes (e.g., set a null array or an array of the wrong size for an extent field, set a null for a mandatory field, etc.)?

#31 Updated by Eric Faulhaber almost 2 years ago

Ovidiu, I'm running a customer application (unrelated to this task). I launched a program which tries to register a temp-table DMO and generate its implementation class. We have a

The issue is these lines in DmoClass:

      Table annotation = dmoIface.getAnnotation(Table.class);
      Class<?> pojoClass = annotation.pojoClass();

Notice in the stack trace, it is trying to register the DMO xxx.dmo._temp.Tt_2_1. This interface has no Table annotation, but its superclass xxx.dmo._temp.Tt_2 does. So, we hit NPE when trying to invoke annotation.pojoClass().

Unless we see a use case for temp-table POJOs, I think we need to exclude temp-tables from this feature.

#32 Updated by Eric Faulhaber almost 2 years ago

Eric Faulhaber wrote:

Ovidiu Maxiniuc wrote:

Are there other concerns about the types or generally about the implementation itself?

Did you post the runtime changes?

You can ignore that question; I missed the entry where you wrote you committed the changes. I didn't realize it, but clearly I have them already ;)

#33 Updated by Eric Faulhaber almost 2 years ago

Eric Faulhaber wrote:

Ovidiu, I'm running a customer application (unrelated to this task). I launched a program which tries to register a temp-table DMO and generate its implementation class. We have a {{collapse(regression.)
[...]
}}

The issue is these lines in DmoClass:

[...]

Notice in the stack trace, it is trying to register the DMO xxx.dmo._temp.Tt_2_1. This interface has no Table annotation, but its superclass xxx.dmo._temp.Tt_2 does. So, we hit NPE when trying to invoke annotation.pojoClass().

Unless we see a use case for temp-table POJOs, I think we need to exclude temp-tables from this feature.

My temporary workaround is a null check, but I think this needs a permanent fix that does not generate unnecessary POJOs for temp-tables:

=== modified file 'src/com/goldencode/p2j/persist/orm/DmoClass.java'
--- src/com/goldencode/p2j/persist/orm/DmoClass.java    2022-07-14 01:33:49 +0000
+++ src/com/goldencode/p2j/persist/orm/DmoClass.java    2022-07-14 19:09:29 +0000
@@ -325,11 +325,14 @@
       }

       Table annotation = dmoIface.getAnnotation(Table.class);
-      Class<?> pojoClass = annotation.pojoClass();
-      if (pojoClass != Object.class)
+      if (annotation != null)
       {
-         create_toPOJO_method(pojoClass, propMeta);
-         create_populateFromPOJO_method(pojoClass, propMeta);
+         Class<?> pojoClass = annotation.pojoClass();
+         if (pojoClass != Object.class)
+         {
+            create_toPOJO_method(pojoClass, propMeta);
+            create_populateFromPOJO_method(pojoClass, propMeta);
+         }
       }
    }

#34 Updated by Ovidiu Maxiniuc almost 2 years ago

I already committed the same fix (r14069).

Notice that, for the moment, the POJOs are not supported for temp database. That is mainly because it is impossible (to my knowledge) to configure the generate-pojos in the p2j.cfg.xml for this database.

#35 Updated by Ovidiu Maxiniuc almost 2 years ago

Constantin Asofiei wrote:

I don't think is OK to be using the SQL types:
  • date/datetime/datetime-tz should all map to java.util.GregorianCalendar
  • clob should map to java.lang.String - I don't even think you can transport this via the network (i.e. serialized).

I have committed r14071. It will generate POJOs using the above types. The toPOJO() method will apply the required conversion when creating the POJOs. The reverse implementation is underway.

#36 Updated by Ovidiu Maxiniuc almost 2 years ago

  • % Done changed from 90 to 100

I committed the (final ?) version of the POJO/DMO marshalling solution. Revision is 14075.

To summarize:
  • the POJOs are created only if the generate-pojos parameter for respective schema was set to "true" (or "yes") in the desired namespace in p2j.cfg.xml. Generation of POJOs for temp-tables is not (yet) possible simply because this cannot be configured;
  • the destination package for POJOs can be configured using the global parameter pojoroot of p2j.cfg.xml. If not specified, a pojo package sibling to dmo will be used;
  • the data types used by POJOs are J2SE classes. The POJOs have no dependencies on FWD code. The full mapping can be found in rules/schema/java_pojo.xml at lines 100-116. All date related use GregorianCalendar instances and the clob is mapped to its String content. The static methods for these conversion are found at the end of Record class ("to...");
  • to obtain a POJO from a DMO object, the toPOJO() method of Record is used. If the POJO class was defined, an object of that type will be returned. On any error null is returned. The method is abstract in Record but it is implemented on-the-fly in the DMO implementation class. A hard constraint for this is that the conversion method needs access to protected data from BaseRecord. The assembler will use the record's recordMeta to generate the method's code without using reflection at all. The required conversions to POJO types are injected as required;
  • to obtain a DMO record from a POJO, the Record.fromPOJO() static method must be used. If the class of the parameter was registered as a POJO type when a DMO interface was registered with DmoMetadataManager then an object of this specific type will be created and populated with data from the POJO object sent as parameter. If the object's type is unknown or in case of other errors, null is return.
    To populate the new DMO with data, the fromPOJO() invokes populateFromPOJO() for the newly created object. At this moment, the DMO object is aware of the type of the POJO and knows what getter methods to access. This method is also abstract in Record and assembled at runtime, when the DMO is registered. Again, there is no reflection used and, if required, static conversion methods from Record class are injected;
  • both to and from assembled methods will attempt to use System.arraycopy when possible for normalized extent fields, when possible. If a type conversion is required or the field was denormalized, this is no longer possible;
  • the POJO classes implement the Serializable interface as a marker but it can be easily removed or changed to Externalizable. This case requires additional work for generating the read/write methods.

#37 Updated by Ovidiu Maxiniuc almost 2 years ago

Constantin,
I know you worked with JSON probably more than any of us. Do you have an API which serialize a POJOs and DMOs as JSON strings? I would like to run some extensive testing here and I think JSON output is a fast way to see differences between objects (using meld eventually) with a minimum of additional code.
Thank you!

#38 Updated by Constantin Asofiei almost 2 years ago

In 3821c/14082 I've added support for Java enums and, if there is no FWD serializer registered for the given type, default to jackson. From your example, a fully initialized tm-all pojo looks like this:

{
    "response": {
        "response": {
            "f1": "abcdef",
            "e1": [
                "a",
                "b",
                "c",
                "d" 
            ],
            "f2": 1658133922731,
            "e2": [
                1689669922731,
                1658169922731
            ],
            "f3": 1234.567,
            "e3": [],
            "f4": 987654,
            "e4": [
                1,
                2,
                3,
                4
            ],
            "f5": 123456,
            "f6": false,
            "f7": 1658133922731,
            "f8": 1658133922731,
            "f9": "enhjdmJubQ==",
            "f10": "asdfgh",
            "f11": "NTY3ODkw",
            "f12": 987654
        }
    }
}

The only difference I see from FWD and jackson is that GregorianCalendar is not serialized as ISO-8601, but as its timestamp. I don't know what happens with the timezone, I didn't test this.

Ovidiu, regarding your question about JSON serialization:
  • for DMOs, FWD has the RecordSerializer - this serializes the DMO as a Json object, with each property serialized using its FWD serializer (BaseDataTypeSerializer)
  • for types which don't have a serializer registered in FWD, this defaults to jackson - the code is in ObjectSerializer - this uses ObjectMapper to read and write the JSON; keep in mind that the entire object is serialized using jackson, FWD serializers will not be used.
    • ObjectMapper.readValue(jsonString, type); to create an instance
    • ObjectMapper.valueToTree(object); to create a JSON node structure.

You can use ObjectMapper standalone to experiment.

#39 Updated by Ovidiu Maxiniuc almost 2 years ago

The documentation can be found at:
https://proj.goldencode.com/projects/timco/wiki/POJOs_-_Tutorial#section-8

I could not find a better place to store it, at this moment.

I also committed revision 14089. It primarily contains a new method associate() in RB which allows a DMO 'rogue' object created using Record.fromPOJO to be taken into the FWD's Session management and be saved. And a few fixes.

Constantin,
I realized that POJO and DMO are not fully compatible when serialized as POJOs. Generally we cannot serialize a DMO and deserialize it as POJO because each has different naming preferences: the DMO will use the legacy name while the POJO will use the property names for the element/attributes. More than that, the serialization of DMO-s does not handle well the denormalized fields (because all properties are mapped to same legacy name).

The best/only solution here is to convert the DMOs to POJOs and then serialize, following at the other end to do the operations is reverse order. From my tests, I could not find any issue in conversion between DMOs and POJOs (both ways).

#40 Updated by Greg Shah almost 2 years ago

I took a quick look at the docs. It looks good. Please move it to here:

Using POJOs for Database Entities

Eric: Please review.

Generally we cannot serialize a DMO and deserialize it as POJO because each has different naming preferences: the DMO will use the legacy name while the POJO will use the property names for the element/attributes.

This is a reasonable restriction. Let's just make sure the limitation is clearly documented.

Is there anything else needed for this task? Do we need a more specific example or some other edits to the customer-specific guide?

#41 Updated by Greg Shah almost 2 years ago

I made some minor edits to the doc. Please review.

#42 Updated by Ovidiu Maxiniuc almost 2 years ago

  • Status changed from WIP to Review

Greg Shah wrote:

I made some minor edits to the doc. Please review.

Thank you for your contribution. I moved the page to new location, removed the link for its old parent document, but kept a redirection message instead of deleting the whole document.

#43 Updated by Ovidiu Maxiniuc almost 2 years ago

Greg Shah wrote:

This is a reasonable restriction. Let's just make sure the limitation is clearly documented.

I added a final section Note on JSON Compatibility between DMOs and POJOs at the end of the document. However, I am positive we can switch the whole protocol to use properties as field names and intermediary switch to data types (for date, datetime and datetime-tz), but we will have to use a separate method than the one we use for 'native' JSON/XML serialization which has to keep compatibility with ABL.

Is there anything else needed for this task? Do we need a more specific example or some other edits to the customer-specific guide?

If there are specific cases in customer code which pose difficulties I am glad to help, but this will probably need to happen in a private task.

Also available in: Atom PDF