diff --git a/jOOQ-test/src/org/jooq/test/_/testcases/BatchTests.java b/jOOQ-test/src/org/jooq/test/_/testcases/BatchTests.java new file mode 100644 index 0000000000..9554905cdb --- /dev/null +++ b/jOOQ-test/src/org/jooq/test/_/testcases/BatchTests.java @@ -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 & Record6, + AP, + B extends UpdatableRecord, + S extends UpdatableRecord & Record1, + B2S extends UpdatableRecord & Record3, + BS extends UpdatableRecord, + L extends TableRecord & Record2, + X extends TableRecord, + DATE extends UpdatableRecord, + BOOL extends UpdatableRecord, + D extends UpdatableRecord, + T extends UpdatableRecord, + U extends TableRecord, + I extends TableRecord, + IPK extends UpdatableRecord, + T725 extends UpdatableRecord, + T639 extends UpdatableRecord, + T785 extends TableRecord> +extends BaseTest { + + public BatchTests(jOOQAbstractTest 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 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())); + } +} diff --git a/jOOQ-test/src/org/jooq/test/_/testcases/GeneralTests.java b/jOOQ-test/src/org/jooq/test/_/testcases/GeneralTests.java index 3b4c3a8506..a2039fc4f1 100644 --- a/jOOQ-test/src/org/jooq/test/_/testcases/GeneralTests.java +++ b/jOOQ-test/src/org/jooq/test/_/testcases/GeneralTests.java @@ -419,130 +419,4 @@ extends BaseTest[] 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 *

@@ -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) { diff --git a/jOOQ/src/main/java/org/jooq/impl/Executor.java b/jOOQ/src/main/java/org/jooq/impl/Executor.java index df58453c40..c59d71a2b3 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Executor.java +++ b/jOOQ/src/main/java/org/jooq/impl/Executor.java @@ -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 DELETE queries in batch mode (with bind + * values). + *

+ * This batch operation can be executed in two modes: + *

With + * {@link Settings#getStatementType()} == {@link StatementType#PREPARED_STATEMENT} + * (the default)

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 + * [number of distinct rendered SQL statements]. In the worst + * case, this corresponds to the number of total records. + *

+ * 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:

+     * // 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();
+     * 
The above results in result.length == 8 and + * the following 5 separate batch statements: + *
    + *
  1. DELETE a1, a2, a3
  2. + *
  3. DELETE b1
  4. + *
  5. DELETE a4
  6. + *
  7. DELETE c1, c2
  8. + *
  9. DELETE a5
  10. + *
+ *

With + * {@link Settings#getStatementType()} == {@link StatementType#STATIC_STATEMENT} + *

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 DELETE in batch mode (with bind values). + * + * @see #batchDelete(UpdatableRecord...) + * @see Statement#executeBatch() + */ + @Support + public final Batch batchDelete(Collection> records) { + return batchDelete(records.toArray(new UpdatableRecord[records.size()])); + } + // ------------------------------------------------------------------------- // XXX DDL Statements // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java index ffaf503461..a4e1230cc2 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java @@ -310,9 +310,7 @@ public class UpdatableRecordImpl> 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); } }