1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.openjpa.jdbc.meta.strats;
20
21 import java.sql.SQLException;
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.Map;
26
27 import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
28 import org.apache.openjpa.jdbc.kernel.JDBCStore;
29 import org.apache.openjpa.jdbc.meta.ClassMapping;
30 import org.apache.openjpa.jdbc.meta.FieldMapping;
31 import org.apache.openjpa.jdbc.meta.FieldStrategy;
32 import org.apache.openjpa.jdbc.schema.Column;
33 import org.apache.openjpa.jdbc.schema.ForeignKey;
34 import org.apache.openjpa.jdbc.sql.Joins;
35 import org.apache.openjpa.jdbc.sql.Result;
36 import org.apache.openjpa.jdbc.sql.Select;
37 import org.apache.openjpa.jdbc.sql.SelectExecutor;
38 import org.apache.openjpa.jdbc.sql.Union;
39 import org.apache.openjpa.kernel.OpenJPAStateManager;
40 import org.apache.openjpa.meta.JavaTypes;
41 import org.apache.openjpa.util.ChangeTracker;
42 import org.apache.openjpa.util.Id;
43 import org.apache.openjpa.util.Proxy;
44
45 /**
46 * Base class for strategies that are stored as a collection, even if
47 * their field value is something else. Handles data loading and basic query
48 * functionality. Subclasses must implement abstract methods and
49 * insert/update/delete behavior as well as overriding
50 * {@link FieldStrategy#toDataStoreValue}, {@link FieldStrategy#join}, and
51 * {@link FieldStrategy#joinRelation} if necessary.
52 *
53 * @author Abe White
54 */
55 public abstract class StoreCollectionFieldStrategy
56 extends ContainerFieldStrategy {
57
58 /**
59 * Return the foreign key used to join to the owning field for the given
60 * element mapping from {@link #getIndependentElementMappings} (or null).
61 */
62 protected abstract ForeignKey getJoinForeignKey(ClassMapping elem);
63
64 /**
65 * Implement this method to select the elements of this field for the
66 * given element mapping from {@link #getIndependentElementMappings}
67 * (or null). Elements of the result will be loaded with
68 * {@link #loadElement}.
69 */
70 protected abstract void selectElement(Select sel, ClassMapping elem,
71 JDBCStore store, JDBCFetchConfiguration fetch, int eagerMode,
72 Joins joins);
73
74 /**
75 * Load an element of the collection. The given state manager might be
76 * null if the load is for a projection or for processing eager parallel
77 * results.
78 */
79 protected abstract Object loadElement(OpenJPAStateManager sm,
80 JDBCStore store, JDBCFetchConfiguration fetch, Result res, Joins joins)
81 throws SQLException;
82
83 /**
84 * Join this value's table to the table for the given element mapping
85 * from {@link #getIndependentElementMappings} (or null).
86 *
87 * @see FieldMapping#joinRelation
88 */
89 protected abstract Joins joinElementRelation(Joins joins,
90 ClassMapping elem);
91
92 /**
93 * Join to the owning field table for the given element mapping from
94 * {@link #getIndependentElementMappings} (or null).
95 */
96 protected abstract Joins join(Joins joins, ClassMapping elem);
97
98 /**
99 * Return a large result set proxy for this field.
100 */
101 protected abstract Proxy newLRSProxy();
102
103 /**
104 * Convert the field value to a collection. Handles collections and
105 * arrays by default.
106 */
107 protected Collection toCollection(Object val) {
108 if (field.getTypeCode() == JavaTypes.COLLECTION)
109 return (Collection) val;
110 return JavaTypes.toList(val, field.getElement().getType(), false);
111 }
112
113 /**
114 * Add an item to the data structure representing a field value.
115 * By default, assumes the structure is a collection.
116 */
117 protected void add(JDBCStore store, Object coll, Object obj) {
118 ((Collection) coll).add(obj);
119 }
120
121 /**
122 * Returns the first independent element mapping, or null.
123 */
124 private ClassMapping getDefaultElementMapping(boolean traverse) {
125 ClassMapping[] elems = getIndependentElementMappings(traverse);
126 return (elems.length == 0) ? null : elems[0];
127 }
128
129 public int supportsSelect(Select sel, int type, OpenJPAStateManager sm,
130 JDBCStore store, JDBCFetchConfiguration fetch) {
131 if (field.isLRS())
132 return 0;
133 if (type == Select.EAGER_PARALLEL)
134 return Math.max(1, getIndependentElementMappings(true).length);
135 if (type != Select.EAGER_INNER && type != Select.EAGER_OUTER)
136 return 0;
137 if (getIndependentElementMappings(true).length > 1)
138 return 0;
139 return (type == Select.EAGER_INNER || store.getDBDictionary().
140 canOuterJoin(sel.getJoinSyntax(), getJoinForeignKey
141 (getDefaultElementMapping(false)))) ? 1 : 0;
142 }
143
144 public void selectEagerParallel(SelectExecutor sel,
145 final OpenJPAStateManager sm, final JDBCStore store,
146 final JDBCFetchConfiguration fetch, final int eagerMode) {
147 if (!(sel instanceof Union))
148 selectEager((Select) sel, getDefaultElementMapping(true), sm,
149 store, fetch, eagerMode, true, false);
150 else {
151 final ClassMapping[] elems = getIndependentElementMappings(true);
152 Union union = (Union) sel;
153 if (fetch.getSubclassFetchMode(field.getElementMapping().
154 getTypeMapping()) != fetch.EAGER_JOIN)
155 union.abortUnion();
156 union.select(new Union.Selector() {
157 public void select(Select sel, int idx) {
158 selectEager(sel, elems[idx], sm, store, fetch, eagerMode,
159 true, false);
160 }
161 });
162 }
163 }
164
165 public void selectEagerJoin(Select sel, OpenJPAStateManager sm,
166 JDBCStore store, JDBCFetchConfiguration fetch, int eagerMode) {
167 // we limit further eager fetches to joins, because after this point
168 // the select has been modified such that parallel clones may produce
169 // invalid sql
170 boolean outer = field.getNullValue() != FieldMapping.NULL_EXCEPTION;
171 // force inner join for inner join fetch
172 if (fetch.hasFetchInnerJoin(field.getFullName(false)))
173 outer = false;
174 selectEager(sel, getDefaultElementMapping(true), sm, store, fetch,
175 JDBCFetchConfiguration.EAGER_JOIN, false,
176 outer);
177 }
178
179 public boolean isEagerSelectToMany() {
180 return true;
181 }
182
183 /**
184 * Select our data eagerly.
185 */
186 private void selectEager(Select sel, ClassMapping elem,
187 OpenJPAStateManager sm, JDBCStore store, JDBCFetchConfiguration fetch,
188 int eagerMode, boolean selectOid, boolean outer) {
189 // force distinct if there was a to-many join to avoid dups, but
190 // if this is a parallel select don't make distinct based on the
191 // eager joins alone if the original wasn't distinct
192 if (eagerMode == JDBCFetchConfiguration.EAGER_PARALLEL) {
193 if (sel.hasJoin(true))
194 sel.setDistinct(true);
195 else if (!sel.isDistinct())
196 sel.setDistinct(false); // set explicitly so remembered
197 }
198
199 // set a variable name that does not conflict with any in the query;
200 // using a variable guarantees that the selected data will use different
201 // aliases and joins than any existing WHERE conditions on this field
202 // that might otherwise limit the elements of the field that match
203 if (selectOid)
204 sel.orderByPrimaryKey(field.getDefiningMapping(), true, true);
205 Joins joins = sel.newJoins().setVariable("*");
206 joins = join(joins, elem);
207
208 // order, ref cols
209 if (field.getOrderColumn() != null || field.getOrders().length > 0
210 || !selectOid) {
211 if (outer)
212 joins = sel.outer(joins);
213 if (!selectOid) {
214 Column[] refs = getJoinForeignKey(elem).getColumns();
215 sel.orderBy(refs, true, joins, true);
216 }
217 field.orderLocal(sel, elem, joins);
218 }
219
220 // select data
221 joins = joinElementRelation(joins, elem);
222 if (outer)
223 joins = sel.outer(joins);
224 field.orderRelation(sel, elem, joins);
225 selectElement(sel, elem, store, fetch, eagerMode, joins);
226 }
227
228 public Object loadEagerParallel(OpenJPAStateManager sm, JDBCStore store,
229 JDBCFetchConfiguration fetch, Object res)
230 throws SQLException {
231 // process batched results if we haven't already
232 Map rels;
233 if (res instanceof Result)
234 rels = processEagerParallelResult(sm, store, fetch, (Result) res);
235 else
236 rels = (Map) res;
237
238 // look up the collection for this oid, and store in instance
239 Object coll = rels.remove(sm.getObjectId());
240 if (field.getTypeCode() == JavaTypes.ARRAY)
241 sm.storeObject(field.getIndex(), JavaTypes.toArray
242 ((Collection) coll, field.getElement().getType()));
243 else {
244 if (coll == null)
245 coll = sm.newProxy(field.getIndex());
246 sm.storeObject(field.getIndex(), coll);
247 }
248 return rels;
249 }
250
251 /**
252 * Process the given batched result.
253 */
254 private Map processEagerParallelResult(OpenJPAStateManager sm,
255 JDBCStore store, JDBCFetchConfiguration fetch, Result res)
256 throws SQLException {
257 // do same joins as for load
258 //### cheat: we know typical result joins only care about the relation
259 //### path; thus we can ignore different mappings
260 ClassMapping elem = getDefaultElementMapping(true);
261 Joins dataJoins = res.newJoins().setVariable("*");
262 dataJoins = join(dataJoins, elem);
263 dataJoins = joinElementRelation(dataJoins, elem);
264 Joins orderJoins = null;
265 if (field.getOrderColumn() != null) {
266 orderJoins = res.newJoins().setVariable("*");
267 orderJoins = join(orderJoins, elem);
268 }
269
270 Map rels = new HashMap();
271 ClassMapping ownerMapping = field.getDefiningMapping();
272 Object nextOid, oid = null;
273 Object coll = null;
274 int seq = 0;
275 while (res.next()) {
276 // extract the owner id value
277 nextOid = getNextObjectId(ownerMapping, store, res, oid);
278 if (nextOid != oid) {
279 // if the old coll was an ordered tracking proxy, set
280 // its seq value to the last order val we read
281 if (seq != 0 && coll instanceof Proxy)
282 ((Proxy) coll).getChangeTracker().setNextSequence(seq);
283
284 // start a new collection
285 oid = nextOid;
286 seq = 0;
287 if (field.getTypeCode() == JavaTypes.ARRAY)
288 coll = new ArrayList();
289 else
290 coll = sm.newProxy(field.getIndex());
291 rels.put(oid, coll);
292 }
293
294 if (field.getOrderColumn() != null)
295 seq = res.getInt(field.getOrderColumn(), orderJoins) + 1;
296 add(store, coll, loadElement(null, store, fetch, res, dataJoins));
297 }
298 res.close();
299
300 return rels;
301 }
302
303 /**
304 * Extract the oid value from the given result. If the next oid is the
305 * same as the given one, returns the given JVM instance.
306 */
307 private Object getNextObjectId(ClassMapping owner, JDBCStore store,
308 Result res, Object oid)
309 throws SQLException {
310 // if this is a datastore id class we can avoid creating a new oid
311 // object for the common case
312 if (oid != null && owner.getIdentityType() == ClassMapping.ID_DATASTORE
313 && owner.isPrimaryKeyObjectId(true)) {
314 long nid = res.getLong(owner.getPrimaryKeyColumns()[0]);
315 long id = ((Id) oid).getId();
316 return (nid == id) ? oid : store.newDataStoreId(nid, owner, true);
317 }
318
319 Object noid = owner.getObjectId(store, res, null, true, null);
320 if (noid == null)
321 return null;
322 return (noid.equals(oid)) ? oid : noid;
323 }
324
325 public void loadEagerJoin(OpenJPAStateManager sm, JDBCStore store,
326 JDBCFetchConfiguration fetch, Result res)
327 throws SQLException {
328 // initialize field value
329 Object coll;
330 if (field.getTypeCode() == JavaTypes.ARRAY)
331 coll = new ArrayList();
332 else
333 coll = sm.newProxy(field.getIndex());
334
335 Joins dataJoins = null;
336 Joins refJoins = res.newJoins().setVariable("*");
337 join(refJoins, false);
338
339 ClassMapping ownerMapping = field.getDefiningMapping();
340 Object ref = null;
341 int seq = 0;
342 int typeIdx = res.indexOf();
343 for (int i = 0; true; i++) {
344 // extract the owner id value
345 ref = getNextRef(ownerMapping, store, res, ref, refJoins);
346 if (ref == null) {
347 // if the old coll was an ordered tracking proxy, set
348 // its seq value to the last order val we read
349 if (seq != 0 && coll instanceof Proxy)
350 ((Proxy) coll).getChangeTracker().setNextSequence(seq);
351 if (i != 0)
352 res.pushBack();
353 break;
354 }
355
356 // do same joins as for load
357 if (dataJoins == null) {
358 dataJoins = res.newJoins().setVariable("*");
359 dataJoins = join(dataJoins, false);
360 dataJoins = joinRelation(dataJoins, false, false);
361 }
362
363 if (field.getOrderColumn() != null)
364 seq = res.getInt(field.getOrderColumn(), refJoins) + 1;
365 res.setBaseMapping(null);
366 add(store, coll, loadElement(sm, store, fetch, res, dataJoins));
367 if (!res.next() || res.indexOf() != typeIdx) {
368 res.pushBack();
369 break;
370 }
371 }
372
373 // load the collection into the object
374 if (field.getTypeCode() == JavaTypes.ARRAY)
375 sm.storeObject(field.getIndex(), JavaTypes.toArray
376 ((Collection) coll, field.getElement().getType()));
377 else
378 sm.storeObject(field.getIndex(), coll);
379 }
380
381 /**
382 * Extract the reference column value(s) from the given result. If the
383 * extracted result is the same as the current one or the current
384 * one is null, returns the extracted result. Else returns null.
385 */
386 private Object getNextRef(ClassMapping mapping, JDBCStore store,
387 Result res, Object ref, Joins refJoins)
388 throws SQLException {
389 Column[] cols = getJoinForeignKey(getDefaultElementMapping(false)).
390 getColumns();
391 Object val;
392 if (cols.length == 1) {
393 val = res.getObject(cols[0], null, refJoins);
394 if (val == null || (ref != null && !val.equals(ref)))
395 return null;
396 return val;
397 }
398
399 Object[] refs = (Object[]) ref;
400 if (refs == null)
401 refs = new Object[cols.length];
402 for (int i = 0; i < cols.length; i++) {
403 val = res.getObject(cols[i], null, refJoins);
404 if (val == null)
405 return null;
406 if (refs[i] != null && !val.equals(refs[i]))
407 return null;
408 refs[i] = val;
409 }
410 return refs;
411 }
412
413 public void load(final OpenJPAStateManager sm, final JDBCStore store,
414 final JDBCFetchConfiguration fetch)
415 throws SQLException {
416 if (field.isLRS()) {
417 Proxy coll = newLRSProxy();
418
419 // if this is ordered we need to know the next seq to use in case
420 // objects are added to the collection
421 if (field.getOrderColumn() != null) {
422 // we don't allow ordering table per class one-many's, so
423 // we know we don't need a union
424 Select sel = store.getSQLFactory().newSelect();
425 sel.setAggregate(true);
426 StringBuffer sql = new StringBuffer();
427 sql.append("MAX(").
428 append(sel.getColumnAlias(field.getOrderColumn())).
429 append(")");
430 sel.select(sql.toString(), field);
431 ClassMapping rel = getDefaultElementMapping(false);
432 sel.whereForeignKey(getJoinForeignKey(rel),
433 sm.getObjectId(), field.getDefiningMapping(), store);
434
435 Result res = sel.execute(store, fetch);
436 try {
437 res.next();
438 coll.getChangeTracker().setNextSequence
439 (res.getInt(field) + 1);
440 } finally {
441 res.close();
442 }
443 }
444 sm.storeObjectField(field.getIndex(), coll);
445 return;
446 }
447
448 // select data for this sm
449 final ClassMapping[] elems = getIndependentElementMappings(true);
450 final Joins[] resJoins = new Joins[Math.max(1, elems.length)];
451 Union union = store.getSQLFactory().newUnion
452 (Math.max(1, elems.length));
453 union.select(new Union.Selector() {
454 public void select(Select sel, int idx) {
455 ClassMapping elem = (elems.length == 0) ? null : elems[idx];
456 resJoins[idx] = selectAll(sel, elem, sm, store, fetch,
457 JDBCFetchConfiguration.EAGER_PARALLEL);
458 }
459 });
460
461 // create proxy
462 Object coll;
463 ChangeTracker ct = null;
464 if (field.getTypeCode() == JavaTypes.ARRAY)
465 coll = new ArrayList();
466 else {
467 coll = sm.newProxy(field.getIndex());
468 if (coll instanceof Proxy)
469 ct = ((Proxy) coll).getChangeTracker();
470 }
471
472 // load values
473 Result res = union.execute(store, fetch);
474 try {
475 int seq = -1;
476 while (res.next()) {
477 if (ct != null && field.getOrderColumn() != null)
478 seq = res.getInt(field.getOrderColumn());
479 add(store, coll, loadElement(sm, store, fetch, res,
480 resJoins[res.indexOf()]));
481 }
482 if (ct != null && field.getOrderColumn() != null)
483 ct.setNextSequence(seq + 1);
484 } finally {
485 res.close();
486 }
487
488 // set into sm
489 if (field.getTypeCode() == JavaTypes.ARRAY)
490 sm.storeObject(field.getIndex(), JavaTypes.toArray
491 ((Collection) coll, field.getElement().getType()));
492 else
493 sm.storeObject(field.getIndex(), coll);
494 }
495
496 /**
497 * Select data for loading, starting in field table.
498 */
499 protected Joins selectAll(Select sel, ClassMapping elem,
500 OpenJPAStateManager sm, JDBCStore store, JDBCFetchConfiguration fetch,
501 int eagerMode) {
502 sel.whereForeignKey(getJoinForeignKey(elem), sm.getObjectId(),
503 field.getDefiningMapping(), store);
504
505 // order first, then select so that if the projection introduces
506 // additional ordering, it will be after our required ordering
507 field.orderLocal(sel, elem, null);
508 Joins joins = joinElementRelation(sel.newJoins(), elem);
509 field.orderRelation(sel, elem, joins);
510 selectElement(sel, elem, store, fetch, eagerMode, joins);
511 return joins;
512 }
513
514 public Object loadProjection(JDBCStore store, JDBCFetchConfiguration fetch,
515 Result res, Joins joins)
516 throws SQLException {
517 return loadElement(null, store, fetch, res, joins);
518 }
519
520 protected ForeignKey getJoinForeignKey() {
521 return getJoinForeignKey(getDefaultElementMapping(false));
522 }
523 }