[#1703] Add Executor.batchDelete(UpdatableRecord<?>...) to mass-delete a

set of UpdatableRecords
This commit is contained in:
Lukas Eder 2012-12-20 16:29:06 +01:00
parent 3fe9e89293
commit b7016bd581
6 changed files with 337 additions and 142 deletions

View File

@ -0,0 +1,233 @@
/**
* Copyright (c) 2009-2012, Lukas Eder, lukas.eder@gmail.com
* All rights reserved.
*
* This software is licensed to you under the Apache License, Version 2.0
* (the "License"); You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* . Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* . Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* . Neither the name "jOOQ" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.jooq.test._.testcases;
import static junit.framework.Assert.assertEquals;
import java.sql.Connection;
import java.sql.Date;
import java.util.Arrays;
import org.jooq.Batch;
import org.jooq.ExecuteContext;
import org.jooq.Record1;
import org.jooq.Record2;
import org.jooq.Record3;
import org.jooq.Record6;
import org.jooq.Result;
import org.jooq.TableRecord;
import org.jooq.UpdatableRecord;
import org.jooq.impl.DefaultConnectionProvider;
import org.jooq.impl.DefaultExecuteListener;
import org.jooq.test.BaseTest;
import org.jooq.test.jOOQAbstractTest;
import org.junit.Test;
public class BatchTests<
A extends UpdatableRecord<A> & Record6<Integer, String, String, Date, Integer, ?>,
AP,
B extends UpdatableRecord<B>,
S extends UpdatableRecord<S> & Record1<String>,
B2S extends UpdatableRecord<B2S> & Record3<String, Integer, Integer>,
BS extends UpdatableRecord<BS>,
L extends TableRecord<L> & Record2<String, String>,
X extends TableRecord<X>,
DATE extends UpdatableRecord<DATE>,
BOOL extends UpdatableRecord<BOOL>,
D extends UpdatableRecord<D>,
T extends UpdatableRecord<T>,
U extends TableRecord<U>,
I extends TableRecord<I>,
IPK extends UpdatableRecord<IPK>,
T725 extends UpdatableRecord<T725>,
T639 extends UpdatableRecord<T639>,
T785 extends TableRecord<T785>>
extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T725, T639, T785> {
public BatchTests(jOOQAbstractTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T725, T639, T785> delegate) {
super(delegate);
}
public static class ConnectionProviderListener extends DefaultExecuteListener {
static Connection c;
@Override
public void start(ExecuteContext ctx) {
ctx.connectionProvider(new DefaultConnectionProvider(c));
}
}
@Test
public void testBatchSingle() throws Exception {
jOOQAbstractTest.reset = false;
// [#1749] TODO Firebird renders CAST(? as VARCHAR(...)) bind values with sizes
// pre-calculated. Hence the param needs to have some min length...
Batch batch = create().batch(create().insertInto(TAuthor())
.set(TAuthor_ID(), 8)
.set(TAuthor_LAST_NAME(), " "))
.bind(8, "Gamma")
.bind(9, "Helm")
.bind(10, "Johnson");
assertEquals(3, batch.size());
int[] result = batch.execute();
assertEquals(3, result.length);
testBatchAuthors("Gamma", "Helm", "Johnson");
}
@Test
public void testBatchMultiple() throws Exception {
jOOQAbstractTest.reset = false;
Batch batch = create().batch(
create().insertInto(TAuthor())
.set(TAuthor_ID(), 8)
.set(TAuthor_LAST_NAME(), "Gamma"),
create().insertInto(TAuthor())
.set(TAuthor_ID(), 9)
.set(TAuthor_LAST_NAME(), "Helm"),
create().insertInto(TBook())
.set(TBook_ID(), 6)
.set(TBook_AUTHOR_ID(), 8)
.set(TBook_PUBLISHED_IN(), 1994)
.set(TBook_LANGUAGE_ID(), 1)
.set(TBook_CONTENT_TEXT(), "Design Patterns are awesome")
.set(TBook_TITLE(), "Design Patterns"),
create().insertInto(TAuthor())
.set(TAuthor_ID(), 10)
.set(TAuthor_LAST_NAME(), "Johnson"));
assertEquals(4, batch.size());
int[] result = batch.execute();
assertEquals(4, result.length);
assertEquals(5, create().fetch(TBook()).size());
assertEquals(1, create().fetch(TBook(), TBook_AUTHOR_ID().equal(8)).size());
testBatchAuthors("Gamma", "Helm", "Johnson");
}
@Test
public void testBatchStore() throws Exception {
jOOQAbstractTest.reset = false;
// First, INSERT two authors and one book
// --------------------------------------
A a1 = create().newRecord(TAuthor());
a1.setValue(TAuthor_ID(), 8);
a1.setValue(TAuthor_LAST_NAME(), "XX");
A a2 = create().newRecord(TAuthor());
a2.setValue(TAuthor_ID(), 9);
a2.setValue(TAuthor_LAST_NAME(), "YY");
B b1 = create().newRecord(TBook());
b1.setValue(TBook_ID(), 80);
b1.setValue(TBook_AUTHOR_ID(), 8);
b1.setValue(TBook_TITLE(), "XX 1");
b1.setValue(TBook_PUBLISHED_IN(), 2000);
b1.setValue(TBook_LANGUAGE_ID(), 1);
Batch batch = create().batchStore(a1, b1, a2);
assertEquals(3, batch.size());
int[] result1 = batch.execute();
assertEquals(3, result1.length);
testBatchAuthors("XX", "YY");
assertEquals("XX 1", create()
.select(TBook_TITLE())
.from(TBook())
.where(TBook_ID().equal(80))
.fetchOne(0));
// Then, update one author and insert another one
// ----------------------------------------------
a2.setValue(TAuthor_LAST_NAME(), "ABC");
A a3 = create().newRecord(TAuthor());
a3.setValue(TAuthor_ID(), 10);
a3.setValue(TAuthor_LAST_NAME(), "ZZ");
int[] result2 = create().batchStore(b1, a1, a2, a3).execute();
assertEquals(2, result2.length);
testBatchAuthors("XX", "ABC", "ZZ");
assertEquals("XX 1", create()
.select(TBook_TITLE())
.from(TBook())
.where(TBook_ID().equal(80))
.fetchOne(0));
}
@Test
public void testBatchDelete() throws Exception {
jOOQAbstractTest.reset = false;
Result<B> books = create().selectFrom(TBook()).where(TBook_ID().in(1, 3, 4)).fetch();
Batch batch = create().batchDelete(books);
assertEquals(3, batch.size());
int[] result = batch.execute();
assertEquals(3, result.length);
assertEquals(1, create().selectFrom(TBook()).fetch().size());
}
private void testBatchAuthors(String... names) throws Exception {
assertEquals(names.length == 3 ? 5 : 4, create().fetch(TAuthor()).size());
assertEquals(
names.length == 3
? Arrays.asList(8, 9, 10)
: Arrays.asList(8, 9),
create().select(TAuthor_ID())
.from(TAuthor())
.where(TAuthor_ID().in(8, 9, 10))
.orderBy(TAuthor_ID())
.fetch(TAuthor_ID()));
assertEquals(Arrays.asList(names),
create().select(TAuthor_LAST_NAME())
.from(TAuthor())
.where(TAuthor_ID().in(8, 9, 10))
.orderBy(TAuthor_ID())
.fetch(TAuthor_LAST_NAME()));
}
}

View File

@ -419,130 +419,4 @@ extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T725,
assertFalse(create().select(TBook_ID(), TBook_TITLE()).from(TBook()).fetch().equals(
create().select(TBook_TITLE(), TBook_ID()).from(TBook()).fetch()));
}
@Test
public void testBatchSingle() throws Exception {
jOOQAbstractTest.reset = false;
// [#1749] TODO Firebird renders CAST(? as VARCHAR(...)) bind values with sizes
// pre-calculated. Hence the param needs to have some min length...
Batch batch = create().batch(create().insertInto(TAuthor())
.set(TAuthor_ID(), 8)
.set(TAuthor_LAST_NAME(), " "))
.bind(8, "Gamma")
.bind(9, "Helm")
.bind(10, "Johnson");
assertEquals(3, batch.size());
int[] result = batch.execute();
assertEquals(3, result.length);
testBatchAuthors("Gamma", "Helm", "Johnson");
}
@Test
public void testBatchMultiple() throws Exception {
jOOQAbstractTest.reset = false;
Batch batch = create().batch(
create().insertInto(TAuthor())
.set(TAuthor_ID(), 8)
.set(TAuthor_LAST_NAME(), "Gamma"),
create().insertInto(TAuthor())
.set(TAuthor_ID(), 9)
.set(TAuthor_LAST_NAME(), "Helm"),
create().insertInto(TBook())
.set(TBook_ID(), 6)
.set(TBook_AUTHOR_ID(), 8)
.set(TBook_PUBLISHED_IN(), 1994)
.set(TBook_LANGUAGE_ID(), 1)
.set(TBook_CONTENT_TEXT(), "Design Patterns are awesome")
.set(TBook_TITLE(), "Design Patterns"),
create().insertInto(TAuthor())
.set(TAuthor_ID(), 10)
.set(TAuthor_LAST_NAME(), "Johnson"));
assertEquals(4, batch.size());
int[] result = batch.execute();
assertEquals(4, result.length);
assertEquals(5, create().fetch(TBook()).size());
assertEquals(1, create().fetch(TBook(), TBook_AUTHOR_ID().equal(8)).size());
testBatchAuthors("Gamma", "Helm", "Johnson");
}
@Test
public void testBatchStore() throws Exception {
jOOQAbstractTest.reset = false;
// First, INSERT two authors and one book
// --------------------------------------
A a1 = create().newRecord(TAuthor());
a1.setValue(TAuthor_ID(), 8);
a1.setValue(TAuthor_LAST_NAME(), "XX");
A a2 = create().newRecord(TAuthor());
a2.setValue(TAuthor_ID(), 9);
a2.setValue(TAuthor_LAST_NAME(), "YY");
B b1 = create().newRecord(TBook());
b1.setValue(TBook_ID(), 80);
b1.setValue(TBook_AUTHOR_ID(), 8);
b1.setValue(TBook_TITLE(), "XX 1");
b1.setValue(TBook_PUBLISHED_IN(), 2000);
b1.setValue(TBook_LANGUAGE_ID(), 1);
Batch batch = create().batchStore(a1, b1, a2);
assertEquals(3, batch.size());
int[] result1 = batch.execute();
assertEquals(3, result1.length);
testBatchAuthors("XX", "YY");
assertEquals("XX 1", create()
.select(TBook_TITLE())
.from(TBook())
.where(TBook_ID().equal(80))
.fetchOne(0));
// Then, update one author and insert another one
// ----------------------------------------------
a2.setValue(TAuthor_LAST_NAME(), "ABC");
A a3 = create().newRecord(TAuthor());
a3.setValue(TAuthor_ID(), 10);
a3.setValue(TAuthor_LAST_NAME(), "ZZ");
int[] result2 = create().batchStore(b1, a1, a2, a3).execute();
assertEquals(2, result2.length);
testBatchAuthors("XX", "ABC", "ZZ");
assertEquals("XX 1", create()
.select(TBook_TITLE())
.from(TBook())
.where(TBook_ID().equal(80))
.fetchOne(0));
}
private void testBatchAuthors(String... names) throws Exception {
assertEquals(names.length == 3 ? 5 : 4, create().fetch(TAuthor()).size());
assertEquals(
names.length == 3
? Arrays.asList(8, 9, 10)
: Arrays.asList(8, 9),
create().select(TAuthor_ID())
.from(TAuthor())
.where(TAuthor_ID().in(8, 9, 10))
.orderBy(TAuthor_ID())
.fetch(TAuthor_ID()));
assertEquals(Arrays.asList(names),
create().select(TAuthor_LAST_NAME())
.from(TAuthor())
.where(TAuthor_ID().in(8, 9, 10))
.orderBy(TAuthor_ID())
.fetch(TAuthor_LAST_NAME()));
}
}

View File

@ -96,6 +96,7 @@ import org.jooq.test._.converters.Boolean_YES_NO_UC;
import org.jooq.test._.converters.Boolean_YN_LC;
import org.jooq.test._.converters.Boolean_YN_UC;
import org.jooq.test._.testcases.AggregateWindowFunctionTests;
import org.jooq.test._.testcases.BatchTests;
import org.jooq.test._.testcases.BenchmarkTests;
import org.jooq.test._.testcases.CRUDTests;
import org.jooq.test._.testcases.DaoTests;
@ -1726,17 +1727,22 @@ public abstract class jOOQAbstractTest<
@Test
public void testBatchSingle() throws Exception {
new GeneralTests(this).testBatchSingle();
new BatchTests(this).testBatchSingle();
}
@Test
public void testBatchMultiple() throws Exception {
new GeneralTests(this).testBatchMultiple();
new BatchTests(this).testBatchMultiple();
}
@Test
public void testBatchStore() throws Exception {
new GeneralTests(this).testBatchStore();
new BatchTests(this).testBatchStore();
}
@Test
public void testBatchDelete() throws Exception {
new BatchTests(this).testBatchDelete();
}
@Test

View File

@ -58,7 +58,7 @@ import org.jooq.exception.DataAccessException;
/**
* @author Lukas Eder
*/
class BatchStore implements Batch {
class BatchCRUD implements Batch {
/**
* Generated UID
@ -67,9 +67,11 @@ class BatchStore implements Batch {
private final Executor create;
private final UpdatableRecord<?>[] records;
private final Action action;
BatchStore(Executor create, UpdatableRecord<?>[] records) {
BatchCRUD(Executor create, Action action, UpdatableRecord<?>[] records) {
this.create = create;
this.action = action;
this.records = records;
}
@ -112,7 +114,7 @@ class BatchStore implements Batch {
try {
records[i].attach(create);
records[i].store();
executeAction(i);
}
catch (QueryCollectorException e) {
Query query = e.getQuery();
@ -166,7 +168,7 @@ class BatchStore implements Batch {
array[i] = result.get(i);
}
setAllUnchanged();
updateChangedFlag();
return array;
}
@ -184,7 +186,7 @@ class BatchStore implements Batch {
try {
records[i].attach(create);
records[i].store();
executeAction(i);
}
catch (QueryCollectorException e) {
Query query = e.getQuery();
@ -206,18 +208,47 @@ class BatchStore implements Batch {
// Resulting statements can be batch executed in their requested order
int[] result = create.batch(queries).execute();
setAllUnchanged();
updateChangedFlag();
return result;
}
private final void setAllUnchanged() {
private void executeAction(int i) {
if (action == Action.STORE) {
records[i].store();
}
else if (action == Action.DELETE) {
records[i].delete();
}
}
private final void updateChangedFlag() {
// 1. Deleted records should be marked as changed, such that subsequent
// calls to store() will insert them again
// 2. Stored records should be marked as unchanged
for (UpdatableRecord<?> record : records) {
if (record instanceof AbstractRecord) {
((AbstractRecord) record).setAllChanged(false);
((AbstractRecord) record).setAllChanged(action == Action.DELETE);
}
}
}
/**
* The action to be performed by this operation
*/
enum Action {
/**
* Corresponds to {@link UpdatableRecord#store()}
*/
STORE,
/**
* Corresponds to {@link UpdatableRecord#delete()}
*/
DELETE
}
/**
* Collect queries
* <p>
@ -245,7 +276,7 @@ class BatchStore implements Batch {
* Generated UID
*/
private static final long serialVersionUID = -9047250761846931903L;
private final String sql;
private final String sql;
private final Query query;
QueryCollectorException(String sql, Query query) {

View File

@ -123,6 +123,7 @@ import org.jooq.exception.DataAccessException;
import org.jooq.exception.InvalidResultException;
import org.jooq.exception.MappingException;
import org.jooq.exception.SQLDialectNotSupportedException;
import org.jooq.impl.BatchCRUD.Action;
import org.jooq.tools.csv.CSVReader;
/**
@ -1895,7 +1896,7 @@ public class Executor implements Configuration {
*/
@Support
public final Batch batchStore(UpdatableRecord<?>... records) {
return new BatchStore(this, records);
return new BatchCRUD(this, Action.STORE, records);
}
/**
@ -1910,6 +1911,58 @@ public class Executor implements Configuration {
return batchStore(records.toArray(new UpdatableRecord[records.size()]));
}
/**
* Execute a set of <code>DELETE</code> queries in batch mode (with bind
* values).
* <p>
* This batch operation can be executed in two modes:
* <h3>With
* <code>{@link Settings#getStatementType()} == {@link StatementType#PREPARED_STATEMENT}</code>
* (the default)</h3> In this mode, record order is preserved as much as
* possible, as long as two subsequent records generate the same SQL (with
* bind variables). The number of executed batch operations corresponds to
* <code>[number of distinct rendered SQL statements]</code>. In the worst
* case, this corresponds to the number of total records.
* <p>
* The record type order is preserved in the way they are passed to this
* method. This is an example of how statements will be ordered: <code><pre>
* // Let's assume a[n] are all of the same type, just as b[n], c[n]...
* int[] result = create.batchStore(a1, a2, a3, b1, a4, c1, c2, a5)
* .execute();
* </pre></code> The above results in <code>result.length == 8</code> and
* the following 5 separate batch statements:
* <ol>
* <li>DELETE a1, a2, a3</li>
* <li>DELETE b1</li>
* <li>DELETE a4</li>
* <li>DELETE c1, c2</li>
* <li>DELETE a5</li>
* </ol>
* <h3>With
* <code>{@link Settings#getStatementType()} == {@link StatementType#STATIC_STATEMENT}</code>
* </h3> This mode may be better for large and complex batch delete
* operations, as the order of records is preserved entirely, and jOOQ can
* guarantee that only a single batch statement is serialised to the
* database.
*
* @see Statement#executeBatch()
*/
@Support
public final Batch batchDelete(UpdatableRecord<?>... records) {
return new BatchCRUD(this, Action.DELETE, records);
}
/**
* Execute a set of <code>DELETE</code> in batch mode (with bind values).
*
* @see #batchDelete(UpdatableRecord...)
* @see Statement#executeBatch()
*/
@Support
public final Batch batchDelete(Collection<? extends UpdatableRecord<?>> records) {
return batchDelete(records.toArray(new UpdatableRecord[records.size()]));
}
// -------------------------------------------------------------------------
// XXX DDL Statements
// -------------------------------------------------------------------------

View File

@ -310,9 +310,7 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
// [#673] If store() is called after delete(), a new INSERT should
// be executed and the record should be recreated
finally {
for (Field<?> field : getFields()) {
getValue0(field).setChanged(true);
}
setAllChanged(true);
}
}