Project

General

Profile

2137_20221114a.patch

Constantin Asofiei, 11/14/2022 10:31 AM

Download (48.2 KB)

View differences:

new/src/com/goldencode/p2j/persist/AbstractQuery.java 2022-11-10 19:19:54 +0000
220 220
**     OM  20210917          Code maintenance.
221 221
**     OM  20220112          Added implementation of SKIP-DELETED-RECORD attribute.
222 222
**     AL2 20220412          Do proxy checks for values used as query parameters.
223
**     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
224
**                           as 'partial/incomplete'.
223 225
*/
224 226

  
225 227
/*
......
282 284
import java.util.logging.*;
283 285
import com.goldencode.p2j.persist.event.*;
284 286
import com.goldencode.p2j.persist.lock.*;
287
import com.goldencode.p2j.persist.orm.*;
285 288
import com.goldencode.p2j.util.*;
286 289
import com.goldencode.p2j.util.ErrorManager;
287 290

  
......
323 326
   /** Flag to indicate if this query is optimized using indexed reposition */
324 327
   protected boolean indexedReposition = false;
325 328
   
329
   /** The included properties for each DMO (FIELDS option). */
330
   protected Map<DataModelObject, Property[]> included = null;
331

  
332
   /** The excluded properties for each DMO (EXCEPT option). */
333
   protected Map<DataModelObject, Property[]> excluded = null;
334
   
326 335
   /** Flag indicating fatal error was encountered processing query substitution parameters */
327 336
   private boolean fatalError = false;
328 337
   
......
3173 3182
    */
3174 3183
   public void include(DataModelObject dmo, String ... fields)
3175 3184
   {
3176
      // UnimplementedFeature.missing("RECORD-PHRASE: FIELDS (...) option");
3177
      // TODO: implement
3185
      RecordBuffer buffer = (RecordBuffer) ((BufferReference) dmo).buffer();
3186
      if (buffer.isTemporary())
3187
      {
3188
         return;
3189
      }
3190
      
3191
      if (included == null)
3192
      {
3193
         included = new IdentityHashMap<>();
3194
      }
3195
      
3196
      collectFields(included, dmo, fields, buffer);
3178 3197
   }
3179 3198
   
3180 3199
   /**
......
3188 3207
    */
3189 3208
   public void exclude(DataModelObject dmo, String ... fields)
3190 3209
   {
3191
      // UnimplementedFeature.missing("RECORD-PHRASE: EXCLUDE (...) option");
3192
      // TODO: implement
3210
      RecordBuffer buffer = (RecordBuffer) ((BufferReference) dmo).buffer();
3211
      if (buffer.isTemporary())
3212
      {
3213
         return;
3214
      }
3215
      
3216
      if (excluded == null)
3217
      {
3218
         excluded = new IdentityHashMap<>();
3219
      }
3220
      
3221
      collectFields(excluded, dmo, fields, buffer);
3193 3222
   }
3194 3223
   
3195 3224
   /**
......
3429 3458
         return new logical(false);
3430 3459
      }
3431 3460
   }
3461
   
3462
   /**
3463
    * Populate the map of with the ORM properties specified in the given fields.
3464
    * 
3465
    * @param    target
3466
    *           The target map.
3467
    * @param    dmo
3468
    *           The DMO instance.
3469
    * @param    fields
3470
    *           The fields to resolve.
3471
    * @param    buffer
3472
    *           The buffer instance.
3473
    */
3474
   private void collectFields(Map<DataModelObject, Property[]> target, 
3475
                              DataModelObject                  dmo, 
3476
                              String[]                         fields, 
3477
                              RecordBuffer                     buffer)
3478
   {
3479
      DmoMeta meta = buffer.getDmoInfo();
3480

  
3481
      Property[] props = target.computeIfAbsent(dmo, (d) ->
3482
      {
3483
         return new Property[meta.getFieldSize()];
3484
      });
3485
      
3486
      for (int i = 0; i < fields.length; i++)
3487
      {
3488
         Property field = meta.getFieldInfo(fields[i]);
3489
         if (field == null)
3490
         {
3491
            // must be a custom extent
3492
            List<Property> extFields = meta.getCustomExtentFieldInfo(fields[i]);
3493
            for (int j = 0; j < extFields.size(); j++)
3494
            {
3495
               field = extFields.get(j);
3496
               props[field.propId - 1] = field;
3497
            }
3498
         }
3499
         else if (field.extent == 0)
3500
         {
3501
            props[field.propId - 1] = field;
3502
         }
3503
      }
3504
   }
3432 3505
}
new/src/com/goldencode/p2j/persist/FastFindCache.java 2022-11-09 18:35:42 +0000
14 14
**                  negative result (i.e., no record is found) do not have the negative result cached.
15 15
**     CA  20220104 Node.l3Cache must be a weak reference, otherwise once the direct cache invalidates a key,
16 16
**                  the reverse lookup has no chance of being cleaned up, as this is not done explicitly.
17
**     CA  20221109 Partial/Incomplete records (loaded with EXCEPT/FIELDS options) can't be cached.
17 18
*/
18 19

  
19 20
/*
......
262 263
    */
263 264
   public void put(Key k, Record value)
