blob: a867249be23d2c17422bec7db5d9b3b63ddc13a6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.jdbc.kernel;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.BitSet;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.jdbc.sql.SelectExecutor;
import org.apache.openjpa.kernel.FetchConfiguration;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.lib.util.Closeable;
import org.apache.openjpa.util.InternalException;
/**
* Object provider implementation that fetches one page of results at a
* a time as it scrolls. If the {@link #getPagedFields} method returns a
* non-null bit set, this this provider is a good fit for your configuration.
* The method tests the following conditions:
* <ul>
* <li>The eager fetch mode is <code>parallel</code>.</li>
* <li>The select's result should be treated as a large result set.</li>
* <li>The mapping being selected has fields that use parallel selects
* under the current fetch configuration.</li>
* </ul>
* To use this provider, select the candidate mapping with eager fetch
* mode set to <code>join</code>. This provider will take care of performing
* <code>parallel</code> mode batch selects for each page it reads.
*
* @author Abe White
*/
public class PagingResultObjectProvider
extends SelectResultObjectProvider {
private final ClassMapping[] _mappings;
private final Object[] _page;
private final int[] _idxs;
private final BitSet[] _paged;
private int _pos = -1; // logical pos
private int _pagePos = -1; // pos of page start
/**
* Return a bit set representing batch select fields that will be paged,
* or null if no fields need paging, which indicates that this provider
* should not be used.
*
* @see #PagingResultObjectProvider
*/
public static BitSet getPagedFields(Select sel, ClassMapping mapping,
JDBCStore store, JDBCFetchConfiguration fetch, int eagerMode,
long size) {
// if we have a range then we always use paging if there are any
// eager select fields; otherwise it depends on lrs and fetch settings
if (size == Long.MAX_VALUE || !sel.getAutoDistinct()) {
// not lrs?
if (!sel.isLRS())
return null;
// not configured for lazy loading?
if (fetch.getFetchBatchSize() < 0)
return null;
}
// not configured for eager selects?
eagerMode = Math.min(eagerMode, fetch.getEagerFetchMode());
if (eagerMode != EagerFetchModes.EAGER_PARALLEL)
return null;
// are there any mappings that require batched selects?
FieldMapping[] fms = mapping.getDefinedFieldMappings();
BitSet paged = null;
for (FieldMapping fm : fms) {
if (fetch.requiresFetch(fm) != FetchConfiguration.FETCH_LOAD)
continue;
if (fm.supportsSelect(sel, Select.EAGER_PARALLEL, null, store,
fetch) > 0 && (fm.isEagerSelectToMany() || fm.
supportsSelect(sel, Select.EAGER_OUTER, null, store, fetch) == 0)) {
if (paged == null)
paged = new BitSet();
paged.set(fm.getIndex());
}
}
return paged;
}
/**
* Constructor.
*
* @param sel the select to execute
* @param mapping the mapping of the result objects
* @param store the store manager to delegate loading to
* @param fetch the fetch configuration, or null for default
* @param paged the bit set returned from {@link #getPagedFields}
* @param size the known maximum size of the result, or
* {@link Long#MAX_VALUE} for no known limit
*/
public PagingResultObjectProvider(SelectExecutor sel,
ClassMapping mapping, JDBCStore store, JDBCFetchConfiguration fetch,
BitSet paged, long size) {
this(sel, new ClassMapping[]{ mapping }, store, fetch,
new BitSet[]{ paged }, size);
}
/**
* Constructor.
*
* @param sel the select to execute
* @param mappings the mappings for the independent classes of the
* result objects
* @param store the store manager to delegate loading to
* @param fetch the fetch configuration, or null for default
* @param paged the bit sets returned from {@link #getPagedFields}
* for each select in the possible union
* @param size the known maximum size of the result, or
* {@link Long#MAX_VALUE} for no known limit
*/
public PagingResultObjectProvider(SelectExecutor sel,
ClassMapping[] mappings, JDBCStore store, JDBCFetchConfiguration fetch,
BitSet[] paged, long size) {
super(sel, store, fetch);
_mappings = mappings;
_paged = paged;
// don't let system construct this type of rop for stupid sizes
if (size <= 1)
throw new InternalException("size=" + size);
// try to find a good page size. if the known size < batch size, use
// it. if the batch size is set, then use that; if it's sorta close
// to the size, then use the size / 2 to get two full pages rather
// than a possible big one and small one
int batch = getFetchConfiguration().getFetchBatchSize();
int pageSize;
if (batch < 0)
pageSize = (int) size;
else {
if (batch == 0)
batch = 50; // reasonable default
if (size <= batch)
pageSize = (int) size;
else if (size <= batch * 2) {
if (size % 2 == 0)
pageSize = (int) (size / 2);
else
pageSize = (int) (size / 2 + 1);
} else
pageSize = batch;
}
_page = new Object[pageSize];
if (_paged.length > 1)
_idxs = new int[pageSize];
else
_idxs = null;
}
/**
* Return the page size in use.
*/
public int getPageSize() {
return _page.length;
}
@Override
public void open()
throws SQLException {
super.open();
_pos = -1;
}
@Override
public boolean next()
throws SQLException {
_pos++;
if (inPage())
return _page[_pos - _pagePos] != null;
if (!super.next()) {
setSize(_pos);
return false;
}
return true;
}
@Override
public boolean absolute(int pos)
throws SQLException {
_pos = pos;
if (inPage())
return _page[_pos - _pagePos] != null;
return super.absolute(pos);
}
@Override
public Object getResultObject()
throws SQLException {
if (!inPage())
fillPage();
return _page[_pos - _pagePos];
}
/**
* Test whether our current position is within the cached page of results.
*/
private boolean inPage() {
return _pagePos != -1 && _pos >= _pagePos
&& _pos < _pagePos + _page.length;
}
/**
* Start a new page at the present position.
*/
private void fillPage()
throws SQLException {
// clear page
Arrays.fill(_page, null);
// cache result objects
JDBCStoreManager storeMgr = (JDBCStoreManager) getStore();
ClassMapping mapping;
Result res;
int idx;
for (int i = 0; i < _page.length; i++) {
res = getResult();
idx = res.indexOf();
if (_idxs != null)
_idxs[i] = idx;
mapping = res.getBaseMapping();
if (mapping == null)
mapping = _mappings[idx];
// rather than use the standard result.load(), we go direct to
// the store manager so we can pass in our eager-fetched fields as
// fields to exclude from the initial load of the objects
_page[i] = storeMgr.load(mapping, getFetchConfiguration(),
_paged[idx], res);
if (i != _page.length - 1 && !getResult().next()) {
setSize(_pos + i + 1);
break;
}
}
// load data for eager fields
_pagePos = _pos;
if (_page[0] != null) {
if (_page.length > 1 && _page[1] == null)
loadEagerFields();
else
executeEagerSelects();
}
}
/**
* When there is only one instance in a page, load fields as normal.
*/
private void loadEagerFields()
throws SQLException {
int idx = (_idxs == null) ? 0 : _idxs[0];
if (_paged[idx] == null)
return;
JDBCStore store = getStore();
OpenJPAStateManager sm = store.getContext().getStateManager(_page[0]);
for (int i = 0, len = _paged[idx].length(); i < len; i++) {
if (_paged[idx].get(i)) {
_mappings[idx].getFieldMapping(i).load(sm, store,
getFetchConfiguration());
}
}
}
/**
* Load eager batch selects for current page of results.
*/
private void executeEagerSelects()
throws SQLException {
if (_idxs == null) {
executeEagerSelects(_mappings[0], _paged[0], 0, _page.length);
return;
}
int start = 0;
int idx = _idxs[0];
int pos = 0;
for (; pos < _page.length && _page[pos] != null; pos++) {
if (idx != _idxs[pos]) {
if (_paged[idx] != null)
executeEagerSelects(_mappings[idx], _paged[idx], start,
pos);
start = pos;
idx = _idxs[pos];
}
}
if (start < pos && _paged[idx] != null) // cleanup remainder
executeEagerSelects(_mappings[idx], _paged[idx], start, pos);
}
/**
* Load eager batch selects for the given mapping and its superclasses.
*/
private void executeEagerSelects(ClassMapping mapping, BitSet paged,
int start, int end)
throws SQLException {
// base case
if (mapping == null)
return;
// recurse on superclass
executeEagerSelects(mapping.getJoinablePCSuperclassMapping(), paged,
start, end);
// figure out how many batch selects to do on this mapping
FieldMapping[] fms = mapping.getDefinedFieldMappings();
int sels = 0;
for (FieldMapping fieldMapping : fms)
if (paged.get(fieldMapping.getIndex()))
sels++;
if (sels == 0)
return;
// create where condition limiting instances to this page
JDBCStore store = getStore();
Select sel = store.getSQLFactory().newSelect();
DBDictionary dict = store.getDBDictionary();
SQLBuffer buf = new SQLBuffer(dict);
Column[] pks = mapping.getPrimaryKeyColumns();
if (pks.length == 1)
createInContains(sel, dict, buf, mapping, pks, start, end);
else
orContains(sel, buf, mapping, pks, start, end);
sel.where(buf);
StoreContext ctx = store.getContext();
JDBCFetchConfiguration fetch = getFetchConfiguration();
// do each batch select
Object res;
int esels = 0;
SelectExecutor esel;
int unions;
for (FieldMapping fm : fms) {
if (!paged.get(fm.getIndex()))
continue;
unions = fm.supportsSelect(sel, Select.EAGER_PARALLEL, null,
store, fetch);
if (unions == 0)
continue;
// we don't need to clone if this is the last select
esels++;
if (esels < sels || unions > 1)
esel = sel.whereClone(unions);
else
esel = sel;
// get result
fm.selectEagerParallel(esel, null, store, fetch,
EagerFetchModes.EAGER_PARALLEL);
res = esel.execute(store, fetch);
try {
// and load result into paged instances
for (int j = start; j < end && _page[j] != null; j++)
res = fm.loadEagerParallel(ctx.getStateManager
(_page[j]), store, fetch, res);
}
finally {
if (res instanceof Closeable)
try {
((Closeable) res).close();
}
catch (Exception e) {
}
}
}
}
/**
* Based on the DBDictionary, create the needed IN clauses.
*/
private void createInContains(Select sel, DBDictionary dict, SQLBuffer buf,
ClassMapping mapping, Column[] pks, int start, int end) {
int inClauseLimit = dict.inClauseLimit;
if (inClauseLimit <= 0 || end - start <= inClauseLimit)
inContains(sel, buf, mapping, pks, start, end);
else {
buf.append("(");
for (int low = start, high; low < end; low = high) {
if (low > start)
buf.append(" OR ");
high = Math.min(low + inClauseLimit, end);
inContains(sel, buf, mapping, pks, low, high);
}
buf.append(")");
}
}
/**
* Create an IN clause limiting the results to the current page.
*/
private void inContains(Select sel, SQLBuffer buf, ClassMapping mapping,
Column[] pks, int start, int end) {
buf.append(sel.getColumnAlias(pks[0])).append(" IN (");
for (int i = start; i < end && _page[i] != null; i++) {
if (i > start)
buf.append(", ");
buf.appendValue(mapping.toDataStoreValue(_page[i], pks,
getStore()), pks[0]);
}
buf.append(")");
}
/**
* Create OR conditions limiting the results to the curent page.
*/
private void orContains(Select sel, SQLBuffer buf, ClassMapping mapping,
Column[] pks, int start, int end) {
String[] aliases = new String[pks.length];
for (int i = 0; i < pks.length; i++)
aliases[i] = sel.getColumnAlias(pks[i]);
Object[] vals;
buf.append("(");
for (int i = start; i < end && _page[i] != null; i++) {
if (i > start)
buf.append(" OR ");
vals = (Object[]) mapping.toDataStoreValue(_page[i], pks,
getStore());
buf.append("(");
for (int j = 0; j < vals.length; j++) {
if (j > 0)
buf.append(" AND ");
buf.append(aliases[j]);
if (vals[j] == null)
buf.append(" IS ");
else
buf.append(" = ");
buf.appendValue(vals[j], pks[j]);
}
buf.append(")");
}
buf.append(")");
}
}