OPENJPA-697: Add new capabilities to support version columns to spread across primary and secondary tables

git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@690346 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Pinaki Poddar 2008-08-29 17:46:15 +00:00
parent 991f5a7dce
commit aa1516f04b
10 changed files with 368 additions and 37 deletions

View File

@ -95,6 +95,21 @@ public abstract class MappingInfo
public List getColumns() {
return (_cols == null) ? Collections.EMPTY_LIST : _cols;
}
/**
* Gets the columns whose table name matches the given table name.
*/
public List getColumns(String tableName) {
if (_cols == null)
return Collections.EMPTY_LIST;
List result = new ArrayList();
for (Object col : _cols) {
if (StringUtils.equals(((Column)col).getTableName(),
tableName))
result.add(col);
}
return result;
}
/**
* Raw column data.
@ -531,10 +546,19 @@ public abstract class MappingInfo
boolean fill = ((MappingRepository) context.getRepository()).
getMappingDefaults().defaultMissingInfo();
if ((!given.isEmpty() || (!adapt && !fill))
&& given.size() != tmplates.length)
throw new MetaDataException(_loc.get(prefix + "-num-cols",
context, String.valueOf(tmplates.length),
String.valueOf(given.size())));
&& given.size() != tmplates.length) {
// also consider when this info has columns from multiple tables
given = getColumns(table.getName());
if ((!adapt && !fill) && given.size() != tmplates.length) {
// try default table
given = getColumns("");
if ((!adapt && !fill) && given.size() != tmplates.length) {
throw new MetaDataException(_loc.get(prefix + "-num-cols",
context, String.valueOf(tmplates.length),
String.valueOf(given.size())));
}
}
}
Column[] cols = new Column[tmplates.length];
_io = null;
@ -547,6 +571,11 @@ public abstract class MappingInfo
}
return cols;
}
boolean canMerge(List given, Column[] templates, boolean adapt, boolean fill) {
return !((!given.isEmpty() || (!adapt && !fill))
&& given.size() != templates.length);
}
/**
* Set the proper internal column I/O metadata for the given column's flags.

View File

@ -18,12 +18,23 @@
*/
package org.apache.openjpa.jdbc.meta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.openjpa.jdbc.meta.strats.NoneVersionStrategy;
import org.apache.openjpa.jdbc.meta.strats.SuperclassVersionStrategy;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.Index;
import org.apache.openjpa.jdbc.schema.SchemaGroup;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.UserException;
/**
* Information about the mapping from a version indicator to the schema, in
@ -36,17 +47,59 @@ import org.apache.openjpa.jdbc.schema.Table;
public class VersionMappingInfo
extends MappingInfo {
private static final Localizer _loc = Localizer.forPackage
(VersionMappingInfo.class);
/**
* Return the columns set for this version, based on the given templates.
*/
public Column[] getColumns(Version version, Column[] tmplates,
public Column[] getColumns(Version version, Column[] templates,
boolean adapt) {
Table table = version.getClassMapping().getTable();
if (spansMultipleTables(templates))
return getMultiTableColumns(version, templates, adapt);
Table table = getSingleTable(version, templates);
version.getMappingRepository().getMappingDefaults().populateColumns
(version, table, tmplates);
return createColumns(version, null, tmplates, table, adapt);
(version, table, templates);
return createColumns(version, null, templates, table, adapt);
}
/**
* Return the columns set for this version when the columns are spread
* across multiple tables.
*/
public Column[] getMultiTableColumns(Version vers, Column[] templates,
boolean adapt) {
Table primaryTable = vers.getClassMapping().getTable();
List<String> secondaryTableNames = Arrays.asList(vers
.getClassMapping().getMappingInfo().getSecondaryTableNames());
Map<Table, List<Column>> assign = new HashMap<Table, List<Column>>();
for (Column col : templates) {
String tableName = col.getTableName();
Table table;
if (StringUtils.isEmpty(tableName)
|| tableName.equals(primaryTable.getName())) {
table = primaryTable;
} else if (secondaryTableNames.contains(tableName)) {
table = primaryTable.getSchema().getTable(tableName);
} else {
throw new UserException(_loc.get("bad-version-column-table",
col.getName(), tableName));
}
if (!assign.containsKey(table))
assign.put(table, new ArrayList<Column>());
assign.get(table).add(col);
}
MappingDefaults def = vers.getMappingRepository().getMappingDefaults();
List<Column> result = new ArrayList<Column>();
for (Table table : assign.keySet()) {
List<Column> cols = assign.get(table);
Column[] partTemplates = cols.toArray(new Column[cols.size()]);
def.populateColumns(vers, table, partTemplates);
result.addAll(Arrays.asList(createColumns(vers, null, partTemplates,
table, adapt)));
}
return result.toArray(new Column[result.size()]);
}
/**
* Return the index to set on the version columns, or null if none.
*/
@ -86,4 +139,30 @@ public class VersionMappingInfo
&& cls.getJoinablePCSuperclassMapping() == null))
setStrategy(strat);
}
/**
* Affirms if the given columns belong to more than one tables.
*/
boolean spansMultipleTables(Column[] cols) {
if (cols == null || cols.length <= 1)
return false;
Set<String> tables = new HashSet<String>();
for (Column col : cols)
if (tables.add(col.getTableName()) && tables.size() > 1)
return true;
return false;
}
/**
* Gets the table where this version columns are mapped.
*/
private Table getSingleTable(Version version, Column[] cols) {
if (cols == null || cols.length == 0
|| StringUtils.isEmpty(cols[0].getTableName()))
return version.getClassMapping().getTable();
return version.getClassMapping().getTable().getSchema()
.getTable(cols[0].getTableName());
}
}