264 265
   {
265
      // if no key or record represents a housekeeping (e.g., dirty database) copy, don't store it
266
      if (k == null || (value != null && value.checkState(DmoState.COPY)))
266
      // if no key or record represents a housekeeping (e.g., dirty database) copy or an incomplete record 
267
      // (partial fields are populated due to EXCEPT/FIELDS options), don't store it
268
      if (k == null || 
269
          (value != null && (value.checkState(DmoState.COPY) || value.checkState(DmoState.INCOMPLETE))))
267 270
      {
268 271
         return; // throw some error?
269 272
      }
new/src/com/goldencode/p2j/persist/Persistence.java 2022-11-10 11:14:34 +0000
577 577
**                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
578 578
**                           processing all the scopeables for each and every block.
579 579
**     TJD 20220504          Upgrade do Java 11 minor changes
580
**     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
581
**                           as 'partial/incomplete'.
580 582
*/
581 583

  
582 584
/*
......
634 636

  
635 637
package com.goldencode.p2j.persist;
636 638

  
637
import java.lang.*;
638 639
import java.sql.*;
639 640
import java.util.*;
641
import java.util.BitSet;
640 642
import java.util.Date;
641 643
import java.util.function.*;
642 644
import java.util.logging.*;
......
1756 1758
            // have to take another hit retrieving the data almost immediately after this method
1757 1759
            // returns, which is the common case for dynamic record retrieval.
1758 1760
            Class<? extends Record> dmoClass = buffer.getDMOImplementationClass();
1759
            initialResult = result = session.get(dmoClass, id);
1761
            initialResult = result = session.get(dmoClass, id, null);
1760 1762
            
1761 1763
            if (result == null)
1762 1764
            {
......
1978 1980
   throws PersistenceException,
1979 1981
          LockTimeoutException
1980 1982
   {
1983
      return load(implClass, id, lockType, timeout, updateLock, null);
1984
   }
1985
   
1986
   /**
1987
    * Load a single record from the session-level cache or, optionally, refresh it from the
1988
    * database. The record will be locked with the specified lock type upon return from this
1989
    * method. Note that the lock refers to a runtime-level lock, not a database-level lock.
1990
    *
1991
    * @param   implClass
1992
    *          The DMO implementation class of record being read.
1993
    * @param   id
1994
    *          Primary key of the record to be loaded into the buffer.  May not be {@code null}.
1995
    * @param   lockType
1996
    *          Type of lock the lock manager should register on the resulting record for the
1997
    *          current user context.
1998
    * @param   timeout
1999
    *          Number of milliseconds to wait to acquire the requested lock type. Throws
2000
    *          {@link LockTimeoutException} if this time elapses before the lock is acquired.
2001
    *          Specify 0 to wait indefinitely for the lock. Ignored if {@code lockType} is
2002
    *          {@code NONE}.
2003
    * @param   updateLock
2004
    *          {@code true} to update the current lock state;
2005
    *          {@code false} to leave the current lock state unchanged.
2006
    * @param   partialFields
2007
    *          For an incomplete record, read only these fields.
2008
    *
2009
    * @return  A single DMO instance loaded from the database, or {@code null} if no result was
2010
    *          found.
2011
    *
2012
    * @throws  LockTimeoutException
2013
    *          if a record was found, but its lock could not be acquired within the positive
2014
    *          {@code timeout} period specified.
2015
    * @throws  LockUnavailableException
2016
    *          if a record was found and a no-wait lock type was specified, but the lock could
2017
    *          not be acquired immediately.
2018
    * @throws  PersistenceException
2019
    *          if there was an error loading the DMO from the ORM session and/or database.
2020
    */
2021
   public Record load(Class<? extends Record> implClass,
2022
                      Long id,
2023
                      LockType lockType,
2024
                      long timeout,
2025
                      boolean updateLock,
2026
                      BitSet partialFields)
2027
   throws PersistenceException,
2028
          LockTimeoutException
2029
   {
1981 2030
      boolean trace = LOG.isLoggable(Level.FINEST);
1982 2031
      boolean warn = (trace || LOG.isLoggable(Level.WARNING));
1983 2032
      boolean trackSlow = warn && loadThreshold > 0L;
......
2025 2074
         }
2026 2075
         
2027 2076
         Session session = local.getSession();
2028
         result = session.get(implClass, id);
2077
         result = session.get(implClass, id, partialFields);
2029 2078
         
2030 2079
         if (result == null)
2031 2080
         {
new/src/com/goldencode/p2j/persist/PreselectQuery.java 2022-11-10 15:36:23 +0000
587 587
**     CA  20221006          Added JMX instrumentation for query assemble. Refs #6814
588 588
**     CA  20221013          In 'repositionByID', if the row number is after the last row in the ResultSet,
589 589
**                           assume an error (query off-end) and do not try to fetch the record.
590
**     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
591
**                           as 'partial/incomplete'.   At this time, extent fields are always loaded.
590 592
*/
591 593

  
592 594
/*
......
4439 4441
         
4440 4442
         boolean qualified = !isOmitAlias();
4441 4443
         
4444
         RecordBuffer buffer = comp.getBuffer();
4445

  
4442 4446
         if (qualified)
4443 4447
         {
4444
            RecordBuffer buffer = comp.getBuffer();
4445 4448
            buf.append(buffer.getDMOAlias());
4446 4449
         }
4447 4450
         
......
4454 4457
            
4455 4458
            buf.append(DatabaseManager.PRIMARY_KEY);
4456 4459
         }
4460
         else 
4461
         {
4462
            Property[] iprops = included == null ? null : included.get(buffer.getDMOProxy());
4463
            Property[] xprops = excluded == null ? null : excluded.get(buffer.getDMOProxy());
4464
            if (iprops != null || xprops != null)
4465
            {
4466
               Property[] props = null;
4467
               
4468
               // only permanent buffers can use include/exclude
4469
               if (iprops == null)
4470
               {
4471
                  DmoMeta meta = buffer.getDmoInfo();
4472
                  
4473
                  props = new Property[meta.getFieldSize()];
4474
                  
4475
                  // include all fields
4476
                  Iterator<Property> piter = meta.getFields(false);
4477
                  while (piter.hasNext())
4478
                  {
4479
                     Property p = piter.next();
4480
                     
4481
                     if (p.propId > 0)
4482
                     {
4483
                        props[p.id - 1] = p;
4484
                     }
4485
                  }
4486
               }
4487
               else
4488
               {
4489
                  props = new Property[iprops.length];
4490
                  System.arraycopy(iprops, 0, props, 0, props.length);
4491
               }
4492
               
4493
               if (xprops != null)
4494
               {
4495
                  // exclude properties
4496
                  for (int j = 0; j < xprops.length; j++)
4497
                  {
4498
                     Property xprop = xprops[j];
4499
                     if (xprop != null)
4500
                     {
4501
                        props[j] = null;
4502
                     }
4503
                  }
4504
               }
4505
               
4506
               String alias = buffer.getDMOAlias();
4507
               buf.append('.').append(DatabaseManager.PRIMARY_KEY);
4508
               for (int j = 0; j < props.length; j++)
4509
               {
4510
                  Property p = props[j];
4511
                  // normalized extents are always loaded (can't be skipped) at this time.
4512
                  if (p == null || (p.extent != 0 && !p.denormalized))
4513
                  {
4514
                     continue;
4515
                  }
4516
                  
4517
                  buf.append(", ").append(alias).append(".").append(p.name);
4518
               }
4519
            }
4520
         }
4457 4521
      }
4458 4522
      
4459 4523
      buf.append(" ");
......
5802 5866
                  {
5803 5867
                     try
5804 5868
                     {
5805
                        dmo = p.load(buffer.getDMOImplementationClass(), id, lt, timeout, updateLock);
5869
                        // force the load of the entire record
5870
                        dmo = p.load(buffer.getDMOImplementationClass(), id, lt, timeout, updateLock, null);
5806 5871
                       
5807 5872
                        break;
5808 5873
                     }
......
5853 5918
               }
5854 5919
               else
5855 5920
               {
5856
                  dmo = p.load(buffer.getDMOImplementationClass(), id, lt, 0L, updateLock);
5921
                  dmo = p.load(buffer.getDMOImplementationClass(), 
5922
                               id, 
5923
                               lt, 
5924
                               0L, 
5925
                               updateLock, 
5926
                               // the entire record is loaded if the lock is not NONE
5927
                               updateLock || dmo == null ? null : dmo._getReadFields());
5857 5928
               }
5858 5929
            }
5859 5930
            
new/src/com/goldencode/p2j/persist/Record.java 2022-11-10 06:41:22 +0000
22 22
**     OM  20220713 Added support for converting to/from POJOs.
23 23
**     OM  20220718 Fixed looping in getPropertyValues(). Added to the returned list the denormalized props.
24 24
**     TJD 20220504 Upgrade do Java 11 minor changes
25
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
26
**                  'partial/incomplete'.
25 27
*/
26 28

  
27 29
/*
......
143 145
    */
144 146
   public final integer _getInteger(int offset)
145 147
   {
148
      checkIncomplete(offset);
149

  
146 150
      Integer datum = (Integer) data[offset];
147 151
      
148 152
      return datum != null ? new integer(datum) : new integer();
......
250 254
    */
251 255
   public final datetimetz _getDatetimetz(int offset)
252 256
   {
257
      checkIncomplete(offset);
258

  
253 259
      OffsetDateTime datum = (OffsetDateTime) data[offset];
254 260
      
255 261
      return datum != null ? new datetimetz(datum) : new datetimetz();
......
354 360
    */
355 361
   public final datetime _getDatetime(int offset)
356 362
   {
363
      checkIncomplete(offset);
364

  
357 365
      Timestamp datum = (Timestamp) data[offset];
358 366
      
359 367
      return datum != null ? new datetime(datum) : new datetime();
......
457 465
    */
458 466
   public final date _getDate(int offset)
459 467
   {
468
      checkIncomplete(offset);
469

  
460 470
      Date datum = (Date) data[offset];
461 471
      
462 472
      return datum != null ? new date(datum) : new date();
......
560 570
    */
561 571
   public final logical _isLogical(int offset)
562 572
   {
573
      checkIncomplete(offset);
574

  
563 575
      Boolean datum = (Boolean) data[offset];
564 576
      
565 577
      return datum != null ? new logical(datum) : new logical();
......
770 782
    */
771 783
   public final decimal _getDecimal(int offset)
772 784
   {
785
      checkIncomplete(offset);
786

  
773 787
      BigDecimal datum = (BigDecimal) data[offset];
774 788
      
775 789
      return datum != null ? new decimal(datum) : new decimal();
......
886 900
    */
887 901
   public final character _getCharacter(int offset)
888 902
   {
903
      checkIncomplete(offset);
904

  
889 905
      String datum = (String) data[offset];
890 906
      
891 907
      if (datum != null)
......
996 1012
    */
997 1013
   public final rowid _getRowid(int offset)
998 1014
   {
1015
      checkIncomplete(offset);
1016

  
999 1017
      Long datum = (Long) data[offset];
1000 1018
      
1001 1019
      return datum != null ? new rowid(datum) : new rowid();
......
1101 1119
    */
1102 1120
   public final handle _getHandle(int offset)
1103 1121
   {
1122
      checkIncomplete(offset);
1123

  
1104 1124
      Long datum = (Long) data[offset];
1105 1125
      
1106 1126
      return datum != null ? handle.fromResourceId(datum, false) : new handle();
......
1206 1226
    */
1207 1227
   public final recid _getRecid(int offset)
1208 1228
   {
1229
      checkIncomplete(offset);
1230

  
1209 1231
      Integer datum = (Integer) data[offset];
1210 1232
      
1211 1233
      return datum != null ? new recid(datum) : new recid();
......
1310 1332
    */
1311 1333
   public final blob _getBlob(int offset)
1312 1334
   {
1335
      checkIncomplete(offset);
1336

  
1313 1337
      byte[] datum = (byte[]) data[offset];
1314 1338
      
1315 1339
      return datum != null ? new blob(datum) : new blob();
......
1415 1439
    */
1416 1440
   public final clob _getClob(int offset)
1417 1441
   {
1442
      checkIncomplete(offset);
1443

  
1418 1444
      return new clob((String) data[offset], getCodePage(offset));
1419 1445
   }
1420 1446
   
......
1516 1542
    */
1517 1543
   public final comhandle _getComhandle(int offset)
1518 1544
   {
1545
      checkIncomplete(offset);
1546

  
1519 1547
      String datum = (String) data[offset];
1520 1548
      
1521 1549
      return datum != null ? comhandle.fromString(datum) : new comhandle();
......
1619 1647
    */
1620 1648
   public final object<?> _getObject(int offset)
1621 1649
   {
1650
      checkIncomplete(offset);
1651

  
1622 1652
      Long datum = (Long) data[offset];
1623 1653
      
1624 1654
      return datum != null ? object.fromResourceId(datum) : new object<>();
......
1726 1756
    */
1727 1757
   public final raw _getRaw(int offset)
1728 1758
   {
1759
      checkIncomplete(offset);
1760

  
1729 1761
      byte[] datum = (byte[]) data[offset];
1730 1762
      
1731 1763
      return datum != null ? new raw(datum) : raw.instantiateUnknownRaw();
......
1893 1925
            {
1894 1926
               sb.append("\"");
1895 1927
            }
1896
            sb.append(data[i] == null ? "?" : data[i]);
1928
            
1929
            boolean incomplete = readFields != null && !readFields.get(i);
1930
            
1931
            sb.append(data[i] == null ? incomplete ? "n/a" : "?" : data[i]);
1897 1932
            if (isChar)
1898 1933
            {
1899 1934
               sb.append("\"");
new/src/com/goldencode/p2j/persist/orm/BaseRecord.java 2022-11-09 18:38:23 +0000
28 28
**                  used in a Java REST service, which must not be associated with a session.
29 29
**     ECF 20221005 Added support for dynamic initial values.
30 30
**     TJD 20220504 Upgrade do Java 11 minor changes
31
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
32
**                  'partial/incomplete'.
31 33
*/
32 34

  
33 35
/*
......
128 130
   /** The data represented by this record, in it's low-level (database friendly) form */
129 131
   protected final Object[] data;
130 132
   
133
   /** For an incomplete record, this holds the fields which were read or assigned in this record. */
134
   protected BitSet readFields = null;
135
   
131 136
   /** Current state of the record */
132 137
   protected final RecordState state = new RecordState();
133 138
   
......
785 790
   }
786 791
   
787 792
   /**
793
    * Get the {@link #readFields}.
794
    * 
795
    * @return   The {@link #readFields}.
796
    */
797
   public BitSet _getReadFields()
798
   {
799
      return readFields;
800
   }
801

  
802
   /**
788 803
    * Set a single datum into the data array, and update the record and property state accordingly.
789 804
    * 
790 805
    * @param   offset
......
794 809
    */
795 810
   protected void setDatum(int offset, Object datum)
796 811
   {
812
      if (readFields != null)
813
      {
814
         readFields.set(offset);
815
      }
816

  
797 817
      if (activeBuffer != null)
798 818
      {
799 819
         activeOffset = offset;
......
1015 1035
   }
1016 1036
   
1017 1037
   /**
1038
    * Check if for an incomplete record, the specified field was read or assigned.
1039
    * 
1040
    * @param    index
1041
    *           The field index.
1042
    */
1043
   protected void checkIncomplete(int index)
1044
   {
1045
      if (checkState(DmoState.INCOMPLETE) && !readFields.get(index))
1046
      {
1047
         RecordMeta meta = _recordMeta();
1048
         String tname = meta.legacyName;
1049
         String fname = meta.getPropertyMeta(false)[index].getLegacyName();
1050
         String msg = "Field " + fname + " from " + tname + " record (recid " + primaryKey() + 
1051
                       ") was missing from FIELDS phrase";
1052
         
1053
         ErrorManager.recordOrThrowError(8826, msg, false);
1054
      }
1055
   }
1056

  
1057
   /**
1058
    * Mark this record as incomplete.
1059
    * 
1060
    * @param    session
1061
    *           Database session (used for UNDO purposes).
1062
    */
1063
   void markIncomplete(Session session)
1064
   {
1065
      updateState(session, DmoState.INCOMPLETE, true);
1066
      readFields = new BitSet(data.length);
1067
   }
1068

  
1069
   /**
1018 1070
    * An index was updated.
1019 1071
    * 
1020 1072
    * @param   idxUid
......
1225 1277
   final int readProperty(int propOffset, ResultSet rs, int rsOffset)
1226 1278
   throws PersistenceException
1227 1279
   {
1280
      return readProperty(propOffset, rs, rsOffset, false);
1281
   }
1282
   
1283
   /**
1284
    * Read a property directly from a {@code ResultSet}.
1285
    *
1286
    * @param   propOffset
1287
    *          The index of the property to be read.
1288
    * @param   rs
1289
    *          The result set to read from.
1290
    * @param   rsOffset
1291
    *          The index in {@code ResultSet} to read from. If the property is a multiple column
1292
    *          then more than one column will ve read from {@code ResultSet}.
1293
    * @param   skip
1294
    *          Flag indicating if this property will not be read from the result-set.
1295
    *          
1296
    * @return  The number of columns actually read from the {@code ResultSet}.
1297
    */
1298
   final int readProperty(int propOffset, ResultSet rs, int rsOffset, boolean skip)
1299
   throws PersistenceException
1300
   {
1228 1301
      PropertyMeta pm = _recordMeta().getPropertyMeta(false)[propOffset];
1229 1302
      PropertyReader propertyReader = pm.getDataHandler();
1303
      if (skip)
1304
      {
1305
         return propertyReader.propertySize();
1306
      }
1230 1307
      
1231 1308
      try
1232 1309
      {
1310
         if (readFields != null)
1311
         {
1312
            readFields.set(propOffset);
1313
         }
1314
         
1233 1315
         return propertyReader.readProperty(rs, rsOffset, data, propOffset);
1234 1316
      }
1235 1317
      catch (java.sql.SQLException sqle)
new/src/com/goldencode/p2j/persist/orm/DmoMeta.java 2022-11-14 14:47:24 +0000
37 37
**                  DmoMeta to avoid duplicating their collection and storage.
38 38
**     OM  20220727 FieldId and PropertyId are different for denormalized extent fields. Renamed some fields
39 39
**                  to a better suited name.
40
**     TJD 20220504 Upgrade do Java 11 minor changes
40
**     TJD 20220504 Upgrade do Java 11 minor changes.
41
**     CA  20221114 Added API to get the denormalized properties for a DMO extent property name.
41 42
*/
42 43

  
43 44
/*
......
536 537
   }
537 538
   
538 539
   /**
540
    * Get the list of denormalized properties associated with this extent DMO property.
541
    * 
542
    * @param    propName
543
    *           The field's property name.
544
    *           
545
    * @return   The list of {@link #customExtents denormalized} properties, or <code>null</code> if this is
546
    *           not a denormalized or not a extent property.
547
    */
548
   public List<Property> getCustomExtentFieldInfo(String propName)
549
   {
550
      return customExtents.get(propName);
551
   }
552
   
553
   /**
554
    * Get the number of properties in this DMO.
555
    * 
556
    * @return   The size of {@link #propsByName}.
557
    */
558
   public int getFieldSize()
559
   {
560
      return propsByName.size();
561
   }
562
   
563
   /**
539 564
    * Looks up a {@code Property} by its legacy field name.<p>
540 565
    * <b>Note:</b><br>
541 566
    *    This method is rather slow. Do not use it unless you have no other solution.<br>
new/src/com/goldencode/p2j/persist/orm/DmoState.java 2022-11-09 18:38:44 +0000
2 2
** Module   : DmoState.java
3 3
** Abstract : DMO states which govern persistence behavior.
4 4
**
5
** Copyright (c) 2020-2021, Golden Code Development Corporation.
5
** Copyright (c) 2020-2022, Golden Code Development Corporation.
6 6
**
7 7
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
8 8
** 001 ECF 20200315 First revision.
9 9
** 002 ECF 20210519 Added TRACKED state.
10 10
**     ECF 20210923 Added DELETING state.
11
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
12
**                  'partial/incomplete'.
11 13
*/
12 14

  
13 15
/*
......
110 112
   
111 113
   /** DMO currently is being tracked for possible UNDO processing */
112 114
   public static final int TRACKED = 0x0400;
115
   
116
   /** DMO is read incomplete (due to EXCEPT/FIELDS options) and can't be cached. */
117
   public static final int INCOMPLETE = 0x0800;
113 118
}
new/src/com/goldencode/p2j/persist/orm/Loader.java 2022-11-14 15:28:02 +0000
11 11
**                  (Temporary?) set the properties to be accessed with old-fashioned accessors.
12 12
**     CA  20201005 Fixed the sort order when loading a child extent table.
13 13
**     TJD 20220504 Upgrade do Java 11 minor changes
14
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
15
**                  'partial/incomplete'.  Extent fields are always loaded at this time.
14 16
*/
15 17

  
16 18
/*
......
69 71
package com.goldencode.p2j.persist.orm;
70 72

  
71 73
import com.goldencode.p2j.persist.*;
74
import com.goldencode.p2j.util.*;
75

  
72 76
import java.sql.*;
73 77
import java.util.*;
74 78

  
......
312 316
   }
313 317
   
314 318
   /**
319
    * Compose the index where each group of normalized extent properties start in the DMO properties array.
320
    * 
321
    * @param   allPropMeta
322
    *          Array of {@link PropertyMeta} objects in their natural order.
323
    * 
324
    * @return  An integer array with the start index of each group of normalized extent properties.  The 
325
    *          first element is always zero.
326
    */
327
   static int[] composeLoadStatementsIndexes(PropertyMeta[] allPropMeta)
328
   {
329
      List<Integer> indexes = new ArrayList<>();
330
      
331
      int i = 0;
332
      int len = allPropMeta.length;
333
      int seriesSize = 0;
334
      
335
      // primary record (if it contains scalar data)
336
      if (len > 0 && allPropMeta[0].getExtent() == 0)
337
      {
338
         // first one is always zero for primary record
339
         indexes.add(i);
340
         seriesSize = allPropMeta[0].seriesSize;
341
         i = seriesSize;
342
      }
343
      
344
      // secondary records (if any)
345
      while (i < len)
346
      {
347
         indexes.add(i);
348

  
349
         PropertyMeta propMeta = allPropMeta[i];
350
         int extent = propMeta.getExtent();
351
         seriesSize = propMeta.seriesSize;
352
         
353
         for (int j = 0; j < seriesSize; j++)
354
         {
355
            propMeta = allPropMeta[i];
356
            i += extent;
357
         }
358
      }
359
      
360
      return Utils.integerCollectionToPrimitive(indexes);
361
   }
362

  
363
   /**
315 364
    * Load a single record from the database, given its DMO class and primary key. The primary
316 365
    * key is assumed to represent a valid identifier; if not, {@code null} will be returned.
317 366
    * <p>
......
323 372
    *          DMO implementation class.
324 373
    * @param   id
325 374
    *          Surrogate primary key.
326
    * 
375
    *          
327 376
    * @return  An instance of {@code dmoClass}, loaded with data from the database, or {@code
328 377
    *          null} if the record was not found. The DMO is instantiated either way, but it will
329 378
    *          be discarded if the record is not found.
......
334 383
   <T extends BaseRecord> T load(Class<T> dmoClass, Long id)
335 384
   throws PersistenceException
336 385
   {
386
      return load(dmoClass, id, null);
387
   }
388
   
389
   /**
390
    * Load a single record from the database, given its DMO class and primary key. The primary
391
    * key is assumed to represent a valid identifier; if not, {@code null} will be returned.
392
    * <p>
393
    * TODO: a SQL statement is prepared and closed for each load statement executed by this
394
    * method. We rely on statement caching at the connection pool level to make this performant,
395
    * but perhaps there is some predictive analysis we can do to make this more efficient.
396
    * 
397
    * @param   dmoClass
398
    *          DMO implementation class.
399
    * @param   id
400
    *          Surrogate primary key.
401
    * @param   partialFields
402
    *          For an incomplete record, read only these fields.
403
    *          
404
    * @return  An instance of {@code dmoClass}, loaded with data from the database, or {@code
405
    *          null} if the record was not found. The DMO is instantiated either way, but it will
406
    *          be discarded if the record is not found.
407
    * 
408
    * @throws  PersistenceException
409
    *          if an error occurred accessing the database.
410
    */
411
   <T extends BaseRecord> T load(Class<T> dmoClass, Long id, BitSet partialFields)
412
   throws PersistenceException
413
   {
337 414
      try
338 415
      {
339 416
         T dmo = dmoClass.getDeclaredConstructor().newInstance();
340 417
         dmo.primaryKey(id);
341 418
         
342
         return load(dmo, id);
419
         return load(dmo, id, partialFields);
343 420
      }
344 421
      catch (ReflectiveOperationException  exc)
345 422
      {
......
360 437
    *          DMO implementation class.
361 438
    * @param   id
362 439
    *          Surrogate primary key.
440
    * @param   partialFields
441
    *          For an incomplete record, read only these fields.
363 442
    * 
364 443
    * @return  An instance of {@code dmoClass}, loaded with data from the database, or {@code
365 444
    *          null} if the record was not found. The DMO is instantiated either way, but it will
......
368 447
    * @throws  PersistenceException
369 448
    *          if an error occurred accessing the database.
370 449
    */
371
   <T extends BaseRecord> T load(T dmo, Long id)
450
   <T extends BaseRecord> T load(T dmo, Long id, BitSet partialFields)
372 451
   throws PersistenceException
373 452
   {
374 453
      try
......
380 459
         boolean hasScalarData = props.length > 0 && props[0].getExtent() == 0;
381 460
         
382 461
         int start = 0;
462
         if (partialFields != null)
463
         {
464
            dmo.markIncomplete(session);
465
         }
383 466
         
384 467
         for (String sql : loadSql)
385 468
         {
......
404 487
                  }
405 488
                  
406 489
                  // the first query is always on the primary table, if the DMO has scalar data
407
                  start = readScalarData(dmo, rs, 0, props);
490
                  start = readScalarData(dmo, rs, 0, props, partialFields);
408 491
               }
409 492
               else
410 493
               {
......
448 531
    *          use 0. If the record data is prepended by a single column (Ex: primaryKey), pass 1.
449 532
    * @param   props
450 533
    *          Array of property metadata objects.
534
    * @param   partialFields
535
    *          For an incomplete record, read only these fields.
451 536
    * 
452 537
    * @return  Index of next slot in the data array to be filled (if any) after this series of
453 538
    *          scalar data has been read, or -1 to indicate the result set was empty (record
......
459 544
   private static int readScalarData(BaseRecord r,
460 545
                                     ResultSet rs,
461 546
                                     int rowOffset,
462
                                     PropertyMeta[] props)
547
                                     PropertyMeta[] props,
548
                                     BitSet partialFields)
463 549
   throws SQLException, PersistenceException
464 550
   {
465 551
      // the current row is expected to be already selected, since the filter is the primary
......
480 566
            break;
481 567
         }
482 568
         
483
         int colCnt = r.readProperty(pm.offset, rs, pm.columnIndex + rowOffset);
569
         boolean skip = partialFields != null && !partialFields.get(pm.offset);
570
         int colCnt = r.readProperty(pm.offset, rs, pm.columnIndex + rowOffset, skip);
484 571
         
485 572
         if (colCnt != pm.columnCount)
486 573
         {
new/src/com/goldencode/p2j/persist/orm/Property.java 2022-11-14 15:22:23 +0000
10 10
**     OM  20201218 Fixed implementation for error and rejected attributes/hidden fields.
11 11
**     CA  20210831 Track the getter method for TempRecord reserved properties.
12 12
**     OM  20220722 Added property identifier.
13
**     CA  20221114 Added flag to mark a denormalized extent property.
13 14
*/
14 15

  
15 16
/*
......
127 128
    */
128 129
   public final int extent;
129 130
   
131
   /** Flag indicating if this extent property has been denormalized. */
132
   public final boolean denormalized;
133
   
130 134
   /**
131 135
    * One-based index in the legacy extent field associated with a DMO property. Will be set only
132 136
    * for field with custom extents.
......
255 259
   Property(com.goldencode.p2j.persist.annotation.Property source, Method annotatedMethod)
256 260
   {
257 261
      this.id = source.id();
258
      this.propId = (source.propertyId() == Integer.MIN_VALUE) ? this.id : source.propertyId();
262
      this.denormalized = source.propertyId() != Integer.MIN_VALUE;
263
      this.propId = !this.denormalized ? this.id : source.propertyId();
259 264
      this.name = source.name().intern();
260 265
      this.legacy = source.legacy().intern();
261 266
      this.column = source.column().intern();
......
333 338
   {
334 339
      this.id = this.propId = id; // this constructor is only invoked for reserved properties which have
335 340
                                  // negative and equal values for both [id] and [propId]
341
      this.denormalized = false;
336 342
      this.name = name.intern();
337 343
      this.legacy = (legacy == null) ? null : legacy.intern();
338 344
      this.column = column.intern();
new/src/com/goldencode/p2j/persist/orm/PropertyReader.java 2022-11-09 18:39:36 +0000
3 3
** Abstract : Functional interface for reading a (possibly multiple column) property from a SQL
4 4
**            result set.
5 5
**
6
** Copyright (c) 2020, Golden Code Development Corporation.
6
** Copyright (c) 2020-2022, Golden Code Development Corporation.
7 7
**
8 8
** -#- -I- --Date-- ---------------------------------Description---------------------------------
9 9
** 001 OM  20200212 Created initial version.
10
** 002 CA  20221109 Added 'propertySize' method which returns the number of columns in the result-set for this
11
**                  property type.
10 12
*/
11 13

  
12 14
/*
......
93 95
    */
94 96
   public int readProperty(ResultSet rs, int rsOffset, Object[] data, int propIndex)
95 97
   throws SQLException;
98
   
99
   /**
100
    * Get the size (number of columns in the result-set) required by this property.
101
    * 
102
    * @return   Defaults to 1.
103
    */
104
   public default int propertySize()
105
   {
106
      return 1;
107
   }
96 108
}
new/src/com/goldencode/p2j/persist/orm/RecordMeta.java 2022-11-14 15:26:55 +0000
20 20
**     OM  20220727 FieldId and PropertyId are different for denormalized extent fields. Both are computed
21 21
**                  at conversion time.
22 22
**     ECF 20221005 Added support for dynamic initial values.
23
**     CA  20221114 Added a helper array to keep the index of the normalized extent properties in the DMO
24
**                  'props' array.
23 25
*/
24 26

  
25 27
/*
......
135 137
   
136 138
   /** SQL query statement(s) to access data needed to load a single DMO instance */
137 139
   final String[] loadSql;
140

  
141
   /** 
142
    * The index in {@link #props} array where each {@link #loadSql} statement has the properties 
143
    * (with the same extent) stored.  The value of <code>loadSqlIndexes[0]</code> is always zero, as the
144
    * {@link #loadSql}'s first element is the scalar properties SELET. 
145
    */
146
   final int[] loadSqlIndexes;
138 147
   
139 148
   /** SQL insert statement(s) which insert a single DMO's data into the database */
140 149
   final String[] insertSql;
......
232 241
      props = RecordMeta.readPropertyMeta(dmoMeta);
233 242
      tables = RecordMeta.tableNames(dmoMeta, props);
234 243
      loadSql = Loader.composeLoadStatements(tables[0], props, temporary);
244
      loadSqlIndexes = Loader.composeLoadStatementsIndexes(props);
235 245
      insertSql = Persister.composeInsertStatements(tables[0], props, temporary);
236 246
      deleteSql = Persister.composeDeleteStatements(tables);
237 247
      
new/src/com/goldencode/p2j/persist/orm/RecordState.java 2022-11-09 18:39:43 +0000
2 2
** Module   : RecordState.java
3 3
** Abstract : Current state of a database record in an application.
4 4
**
5
** Copyright (c) 2019-2021, Golden Code Development Corporation.
5
** Copyright (c) 2019-2022, Golden Code Development Corporation.
6 6
**
7 7
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
8 8
** 001 ECF 20191001 First revision. Object which tracks DMO state as changes are applied to it.
9 9
** 002 ECF 20210519 Added TRACKED state to toString.
10 10
**     ECF 20210924 Added DELETING state to toString.
11
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
12
**                  'partial/incomplete'.
11 13
*/
12 14

  
13 15
/*
......
144 146
         }
145 147
         if ((state & DmoState.TRACKED) == DmoState.TRACKED)
146 148
         {
147
            sb.append("TRACKED");
149
            sb.append("TRACKED ");
150
         }
151
         if ((state & DmoState.INCOMPLETE) == DmoState.INCOMPLETE)
152
         {
153
            sb.append("INCOMPLETE");
148 154
         }
149 155
      }
150 156
      sb.append("}");
new/src/com/goldencode/p2j/persist/orm/SQLQuery.java 2022-11-10 14:50:30 +0000
33 33
**                  a wrapper for two separate parameters, changed hydrateRecordImpl logic to skip fields that are not
34 34
**                  queried.
35 35
**     TJD 20220504 Upgrade do Java 11 minor changes
36
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
37
**                  'partial/incomplete'.  Extent fields are always loaded at this time.
36 38
*/
37 39

  
38 40
/*
......
684 686
      return res[0];
685 687
   }
686 688
   
687
   
688 689
   /**
689 690
    * Hydrate a {@code Record} from a {@code ResultSet}, ie a new object that implements the DMO
690 691
    * interface and having the properties set from the current row of the record set. The method
......
771 772
         }
772 773
         int k = 0;
773 774
         int recOffset = 0;
774
         boolean hasExtents = false;
775
         // TODO: EXCEPT/FIELDS for extent fields; for now, load all extents
776
         boolean incomplete = fieldEntries != allFieldEntries;
777
         boolean hasExtents = incomplete; 
778
         if (incomplete)
779
         {
780
            r.markIncomplete(session);
781
         }
775 782
         
776 783
         for (Map.Entry<String, Property> prop : fieldEntries)
777 784
         {
......
831 838
                     rsOffset += r.readProperty(recOffset++, resultSet, rsOffset);
832 839
                     break;
833 840
                  default:
834
                     if (fieldEntries != allFieldEntries)
841
                     if (incomplete)
835 842
                     {
836 843
                        recOffset = dmoInfo.recordMeta.getIndexOfProperty(key);
837
                     }
838
                     rsOffset += r.readProperty(recOffset++, resultSet, rsOffset);
844
                        rsOffset += r.readProperty(recOffset, resultSet, rsOffset);
845
                     }
846
                     else
847
                     {
848
                        rsOffset += r.readProperty(recOffset++, resultSet, rsOffset);
849
                     }
839 850
                     break;
840 851
               }
841 852
            }
......
845 856
         if (hasExtents)
846 857
         {
847 858
            RecordMeta recordMeta = r._recordMeta();
859
            PropertyMeta[] props = recordMeta.getPropertyMeta(false);
848 860
            String[] loadSqls = recordMeta.loadSql;
861
            int[] loadSqlsIndexes = recordMeta.loadSqlIndexes;
862
            
849 863
            // only use the extent queries, skip the loadSqls[0] which contains the simple properties
850 864
            for (int i = 1; i < loadSqls.length; i++) // skip "plain" columns 
851 865
            {
866
               if (incomplete)
867
               {
868
                  // resolve the offset in the 'props' array where this sql starts.  
869
                  // all normalized extent fields are loaded even for incomplete records.
870
                  recOffset = loadSqlsIndexes[i];
871
               }
872

  
852 873
               ResultSet rs = null;
853 874
               try (PreparedStatement ps = conn.prepareStatement(loadSqls[i]))
854 875
               {
855 876
                  // set the [parent_id] parameter
856 877
                  ps.setLong(1, r.primaryKey());
857 878
                  rs = ps.executeQuery();
858
                  recOffset = Loader.readExtentData(r, rs, recordMeta.getPropertyMeta(false), recOffset);
879
                  recOffset = Loader.readExtentData(r, rs, props, recOffset);
859 880
               }
860 881
               finally
861 882
               {
new/src/com/goldencode/p2j/persist/orm/Session.java 2022-11-09 18:40:30 +0000
25 25
**     ECF 20211022 A cache expiration is not tracked as an undoable event.
26 26
**     ECF 20211230 Do not attempt to delete a newly created record that was never flushed to the database.
27 27
**     OM  20220628 Signature change for createSQLQuery() method.
28
**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
29
**                  'partial/incomplete'.  Partial/Incomplete records are being cached, but when they try to
30
**                  be loaded with no 'partial field information', the cached record is fully refreshed from
31
**                  the database, and not just returned.
28 32
*/
29 33

  
30 34
/*
......
450 454
   public <T extends BaseRecord> T get(Class<T> dmoImplClass, Long id)
451 455
   throws PersistenceException
452 456
   {
453
      return getImpl(dmoImplClass, id, new RecordIdentifier<>(dmoImplClass.getName(), id));
457
      return getImpl(dmoImplClass, id, new RecordIdentifier<>(dmoImplClass.getName(), id), null);
458
   }
459

  
460
   /**
461
    * Gets a specified record of a table. To lookup the record local cache is analyzed first. If
462
    * the record is not found in cache, the backing database is queried. If the specified record
463
    * is not found in database, {@code null} is returned. If the record was fetched from the
464
    * database at this moment, it is kept in cache for further access.
465
    *
466
    * @param   <T>
467
    *          The type of the record.
468
    * @param   dmoImplClass
469
    *          The class of the entity (class that implements the table's DMO interface).
470
    * @param   id
471
    *          The id/rowid of the record to be returned.
472
    * @param   partialFields
473
    *          For an incomplete record, read only these fields.
474
    *
475
    * @return  The requested record, if found in cache or database and {@code null} otherwise.
476
    *
477
    * @throws  PersistenceException
478
    *          In case some error occurs in the process.
479
    */
480
   public <T extends BaseRecord> T get(Class<T> dmoImplClass, Long id, BitSet partialFields)
481
   throws PersistenceException
482
   {
483
      return getImpl(dmoImplClass, id, new RecordIdentifier<>(dmoImplClass.getName(), id), partialFields);
454 484
   }
455 485
   
456 486
   /**
......
473 503
   public <T extends BaseRecord> T get(RecordIdentifier<String> key)
474 504
   throws PersistenceException
475 505
   {
476
      return getImpl(null, key.getRecordID(), key);
506
      return getImpl(null, key.getRecordID(), key, null);
477 507
   }
478 508
   
479 509
   /**
......
497 527
   public <T extends BaseRecord> T get(String entity, Long id)
498 528
   throws PersistenceException
499 529
   {
500
      return getImpl(null, id, new RecordIdentifier<>(entity, id));
530
      return getImpl(null, id, new RecordIdentifier<>(entity, id), null);
501 531
   }
502 532
   
503 533
   /**
......
513 543
    * @param   key
514 544
    *          The cache key of the entity. If {@code null} the returned value is {@code null};
515 545
    *          provided.
546
    * @param   partialFields
547
    *          For an incomplete record, read only these fields.
516 548
    * @param   <T>
517 549
    *          The compile type of the record to be returned. 
518 550
    *
......
522 554
    * @throws  PersistenceException
523 555
    *          if there is an error getting a JDBC connection from the data source.
524 556
    */
525
   private <T extends BaseRecord> T getImpl(Class<T> dmoImplClass, Long id, RecordIdentifier<String> key)
557
   private <T extends BaseRecord> T getImpl(Class<T> dmoImplClass, 
558
                                            Long id, 
559
                                            RecordIdentifier<String> key, 
560
                                            BitSet partialFields)
526 561
   throws PersistenceException
527 562
   {
528 563
      // quick out
......
544 579
      
545 580
      T dmo = getCached(key);
546 581
      
582
      boolean refreshIncomplete = false;
547 583
      if (dmo != null)
548 584
      {
549 585
         // assert that we have the right DMO type for this primary key
......
557 593
            throw new PersistenceException(msg);
558 594
         }
559 595
         
560
         return dmo;
596
         if (dmo.checkState(DmoState.INCOMPLETE) && partialFields == null)
597
         {
598
            // we have a cached record which is incomplete, and this requires to refresh the incomplete record
599
            // from the database
600
            refreshIncomplete = true;
601
         }
602
         else
603
         {
604
            return dmo;
605
         }
561 606
      }
562 607
      
563 608
      if (dmoImplClass == null)
......
579 624
         throw new PersistenceException("Invalid entity name: " + dmoImplClass.getName());
580 625
      }
581 626
      
582
      dmo = loader.load(dmoImplClass, id);
627
      T loaded = loader.load(dmoImplClass, id, partialFields);
628
      if (refreshIncomplete)
629
      {
630
         // evict the incomplete record from the cache, will be recached if one was found
631
         evict(dmo);
632
         
633
         if (loaded == null)
634
         {
635
            dmo = null;
636
         }
637
         else
638
         {
639
            System.arraycopy(loaded.data, 0, dmo.data, 0, dmo.data.length);
640

  
641
            // set the incomplete record as complete
642
            dmo.updateState(this, DmoState.INCOMPLETE, false);
643
            dmo.readFields = null;
644
         }
645
      }
646
      else
647
      {
648
         dmo = loaded;
649
      }
583 650
      
584 651
      if (dmo != null)
585 652
      {
......
962 1029
      // ignore the cache and go directly to underlying database
963 1030
      // use the variant of load which overwrites the DMOs current field values with data
964 1031
      // fetched from the database
965
      T loaded = loader.load(dmo, dmo.id);
1032
      T loaded = loader.load(dmo, dmo.id, null);
966 1033
      
967 1034
      RecordIdentifier<String> key = new RecordIdentifier<>(dmo.getClass().getName(), dmo.id);
968 1035
      
new/src/com/goldencode/p2j/persist/orm/types/DatetimetzType.java 2022-11-09 18:41:02 +0000
13 13
**     CA  20210304 Fixed datetimetz literal parsing.
14 14
**     OM  20220218 Fixed timezone when the dtz is rehydrated.
15 15
**     ECF 20221005 Added support for dynamic initial values.
16
**     CA  20221109 Added 'propertySize' method which returns the number of columns in the result-set for this
17
**                  property type.
16 18
*/
17 19

  
18 20
/*
......
180 182
   }
181 183
   
182 184
   /**
185
    * Get the number of columns read from the result-set.
186
    * 
187
    * @return   always 2.
188
    */
189
   @Override
190
   public int propertySize()
191
   {
192
      return 2;
193
   }
194
   
195
   /**
183 196
    * Instantiate a low-level initial value suitable for storage in a {@link BaseRecord}'s data
184 197
    * array, parsed from a string representation.
185 198
    *