24
24
import java .sql .SQLException ;
25
25
import java .sql .Statement ;
26
26
import java .util .ArrayList ;
27
+ import java .util .Arrays ;
27
28
import java .util .HashSet ;
28
29
import java .util .List ;
29
30
import java .util .Set ;
30
31
import java .util .UUID ;
32
+ import java .util .stream .Collectors ;
31
33
32
34
import static com .conveyal .gtfs .loader .JdbcGtfsLoader .INSERT_BATCH_SIZE ;
33
35
@@ -150,6 +152,12 @@ public String update(Integer id, String json, boolean autoCommit) throws SQLExce
150
152
updateChildTable (childEntitiesArray , entityId , isCreating , referencingTable , connection );
151
153
}
152
154
}
155
+ // Iterate over table's fields and apply linked values to any tables
156
+ if ("routes" .equals (specTable .name )) {
157
+ updateLinkedFields (specTable , jsonObject , "trips" , "route_id" , "wheelchair_accessible" );
158
+ } else if ("patterns" .equals (specTable .name )) {
159
+ updateLinkedFields (specTable , jsonObject , "trips" , "pattern_id" , "direction_id" );
160
+ }
153
161
if (autoCommit ) {
154
162
// If nothing failed up to this point, it is safe to assume there were no problems updating/creating the
155
163
// main entity and any of its children, so we commit the transaction.
@@ -173,6 +181,51 @@ public String update(Integer id, String json, boolean autoCommit) throws SQLExce
173
181
}
174
182
}
175
183
184
+ /**
185
+ * Updates linked fields with values from entity being updated. This is used to update identical fields in related
186
+ * tables (for now just fields in trips and stop_times) where the reference table's value should take precedence over
187
+ * the related table (e.g., pattern_stop#timepoint should update all of its related stop_times).
188
+ */
189
+ private void updateLinkedFields (Table referenceTable , ObjectNode jsonObject , String tableName , String keyField , String ...fieldNames ) throws SQLException {
190
+ // Collect fields, the JSON values for these fields, and the strings to add to the prepared statement into Lists.
191
+ List <Field > fields = new ArrayList <>();
192
+ List <JsonNode > values = new ArrayList <>();
193
+ List <String > fieldStrings = new ArrayList <>();
194
+ for (String field : fieldNames ) {
195
+ fields .add (referenceTable .getFieldForName (field ));
196
+ values .add (jsonObject .get (field ));
197
+ fieldStrings .add (String .format ("%s = ?" , field ));
198
+ }
199
+ String setFields = String .join (", " , fieldStrings );
200
+ // If updating stop_times, use a more complex query that joins trips to stop_times in order to match on pattern_id
201
+ boolean updatingStopTimes = "stop_times" .equals (tableName );
202
+ Field orderField = updatingStopTimes ? referenceTable .getFieldForName (referenceTable .getOrderFieldName ()) : null ;
203
+ String sql = updatingStopTimes
204
+ ? String .format ("update %s.stop_times st set %s from %s.trips t " +
205
+ "where st.trip_id = t.trip_id AND t.%s = ? AND st.%s = ?" ,
206
+ tablePrefix , setFields , tablePrefix , keyField , orderField .name )
207
+ : String .format ("update %s.%s set %s where %s = ?" , tablePrefix , tableName , setFields , keyField );
208
+ // Prepare the statement and set statement parameters
209
+ PreparedStatement statement = connection .prepareStatement (sql );
210
+ int oneBasedIndex = 1 ;
211
+ // Iterate over list of fields that need to be updated and set params.
212
+ for (int i = 0 ; i < fields .size (); i ++) {
213
+ String newValue = values .get (i ).isNull () ? null : values .get (i ).asText ();
214
+ fields .get (i ).setParameter (statement , oneBasedIndex ++, newValue );
215
+ }
216
+ // Set "where clause" with value for key field (e.g., set values where pattern_id = '3')
217
+ statement .setString (oneBasedIndex ++, jsonObject .get (keyField ).asText ());
218
+ if (updatingStopTimes ) {
219
+ // If updating stop times set the order field parameter (stop_sequence)
220
+ String orderValue = jsonObject .get (orderField .name ).asText ();
221
+ orderField .setParameter (statement , oneBasedIndex ++, orderValue );
222
+ }
223
+ // Log query, execute statement, and log result.
224
+ LOG .info (statement .toString ());
225
+ int entitiesUpdated = statement .executeUpdate ();
226
+ LOG .info ("{} {} linked fields updated" , entitiesUpdated , tableName );
227
+ }
228
+
176
229
/**
177
230
* Creates a prepared statement for an entity create or update operation. If not performing a batch operation, the
178
231
* method will set parameters for the prepared statement with values found in the provided JSON ObjectNode. The Table
@@ -279,42 +332,41 @@ private void setStatementParameters(ObjectNode jsonObject, Table table, Prepared
279
332
280
333
/**
281
334
* This updates those tables that depend on the table currently being updated. For example, if updating/creating a
282
- * pattern, this method handles updating its pattern stops and shape points. For trips, this would handle updating
335
+ * pattern, this method handles deleting any pattern stops and shape points. For trips, this would handle updating
283
336
* the trips' stop times.
337
+ *
338
+ * This method should only be used on tables that have a single foreign key reference to another table, i.e., they
339
+ * have a hierarchical relationship.
284
340
* FIXME develop a better way to update tables with foreign keys to the table being updated.
285
341
*/
286
- private void updateChildTable (ArrayNode entityList , Integer id , boolean isCreating , Table subTable , Connection connection ) throws SQLException {
342
+ private void updateChildTable (ArrayNode entityList , Integer id , boolean isCreatingNewEntity , Table subTable , Connection connection ) throws SQLException {
287
343
// Get parent table's key field
288
344
Field keyField ;
289
345
String keyValue ;
290
- // FIXME: This is shapes specific code that should probably be made generic.
291
- boolean updatingShapes = subTable .name .equals ("shapes" );
292
- if (updatingShapes ) {
293
- keyField = subTable .getFieldForName ("shape_id" );
294
- } else {
295
- keyField = specTable .getFieldForName (specTable .getKeyFieldName ());
296
- }
346
+ // Primary key fields are always referenced by foreign key fields with the same name.
347
+ keyField = specTable .getFieldForName (subTable .getKeyFieldName ());
297
348
// Get parent entity's key value
298
349
keyValue = getValueForId (id , keyField .name , tablePrefix , specTable , connection );
299
350
String childTableName = String .join ("." , tablePrefix , subTable .name );
300
351
// FIXME: add check for pattern stop consistency.
301
352
// FIXME: re-order stop times if pattern stop order changes.
302
353
// FIXME: allow shapes to be updated on pattern geometry change.
303
- if (!isCreating ) {
304
- String deleteSql ;
305
- // Delete existing sub-entities for given entity ID if the parent entity is not being created
306
- deleteSql = getUpdateReferencesSql (SqlMethod .DELETE , childTableName , keyField , keyValue , null );
354
+ if (!isCreatingNewEntity ) {
355
+ // Delete existing sub-entities for given entity ID if the parent entity is not being newly created.
356
+ String deleteSql = getUpdateReferencesSql (SqlMethod .DELETE , childTableName , keyField , keyValue , null );
307
357
LOG .info (deleteSql );
308
358
Statement statement = connection .createStatement ();
309
- // FIXME: Copy on update (instead of deleting here)
359
+ // FIXME: Use copy on update for a pattern's shape instead of deleting the previous shape and replacing it.
360
+ // This would better account for GTFS data loaded from a file where multiple patterns reference a single
361
+ // shape.
310
362
int result = statement .executeUpdate (deleteSql );
311
363
LOG .info ("Deleted {}" , result );
312
364
// FIXME: are there cases when an update should not return zero?
313
365
// if (result == 0) throw new SQLException("No stop times found for trip ID");
314
366
}
315
367
int entityCount = 0 ;
316
368
PreparedStatement insertStatement = null ;
317
- // Iterate over the entities found in the array and
369
+ // Iterate over the entities found in the array and add to batch for inserting into table.
318
370
for (JsonNode entityNode : entityList ) {
319
371
// Cast entity node to ObjectNode to allow mutations (JsonNode is immutable).
320
372
ObjectNode entity = (ObjectNode )entityNode ;
@@ -325,7 +377,10 @@ private void updateChildTable(ArrayNode entityList, Integer id, boolean isCreati
325
377
if (entityCount == 0 ) {
326
378
insertStatement = createPreparedUpdate (id , true , entity , subTable , connection , true );
327
379
}
328
- // LOG.info("{}", entityCount);
380
+ // Update linked stop times fields for updated pattern stop (e.g., timepoint, pickup/drop off type).
381
+ if ("pattern_stops" .equals (subTable .name )) {
382
+ updateLinkedFields (subTable , entity , "stop_times" , "pattern_id" , "timepoint" , "drop_off_type" , "pickup_type" , "shape_dist_traveled" );
383
+ }
329
384
setStatementParameters (entity , subTable , insertStatement , connection );
330
385
if (entityCount == 0 ) LOG .info (insertStatement .toString ());
331
386
insertStatement .addBatch ();
0 commit comments