View File

@ -31,6 +31,7 @@ import org.apache.openjpa.jdbc.meta.VersionMappingInfo;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ColumnIO;
import org.apache.openjpa.jdbc.schema.Index;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.jdbc.sql.RowManager;
@ -48,6 +49,7 @@ import serp.util.Numbers;
* Uses a single column and corresponding version object.
*
* @author Marc Prud'hommeaux
* @author Pinaki Poddar
*/
public abstract class ColumnVersionStrategy
extends AbstractVersionStrategy {
@ -110,19 +112,23 @@ public abstract class ColumnVersionStrategy
/**
* Compare each element of the given arrays that must be of equal size.
*
* @return If each element comparison results into same sign then returns
* that sign. If some elements compare equal and all the rest has the same
* sign then return that sign. Otherwise, return 1.
* @return If any element of a1 is later than corresponding element of
* a2 then return 1 i.e. a1 as a whole is later than a2.
* If each element of a1 is to equal corresponding element of a2 then return
* 0 i.e. a1 is as a whole equals to a2.
* else return a negative number i.e. a1 is earlier than a2.
*/
protected int compare(Object[] a1, Object[] a2) {
if (a1.length != a2.length)
throw new InternalException();
Set<Integer> comps = new HashSet<Integer>();
for (int i = 0; i < a1.length; i++)
comps.add(sign(compare(a1[i], a2[i])));
if (comps.size() == 1 || (comps.size() == 2 && comps.remove(0)))
return comps.iterator().next();
return 1;
int total = 0;
for (int i = 0; i < a1.length; i++) {
int c = compare(a1[i], a2[i]);
if (c > 0)
return 1;
total += c;
}
return total;
}
int sign(int i) {
@ -144,11 +150,12 @@ public abstract class ColumnVersionStrategy
for (int i = 0; i < info.getColumns().size(); i++) {
templates[i] = new Column();
Column infoColumn = (Column)info.getColumns().get(i);
templates[i].setTableName(infoColumn.getTableName());
templates[i].setType(infoColumn.getType());
templates[i].setSize(infoColumn.getSize());
templates[i].setDecimalDigits(infoColumn.getDecimalDigits());
templates[i].setJavaType(getJavaType(i));
templates[i].setName("versn" +i);
templates[i].setName(infoColumn.getName());
}
Column[] cols = info.getColumns(vers, templates, adapt);
for (int i = 0; i < cols.length; i++)
@ -175,12 +182,11 @@ public abstract class ColumnVersionStrategy
Column[] cols = vers.getColumns();
ColumnIO io = vers.getColumnIO();
Object initial = nextVersion(null);
Row row = rm.getRow(vers.getClassMapping().getTable(),
Row.ACTION_INSERT, sm, true);
for (int i = 0; i < cols.length; i++)
for (int i = 0; i < cols.length; i++) {
Row row = rm.getRow(cols[i].getTable(), Row.ACTION_INSERT, sm, true);
if (io.isInsertable(i, initial == null))
row.setObject(cols[i], getColumnValue(initial, i));
}
// set initial version into state manager
Object nextVersion;
nextVersion = initial;
@ -197,12 +203,11 @@ public abstract class ColumnVersionStrategy
Object curVersion = sm.getVersion();
Object nextVersion = nextVersion(curVersion);
Row row = rm.getRow(vers.getClassMapping().getTable(),
Row.ACTION_UPDATE, sm, true);
row.setFailedObject(sm.getManagedInstance());
// set where and update conditions on row
for (int i = 0; i < cols.length; i++) {
Row row = rm.getRow(cols[i].getTable(), Row.ACTION_UPDATE, sm, true);
row.setFailedObject(sm.getManagedInstance());
if (curVersion != null && sm.isVersionCheckRequired())
row.whereObject(cols[i], getColumnValue(curVersion, i));
if (vers.getColumnIO().isUpdatable(i, nextVersion == null))
@ -215,14 +220,14 @@ public abstract class ColumnVersionStrategy
public void delete(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
throws SQLException {
Row row = rm.getRow(vers.getClassMapping().getTable(),
Row.ACTION_DELETE, sm, true);
row.setFailedObject(sm.getManagedInstance());
Column[] cols = vers.getColumns();
Object curVersion = sm.getVersion();
Object cur;
for (int i = 0; i < cols.length; i++) {
Row row = rm.getRow(cols[i].getTable(),
Row.ACTION_DELETE, sm, true);
row.setFailedObject(sm.getManagedInstance());
cur = getColumnValue(curVersion, i);
// set where and update conditions on row
if (cur != null)

View File

@ -414,4 +414,6 @@ unique-missing-column: The column "{1}" in a unique constraint in "{0}" on \
table "{2}" can not be found in the list of available columns "{3}".
unique-no-table: A unique constraint on table "{0}" can not be added to \
mapping of class "{1}" because the table does neither match its primary \
table "{2}" nor any of its secondary table(s) "{3}".
table "{2}" nor any of its secondary table(s) "{3}".
bad-version-column-table: One of the version column "{0}" has been associated \
with table "{1}", but no primary or secondary table of such name exists.

View File

@ -807,6 +807,7 @@ public class AnnotationPersistenceMappingParser
*/
private static Column newColumn(VersionColumn anno) {
Column col = new Column();
col.setTableName(anno.table());
if (!StringUtils.isEmpty(anno.name()))
col.setName(anno.name());
if (anno.precision() != 0)

View File

@ -49,4 +49,6 @@ public @interface VersionColumn {
int precision() default 0; // decimal precision
int scale() default 0; // decimal scale
String table() default "";
}

View File

@ -0,0 +1,95 @@
/*
* 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.persistence.jdbc.annotations;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.SecondaryTable;
import javax.persistence.SecondaryTables;
import javax.persistence.Table;
import org.apache.openjpa.persistence.jdbc.VersionColumn;
import org.apache.openjpa.persistence.jdbc.VersionColumns;
import org.apache.openjpa.persistence.jdbc.VersionStrategy;
/**
* Persistent entity for testing multiple column numeric version strategy as set
* by <code>@VersionColumns</code> annotations and where the version columns are
* spread over primary and secondary table(s).
*
* @author Pinaki Poddar
*
*/
@Entity
@Table(name="MCSV")
@SecondaryTables({
@SecondaryTable(name = "MCSV1", pkJoinColumns=@PrimaryKeyJoinColumn(name="ID")),
@SecondaryTable(name = "MCSV2", pkJoinColumns=@PrimaryKeyJoinColumn(name="ID"))
})
@VersionStrategy("version-numbers")
@VersionColumns({
@VersionColumn(name = "v11", table="MCSV1"),
@VersionColumn(name = "v12", table="MCSV1"),
@VersionColumn(name = "v21", table="MCSV2"),
@VersionColumn(name = "v01") // default is the primary table
})
public class MultiColumnSecondaryVersionPC {
@Id
@GeneratedValue
private long id;
private String name;
@Column(table="MCSV1")
private String s1;
@Column(table="MCSV2")
private String s2;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getS1() {
return s1;
}
public void setS1(String s1) {
this.s1 = s1;
}
public String getS2() {
return s2;
}
public void setS2(String s2) {
this.s2 = s2;
}
}

View File

@ -21,6 +21,7 @@ package org.apache.openjpa.persistence.jdbc.annotations;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import org.apache.openjpa.persistence.jdbc.VersionColumn;
import org.apache.openjpa.persistence.jdbc.VersionColumns;
@ -36,6 +37,7 @@ import org.apache.openjpa.persistence.jdbc.VersionStrategy;
*
*/
@Entity
@Table(name="MCV")
@VersionStrategy("version-numbers")
@VersionColumns({
@VersionColumn(name="v1"),

View File

@ -19,7 +19,6 @@
package org.apache.openjpa.persistence.jdbc.annotations;
import java.lang.reflect.Array;
import java.util.Arrays;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.strats.MultiColumnVersionStrategy;
@ -27,23 +26,30 @@ import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;
/**
* Tests numeric version spanning multiple columns.
* Tests numeric version spanning multiple columns and those columns spanning
* multiple tables.
*
* @author Pinaki Poddar
*/
public class TestMultiColumnVersion extends SingleEMFTestCase {
public void setUp() {
setUp(MultiColumnVersionPC.class, CLEAR_TABLES);
setUp(MultiColumnVersionPC.class, MultiColumnSecondaryVersionPC.class,
CLEAR_TABLES);
}
public void testVersionStrategyIsSet() {
ClassMapping mapping = getMapping(MultiColumnVersionPC.class);
assertStrategy(MultiColumnVersionPC.class);
assertStrategy(MultiColumnSecondaryVersionPC.class);
}
public void assertStrategy(Class cls) {
ClassMapping mapping = getMapping(cls);
assertNotNull(mapping.getVersion());
assertTrue(mapping.getVersion().getStrategy()
instanceof MultiColumnVersionStrategy);
}
public void testVersionOnPersistAndUpdate() {
public void testVersionOnPersistAndUpdateForSingleTable() {
OpenJPAEntityManager em = emf.createEntityManager();
em.getTransaction().begin();
MultiColumnVersionPC pc = new MultiColumnVersionPC();
@ -59,7 +65,7 @@ public class TestMultiColumnVersion extends SingleEMFTestCase {
assertVersionEquals(new Number[]{2,2, 2.0f}, em.getVersion(pc));
}
public void testConcurrentOptimisticUpdateFails() {
public void testConcurrentOptimisticUpdateFailsForSingleTable() {
OpenJPAEntityManager em1 = emf.createEntityManager();
em1.getTransaction().begin();
OpenJPAEntityManager em2 = emf.createEntityManager();
@ -88,7 +94,7 @@ public class TestMultiColumnVersion extends SingleEMFTestCase {
}
}
public void testConcurrentOptimisticReadSucceeds() {
public void testConcurrentOptimisticReadSucceedsForSingleTable() {
OpenJPAEntityManager em1 = emf.createEntityManager();
em1.getTransaction().begin();
OpenJPAEntityManager em2 = emf.createEntityManager();
@ -108,6 +114,71 @@ public class TestMultiColumnVersion extends SingleEMFTestCase {
em2.getTransaction().commit();
}
public void testVersionOnPersistAndUpdateForMultiTable() {
OpenJPAEntityManager em = emf.createEntityManager();
em.getTransaction().begin();
MultiColumnSecondaryVersionPC pc = new MultiColumnSecondaryVersionPC();
assertEquals(null, em.getVersion(pc));
em.persist(pc);
em.getTransaction().commit();
assertVersionEquals(new Number[]{1,1,1,1}, em.getVersion(pc));
em.getTransaction().begin();
pc.setName("updated");
em.merge(pc);
em.getTransaction().commit();
assertVersionEquals(new Number[]{2,2,2,2}, em.getVersion(pc));
}
public void testConcurrentOptimisticUpdateFailsForMultiTable() {
OpenJPAEntityManager em1 = emf.createEntityManager();
em1.getTransaction().begin();
OpenJPAEntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
MultiColumnSecondaryVersionPC pc1 = new MultiColumnSecondaryVersionPC();
em1.persist(pc1);
em1.getTransaction().commit();
em1.getTransaction().begin();
Object oid = em1.getObjectId(pc1);
MultiColumnSecondaryVersionPC pc2 = em2.find(MultiColumnSecondaryVersionPC.class, oid);
assertVersionEquals(em1.getVersion(pc1), em2.getVersion(pc2));
pc1.setName("Updated in em1");
pc2.setName("Updated in em2");
em1.getTransaction().commit();
try {
em2.getTransaction().commit();
fail("Optimistic fail");
} catch (Exception e) {
} finally {
em2.close();
}
}
public void testConcurrentOptimisticReadSucceedsForMultiTable() {
OpenJPAEntityManager em1 = emf.createEntityManager();
em1.getTransaction().begin();
OpenJPAEntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
MultiColumnSecondaryVersionPC pc1 = new MultiColumnSecondaryVersionPC();
em1.persist(pc1);
em1.getTransaction().commit();
em1.getTransaction().begin();
Object oid = em1.getObjectId(pc1);
MultiColumnSecondaryVersionPC pc2 = em2.find(MultiColumnSecondaryVersionPC.class, oid);
assertVersionEquals(em1.getVersion(pc1), em2.getVersion(pc2));
em1.getTransaction().commit();
em2.getTransaction().commit();
}
static void assertVersionEquals(Object expected, Object actual) {
assertTrue(expected.getClass().isArray());
assertTrue(actual.getClass().isArray());

View File

@ -1629,6 +1629,11 @@ values. Each <classname>VersionColumn</classname> has the following properties:
</listitem>
<listitem>
<para>
<literal>String table</literal>
</para>
</listitem>
<listitem>
<para>
<literal>int length</literal>
</para>
</listitem>
@ -1674,6 +1679,46 @@ strategy. You can choose a different strategy with the <classname>
VersionStrategy</classname> annotation described in
<xref linkend="version-strategy"/>.
</para>
<para>
If multiple columns are used for surrogate versioning, then each column,
by default, uses a version number. But column definition for each version
column can be set independently to other numeric types. The version values are
compared to detect optimistic concurrent modification. Such comparison must
determine whether a version value <literal>v1</literal> represents an earlier,
later or same with respect to another version value <literal>v2</literal>. While
result of such comparison is obvious for a single numeric column that
monotonically increases on each update, the same is not true when version value
is an array of numbers. By default, OpenJPA compares a version
<literal>v1</literal> as later than another version <literal>v2</literal>,
if any array element of <literal>v1</literal> is
later than the corresponding element of <literal>v2</literal>.
<literal>v1</literal> is equal to <literal>v2</literal> if every array element
is equal and <literal>v1</literal> is earlier to <literal>v1</literal> if some
elements of <literal>v1</literal> are earlier and rest are equal to corresponding
element of <literal>v2</literal>.
</para>
<para>
Multiple surrogate version columns can be spread across primary and secondary
tables. For example, following example shows 3 version columns
<literal>v01, v11, v12, v21</literal> defined across the primary and secondary tables of
a persistent entity
</para>
<programlisting>
@Entity
@Table(name="PRIMARY")
@SecondaryTables({
@SecondaryTable(name = "SECONDARY_1"),
@SecondaryTable(name = "SECONDARY_2")
})
@VersionStrategy("version-numbers")
@VersionColumns({
@VersionColumn(name = "v01") // default is the PRIMARY table
@VersionColumn(name = "v11", table="SECONDARY_1", columnDefinition="FLOAT", scale=3, precision=10),
@VersionColumn(name = "v12", table="SECONDARY_1"),
@VersionColumn(name = "v21", table="SECONDARY_2"),
})
</programlisting>
</section>
<section id="ref_guide_mapping_jpa_columns">
<title>