From cea755069dcc69c493490da2a9cef50eb404292b Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Tue, 12 Feb 2013 16:22:04 +0100 Subject: [PATCH] [#834] Add support for the Firebird / Postgres UPDATE .. RETURNING clause --- .../test/_/testcases/InsertUpdateTests.java | 54 +++ .../src/org/jooq/test/jOOQAbstractTest.java | 5 + jOOQ/src/main/java/org/jooq/InsertQuery.java | 95 ++---- jOOQ/src/main/java/org/jooq/StoreQuery.java | 108 ++++++ .../java/org/jooq/UpdateConditionStep.java | 2 +- jOOQ/src/main/java/org/jooq/UpdateQuery.java | 67 +++- .../main/java/org/jooq/UpdateResultStep.java | 100 ++++++ .../java/org/jooq/UpdateReturningStep.java | 95 ++++++ .../main/java/org/jooq/UpdateWhereStep.java | 2 +- .../org/jooq/impl/AbstractStoreQuery.java | 308 +++++++++++++++++- .../java/org/jooq/impl/InsertQueryImpl.java | 287 +--------------- .../main/java/org/jooq/impl/UpdateImpl.java | 35 +- .../java/org/jooq/impl/UpdateQueryImpl.java | 3 + 13 files changed, 797 insertions(+), 364 deletions(-) create mode 100644 jOOQ/src/main/java/org/jooq/UpdateResultStep.java create mode 100644 jOOQ/src/main/java/org/jooq/UpdateReturningStep.java diff --git a/jOOQ-test/src/org/jooq/test/_/testcases/InsertUpdateTests.java b/jOOQ-test/src/org/jooq/test/_/testcases/InsertUpdateTests.java index 67017d1183..614153df43 100644 --- a/jOOQ-test/src/org/jooq/test/_/testcases/InsertUpdateTests.java +++ b/jOOQ-test/src/org/jooq/test/_/testcases/InsertUpdateTests.java @@ -55,6 +55,7 @@ import static org.jooq.SQLDialect.SYBASE; import static org.jooq.impl.Factory.cast; import static org.jooq.impl.Factory.castNull; import static org.jooq.impl.Factory.count; +import static org.jooq.impl.Factory.decode; import static org.jooq.impl.Factory.falseCondition; import static org.jooq.impl.Factory.inline; import static org.jooq.impl.Factory.max; @@ -700,6 +701,59 @@ extends BaseTest result1 = + create().update(TBook()) + .set(TBook_TITLE(), "XYZ") + .where(TBook_ID().eq(1)) + .returning(TBook_ID(), TBook_TITLE()) + .fetch(); + + assertEquals(1, result1.size()); + assertEquals(1, (int) result1.get(0).getValue(TBook_ID())); + assertEquals("XYZ", result1.get(0).getValue(TBook_TITLE())); + + switch (getDialect()) { + case FIREBIRD: { + break; + } + + // Some databases do not support RETURNING clauses that affect more + // than one row. + default: { + Result result2 = + create().update(TBook()) + .set(TBook_TITLE(), decode().value(TBook_ID()).when(1, "ABC").otherwise(TBook_TITLE())) + .where(TBook_ID().in(1, 2)) + .returning(TBook_ID(), TBook_TITLE()) + .fetch(); + + assertEquals(2, result2.size()); + assertEquals(asList(1, 2), result2.getValues(TBook_ID())); + assertEquals(asList("ABC", "Animal Farm"), result2.getValues(TBook_TITLE())); + } + } + } + @Test public void testInsertOnDuplicateKeyUpdate() throws Exception { switch (getDialect()) { diff --git a/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java b/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java index fc742c294a..55d6d36720 100644 --- a/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java +++ b/jOOQ-test/src/org/jooq/test/jOOQAbstractTest.java @@ -1378,6 +1378,11 @@ public abstract class jOOQAbstractTest< new InsertUpdateTests(this).testInsertReturning(); } + @Test + public void testUpdateReturning() throws Exception { + new InsertUpdateTests(this).testUpdateReturning(); + } + @Test public void testMerge() throws Exception { new InsertUpdateTests(this).testMerge(); diff --git a/jOOQ/src/main/java/org/jooq/InsertQuery.java b/jOOQ/src/main/java/org/jooq/InsertQuery.java index 6c66605efc..35ebd18bc7 100644 --- a/jOOQ/src/main/java/org/jooq/InsertQuery.java +++ b/jOOQ/src/main/java/org/jooq/InsertQuery.java @@ -170,107 +170,56 @@ public interface InsertQuery extends StoreQuery, Insert void addValuesForUpdate(Map, ?> map); /** - * Configure the INSERT statement to return all fields in - * R. - * - * @see #getReturnedRecords() + * {@inheritDoc} + *

+ * This feature works with INSERT statements for all SQL dialects */ + @Override @Support void setReturning(); /** - * Configure the INSERT statement to return the generated - * identity value. - * - * @param identity The table's identity - * @see #getReturnedRecords() + * {@inheritDoc} + *

+ * This feature works with INSERT statements for all SQL dialects */ + @Override @Support void setReturning(Identity identity); /** - * Configure the INSERT statement to return a list of fields in - * R. - * - * @param fields Fields to be returned - * @see #getReturnedRecords() + * {@inheritDoc} + *

+ * This feature works with INSERT statements for all SQL dialects */ + @Override @Support void setReturning(Field... fields); /** - * Configure the INSERT statement to return a list of fields in - * R. - * - * @param fields Fields to be returned - * @see #getReturnedRecords() + * {@inheritDoc} + *

+ * This feature works with INSERT statements for all SQL dialects */ + @Override @Support void setReturning(Collection> fields); /** - * The record holding returned values as specified by any of the - * {@link #setReturning()} methods. + * {@inheritDoc} *

- * If the insert statement returns several records, this is the same as - * calling getReturnedRecords().get(0) - *

- * This implemented differently for every dialect: - *

    - *
  • Firebird and Postgres have native support for - * INSERT .. RETURNING clauses
  • - *
  • HSQLDB, Oracle, and DB2 JDBC drivers allow for retrieving any table - * column as "generated key" in one statement
  • - *
  • Derby, H2, Ingres, MySQL, SQL Server only allow for retrieving - * IDENTITY column values as "generated key". If other fields are requested, - * a second statement is issued. Client code must assure transactional - * integrity between the two statements.
  • - *
  • Sybase and SQLite allow for retrieving IDENTITY values as - * @@identity or last_inserted_rowid() values. - * Those values are fetched in a separate SELECT statement. If - * other fields are requested, a second statement is issued. Client code - * must assure transactional integrity between the two statements.
  • - *
- * - * @return The returned value as specified by any of the - * {@link #setReturning()} methods. This may return - * null in case jOOQ could not retrieve any generated - * keys from the JDBC driver. - * @see #getReturnedRecords() + * This feature works with INSERT statements for all SQL dialects */ + @Override @Support R getReturnedRecord(); /** - * The records holding returned values as specified by any of the - * {@link #setReturning()} methods. + * {@inheritDoc} *

- * This implemented differently for every dialect: - *

    - *
  • Firebird and Postgres have native support for - * INSERT .. RETURNING clauses
  • - *
  • HSQLDB, Oracle, and DB2 JDBC drivers allow for retrieving any table - * column as "generated key" in one statement
  • - *
  • Derby, H2, Ingres, MySQL, SQL Server only allow for retrieving - * IDENTITY column values as "generated key". If other fields are requested, - * a second statement is issued. Client code must assure transactional - * integrity between the two statements.
  • - *
  • Sybase and SQLite allow for retrieving IDENTITY values as - * @@identity or last_inserted_rowid() values. - * Those values are fetched in a separate SELECT statement. If - * other fields are requested, a second statement is issued. Client code - * must assure transactional integrity between the two statements.
  • - *
- * - * @return The returned values as specified by any of the - * {@link #setReturning()} methods. Note: - *
    - *
  • Not all databases / JDBC drivers support returning several - * values on multi-row inserts!
  • This may return an empty - * Result in case jOOQ could not retrieve any generated - * keys from the JDBC driver.
  • - *
+ * This feature works with INSERT statements for all SQL dialects */ + @Override @Support Result getReturnedRecords(); diff --git a/jOOQ/src/main/java/org/jooq/StoreQuery.java b/jOOQ/src/main/java/org/jooq/StoreQuery.java index e8b8f0354b..722fae2901 100644 --- a/jOOQ/src/main/java/org/jooq/StoreQuery.java +++ b/jOOQ/src/main/java/org/jooq/StoreQuery.java @@ -35,6 +35,7 @@ */ package org.jooq; +import java.util.Collection; import java.util.Map; /** @@ -83,4 +84,111 @@ public interface StoreQuery extends Query { @Support void addValues(Map, ?> map); + /** + * Configure the INSERT or UPDATE statement to return all fields in + * R. + * + * @see #getReturnedRecords() + */ + @Support + void setReturning(); + + /** + * Configure the INSERT or UPDATE statement to return the generated + * identity value. + * + * @param identity The table's identity + * @see #getReturnedRecords() + */ + @Support + void setReturning(Identity identity); + + /** + * Configure the INSERT or UPDATE statement to return a list of fields in + * R. + * + * @param fields Fields to be returned + * @see #getReturnedRecords() + */ + @Support + void setReturning(Field... fields); + + /** + * Configure the INSERT or UPDATE statement to return a list of fields in + * R. + * + * @param fields Fields to be returned + * @see #getReturnedRecords() + */ + @Support + void setReturning(Collection> fields); + + /** + * The record holding returned values as specified by any of the + * {@link #setReturning()} methods. + *

+ * If the insert statement returns several records, this is the same as + * calling getReturnedRecords().get(0) + *

+ * This implemented differently for every dialect: + *

    + *
  • Firebird and Postgres have native support for + * INSERT .. RETURNING and UPDATE .. RETURNING + * clauses
  • + *
  • HSQLDB, Oracle, and DB2 JDBC drivers allow for retrieving any table + * column as "generated key" in one statement
  • + *
  • Derby, H2, Ingres, MySQL, SQL Server only allow for retrieving + * IDENTITY column values as "generated key". If other fields are requested, + * a second statement is issued. Client code must assure transactional + * integrity between the two statements.
  • + *
  • Sybase and SQLite allow for retrieving IDENTITY values as + * @@identity or last_inserted_rowid() values. + * Those values are fetched in a separate SELECT statement. If + * other fields are requested, a second statement is issued. Client code + * must assure transactional integrity between the two statements.
  • + *
+ * + * @return The returned value as specified by any of the + * {@link #setReturning()} methods. This may return + * null in case jOOQ could not retrieve any generated + * keys from the JDBC driver. + * @see #getReturnedRecords() + */ + @Support + R getReturnedRecord(); + + /** + * The records holding returned values as specified by any of the + * {@link #setReturning()} methods. + *

+ * This implemented differently for every dialect: + *

    + *
  • Firebird and Postgres have native support for + * INSERT .. RETURNING and UPDATE .. RETURNING + * clauses
  • + *
  • HSQLDB, Oracle, and DB2 JDBC drivers allow for retrieving any table + * column as "generated key" in one statement
  • + *
  • Derby, H2, Ingres, MySQL, SQL Server only allow for retrieving + * IDENTITY column values as "generated key". If other fields are requested, + * a second statement is issued. Client code must assure transactional + * integrity between the two statements.
  • + *
  • Sybase and SQLite allow for retrieving IDENTITY values as + * @@identity or last_inserted_rowid() values. + * Those values are fetched in a separate SELECT statement. If + * other fields are requested, a second statement is issued. Client code + * must assure transactional integrity between the two statements.
  • + *
+ * + * @return The returned values as specified by any of the + * {@link #setReturning()} methods. Note: + *
    + *
  • Not all databases / JDBC drivers support returning several + * values on multi-row inserts!
  • This may return an empty + * Result in case jOOQ could not retrieve any generated + * keys from the JDBC driver.
  • + *
+ */ + @Support + Result getReturnedRecords(); + } diff --git a/jOOQ/src/main/java/org/jooq/UpdateConditionStep.java b/jOOQ/src/main/java/org/jooq/UpdateConditionStep.java index 7219d47575..21b60cf86a 100644 --- a/jOOQ/src/main/java/org/jooq/UpdateConditionStep.java +++ b/jOOQ/src/main/java/org/jooq/UpdateConditionStep.java @@ -53,7 +53,7 @@ import org.jooq.impl.Factory; * * @author Lukas Eder */ -public interface UpdateConditionStep extends UpdateFinalStep { +public interface UpdateConditionStep extends UpdateFinalStep, UpdateReturningStep { /** * Combine the currently assembled conditions with another one using the diff --git a/jOOQ/src/main/java/org/jooq/UpdateQuery.java b/jOOQ/src/main/java/org/jooq/UpdateQuery.java index 4280638cd3..6d1fed5bba 100644 --- a/jOOQ/src/main/java/org/jooq/UpdateQuery.java +++ b/jOOQ/src/main/java/org/jooq/UpdateQuery.java @@ -37,6 +37,7 @@ package org.jooq; import static org.jooq.SQLDialect.DB2; +import static org.jooq.SQLDialect.FIREBIRD; import static org.jooq.SQLDialect.H2; import static org.jooq.SQLDialect.HSQLDB; import static org.jooq.SQLDialect.INGRES; @@ -369,7 +370,7 @@ public interface UpdateQuery extends StoreQuery, ConditionP // [jooq-tools] END [addValues] // ------------------------------------------------------------------------ - // Methods from ConditionProvider + // XXX: Methods from ConditionProvider // ------------------------------------------------------------------------ /** @@ -400,4 +401,68 @@ public interface UpdateQuery extends StoreQuery, ConditionP @Support void addConditions(Operator operator, Collection conditions); + // ------------------------------------------------------------------------ + // XXX: Methods for the UPDATE .. RETURNING syntax + // ------------------------------------------------------------------------ + + /** + * {@inheritDoc} + *

+ * This feature works with UPDATE statements for a subset of + * SQL dialects + */ + @Override + @Support({ FIREBIRD, POSTGRES }) + void setReturning(); + + /** + * {@inheritDoc} + *

+ * This feature works with UPDATE statements for a subset of + * SQL dialects + */ + @Override + @Support({ FIREBIRD, POSTGRES }) + void setReturning(Identity identity); + + /** + * {@inheritDoc} + *

+ * This feature works with UPDATE statements for a subset of + * SQL dialects + */ + @Override + @Support({ FIREBIRD, POSTGRES }) + void setReturning(Field... fields); + + /** + * {@inheritDoc} + *

+ * This feature works with UPDATE statements for a subset of + * SQL dialects + */ + @Override + @Support({ FIREBIRD, POSTGRES }) + void setReturning(Collection> fields); + + /** + * {@inheritDoc} + *

+ * This feature works with UPDATE statements for a subset of + * SQL dialects + */ + @Override + @Support({ FIREBIRD, POSTGRES }) + R getReturnedRecord(); + + /** + * {@inheritDoc} + *

+ * This feature works with UPDATE statements for a subset of + * SQL dialects + */ + @Override + @Support({ FIREBIRD, POSTGRES }) + Result getReturnedRecords(); + } diff --git a/jOOQ/src/main/java/org/jooq/UpdateResultStep.java b/jOOQ/src/main/java/org/jooq/UpdateResultStep.java new file mode 100644 index 0000000000..f8883f2b32 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/UpdateResultStep.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2009-2013, 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; + +import static org.jooq.SQLDialect.FIREBIRD; +import static org.jooq.SQLDialect.POSTGRES; + +import org.jooq.exception.DataAccessException; + +/** + * This type is used for the {@link Update}'s DSL API. + *

+ * Example:

+ * Executor create = new Executor(config);
+ *
+ * TableRecord record =
+ * create.update(table)
+ *       .set(field1, value1)
+ *       .set(field2, value2)
+ *       .returning(field1)
+ *       .fetchOne();
+ * 
+ *

+ * This implemented differently for every dialect: + *

    + *
  • Firebird and Postgres have native support for + * UPDATE .. RETURNING clauses
  • + *
+ * + * @author Lukas Eder + */ +public interface UpdateResultStep extends Insert { + + /** + * The result holding returned values as specified by the + * {@link UpdateReturningStep} + *

+ * This currently only works well for DB2, HSQLDB, MySQL, and Postgres + * + * @return The returned values as specified by the + * {@link UpdateReturningStep}. Note: + *

    + *
  • Not all databases / JDBC drivers support returning several + * values on multi-row inserts!
  • This may return an empty + * Result in case jOOQ could not retrieve any generated + * keys from the JDBC driver.
  • + *
+ * @throws DataAccessException if something went wrong executing the query + * @see UpdateQuery#getReturnedRecords() + */ + @Support({ FIREBIRD, POSTGRES }) + Result fetch() throws DataAccessException; + + /** + * The record holding returned values as specified by the + * {@link UpdateReturningStep} + * + * @return The returned value as specified by the + * {@link UpdateReturningStep}. This may return null in + * case jOOQ could not retrieve any generated keys from the JDBC + * driver. + * @throws DataAccessException if something went wrong executing the query + * @see UpdateQuery#getReturnedRecord() + */ + @Support({ FIREBIRD, POSTGRES }) + R fetchOne() throws DataAccessException; +} diff --git a/jOOQ/src/main/java/org/jooq/UpdateReturningStep.java b/jOOQ/src/main/java/org/jooq/UpdateReturningStep.java new file mode 100644 index 0000000000..b19d0b414a --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/UpdateReturningStep.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2009-2013, 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; + +import static org.jooq.SQLDialect.FIREBIRD; +import static org.jooq.SQLDialect.POSTGRES; + +import java.util.Collection; + +/** + * This type is used for the {@link Update}'s DSL API. + *

+ * Example:

+ * Executor create = new Executor(config);
+ *
+ * TableRecord record =
+ * create.update(table)
+ *       .set(field1, value1)
+ *       .set(field2, value2)
+ *       .returning(field1)
+ *       .fetchOne();
+ * 
+ *

+ * This implemented differently for every dialect: + *

    + *
  • Firebird and Postgres have native support for + * UPDATE .. RETURNING clauses
  • + *
+ * + * @author Lukas Eder + */ +public interface UpdateReturningStep { + + /** + * Configure the UPDATE statement to return all fields in + * R. + * + * @see UpdateResultStep + */ + @Support({ FIREBIRD, POSTGRES }) + UpdateResultStep returning(); + + /** + * Configure the UPDATE statement to return a list of fields in + * R. + * + * @param fields Fields to be returned + * @see UpdateResultStep + */ + @Support({ FIREBIRD, POSTGRES }) + UpdateResultStep returning(Field... fields); + + /** + * Configure the UPDATE statement to return a list of fields in + * R. + * + * @param fields Fields to be returned + * @see UpdateResultStep + */ + @Support({ FIREBIRD, POSTGRES }) + UpdateResultStep returning(Collection> fields); +} diff --git a/jOOQ/src/main/java/org/jooq/UpdateWhereStep.java b/jOOQ/src/main/java/org/jooq/UpdateWhereStep.java index f8cef77893..0b03b47a81 100644 --- a/jOOQ/src/main/java/org/jooq/UpdateWhereStep.java +++ b/jOOQ/src/main/java/org/jooq/UpdateWhereStep.java @@ -54,7 +54,7 @@ import org.jooq.impl.Factory; * * @author Lukas Eder */ -public interface UpdateWhereStep extends UpdateFinalStep { +public interface UpdateWhereStep extends UpdateFinalStep, UpdateReturningStep { /** * Add conditions to the query diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java b/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java index 0d898453e4..9a58fde893 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractStoreQuery.java @@ -35,13 +35,33 @@ */ package org.jooq.impl; +import static org.jooq.impl.Utils.fieldArray; +import static org.jooq.util.sqlite.SQLiteFactory.rowid; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.Map; +import org.jooq.BindContext; import org.jooq.Configuration; +import org.jooq.ExecuteContext; +import org.jooq.ExecuteListener; import org.jooq.Field; +import org.jooq.Identity; +import org.jooq.QueryPart; import org.jooq.Record; +import org.jooq.RenderContext; +import org.jooq.Result; +import org.jooq.SQLDialect; import org.jooq.StoreQuery; import org.jooq.Table; +import org.jooq.tools.jdbc.JDBCUtils; /** * A default implementation for store queries. @@ -53,14 +73,17 @@ abstract class AbstractStoreQuery extends AbstractQuery implem /** * Generated UID */ - private static final long serialVersionUID = 6864591335823160569L; + private static final long serialVersionUID = 6864591335823160569L; - private final Table into; + private final Table into; + private final QueryPartList> returning; + private Result returned; AbstractStoreQuery(Configuration configuration, Table into) { super(configuration); this.into = into; + this.returning = new QueryPartList>(); } protected abstract Map, Field> getValues(); @@ -82,4 +105,285 @@ abstract class AbstractStoreQuery extends AbstractQuery implem public final void addValue(Field field, Field value) { getValues().put(field, Utils.field(value, field)); } + + @Override + public final void setReturning() { + setReturning(getInto().fields()); + } + + @Override + public final void setReturning(Identity identity) { + if (identity != null) { + setReturning(identity.getField()); + } + } + + @Override + public final void setReturning(Field... fields) { + setReturning(Arrays.asList(fields)); + } + + @Override + public final void setReturning(Collection> fields) { + returning.clear(); + returning.addAll(fields); + } + + @Override + public final R getReturnedRecord() { + if (getReturnedRecords().size() == 0) { + return null; + } + + return getReturnedRecords().get(0); + } + + @Override + public final Result getReturnedRecords() { + if (returned == null) { + returned = new ResultImpl(getConfiguration(), returning); + } + + return returned; + } + + final void toSQLReturning(RenderContext context) { + if (!returning.isEmpty()) { + switch (context.getDialect()) { + case FIREBIRD: + case POSTGRES: + context.formatSeparator() + .keyword("returning ") + .sql(returning); + break; + + default: + // Other dialects don't render a RETURNING clause, but + // use JDBC's Statement.RETURN_GENERATED_KEYS mode instead + } + } + } + + final void bindReturning(BindContext context) { + switch (context.getDialect()) { + case FIREBIRD: + case POSTGRES: + context.bind((QueryPart) returning); + break; + + default: + // Other dialects don't bind a RETURNING clause, but + // use JDBC's Statement.RETURN_GENERATED_KEYS mode instead + } + } + + @Override + protected final void prepare(ExecuteContext ctx) throws SQLException { + Connection connection = ctx.connection(); + + // Just in case, always set Sybase ASE statement mode to return + // Generated keys if client code wants to SELECT @@identity afterwards + if (ctx.getDialect() == SQLDialect.ASE) { + ctx.statement(connection.prepareStatement(ctx.sql(), Statement.RETURN_GENERATED_KEYS)); + return; + } + + // Normal statement preparing if no values should be returned + else if (returning.isEmpty()) { + super.prepare(ctx); + return; + } + + // Values should be returned from the INSERT + else { + switch (ctx.getDialect()) { + + // Postgres uses the RETURNING clause in SQL + case FIREBIRD: + case POSTGRES: + // SQLite will select last_insert_rowid() after the INSER + case SQLITE: + // Sybase will select @@identity after the INSERT + case CUBRID: + case SYBASE: + super.prepare(ctx); + return; + + // Some dialects can only return AUTO_INCREMENT values + // Other values have to be fetched in a second step + // [#1260] TODO CUBRID supports this, but there's a JDBC bug + case ASE: + case DERBY: + case H2: + case INGRES: + case MYSQL: + case SQLSERVER: + ctx.statement(connection.prepareStatement(ctx.sql(), Statement.RETURN_GENERATED_KEYS)); + return; + + // The default is to return all requested fields directly + case DB2: + case HSQLDB: + case ORACLE: + default: { + List names = new ArrayList(); + + for (Field field : returning) { + names.add(field.getName()); + } + + ctx.statement(connection.prepareStatement(ctx.sql(), names.toArray(new String[names.size()]))); + return; + } + } + } + } + + @Override + protected final int execute(ExecuteContext ctx, ExecuteListener listener) throws SQLException { + if (returning.isEmpty()) { + return super.execute(ctx, listener); + } + else { + int result = 1; + ResultSet rs; + switch (ctx.getDialect()) { + + // SQLite can select _rowid_ after the insert + case SQLITE: { + listener.executeStart(ctx); + result = ctx.statement().executeUpdate(); + listener.executeEnd(ctx); + + Executor create = new Executor(ctx.connection(), SQLDialect.SQLITE, ctx.getSettings()); + returned = + create.select(returning) + .from(getInto()) + .where(rowid().equal(rowid().getDataType().convert(create.lastID()))) + .fetchInto(getInto()); + + return result; + } + + // Sybase can select @@identity after the insert + // TODO [#832] Fix this. This might be a driver issue. JDBC + // Generated keys don't work with jconn3, but they seem to work + // with jTDS (which is used for Sybase ASE integration) + case CUBRID: + case SYBASE: { + listener.executeStart(ctx); + result = ctx.statement().executeUpdate(); + listener.executeEnd(ctx); + + selectReturning(ctx.configuration(), create(ctx).lastID()); + return result; + } + + // Some dialects can only retrieve "identity" (AUTO_INCREMENT) values + // Additional values have to be fetched explicitly + // [#1260] TODO CUBRID supports this, but there's a JDBC bug + case ASE: + case DERBY: + case H2: + case INGRES: + case MYSQL: + case SQLSERVER: { + listener.executeStart(ctx); + result = ctx.statement().executeUpdate(); + listener.executeEnd(ctx); + + rs = ctx.statement().getGeneratedKeys(); + + try { + List list = new ArrayList(); + + // Some JDBC drivers seem to illegally return null + // from getGeneratedKeys() sometimes + if (rs != null) { + while (rs.next()) { + list.add(rs.getObject(1)); + } + } + + selectReturning(ctx, list.toArray()); + return result; + } + finally { + JDBCUtils.safeClose(rs); + } + } + + // Firebird and Postgres can execute the INSERT .. RETURNING + // clause like a select clause. JDBC support is not implemented + // in the Postgres JDBC driver + case FIREBIRD: + case POSTGRES: { + listener.executeStart(ctx); + rs = ctx.statement().executeQuery(); + listener.executeEnd(ctx); + + break; + } + + // These dialects have full JDBC support + case DB2: + case HSQLDB: + case ORACLE: + default: { + listener.executeStart(ctx); + result = ctx.statement().executeUpdate(); + listener.executeEnd(ctx); + + rs = ctx.statement().getGeneratedKeys(); + break; + } + } + + ExecuteContext ctx2 = new DefaultExecuteContext(ctx.configuration()); + ExecuteListener listener2 = new ExecuteListeners(ctx2); + + ctx2.resultSet(rs); + returned = new CursorImpl(ctx2, listener2, fieldArray(returning), null, false).fetch().into(getInto()); + return result; + } + } + + /** + * Get the returning record in those dialects that do not support fetching + * arbitrary fields from JDBC's {@link Statement#getGeneratedKeys()} method. + */ + @SuppressWarnings("unchecked") + private final void selectReturning(Configuration configuration, Object... values) { + if (values != null && values.length > 0) { + + // This shouldn't be null, as relevant dialects should + // return empty generated keys ResultSet + if (into.getIdentity() != null) { + Field field = (Field) into.getIdentity().getField(); + Number[] ids = new Number[values.length]; + for (int i = 0; i < values.length; i++) { + ids[i] = field.getDataType().convert(values[i]); + } + + // Only the IDENTITY value was requested. No need for an + // additional query + if (returning.size() == 1 && returning.get(0).equals(field)) { + for (Number id : ids) { + R typed = Utils.newRecord(into, configuration); + ((AbstractRecord) typed).setValue(field, new Value(id)); + getReturnedRecords().add(typed); + } + } + + // Other values are requested, too. Run another query + else { + returned = + create(configuration).select(returning) + .from(into) + .where(field.in(ids)) + .fetchInto(into); + } + } + } + } } diff --git a/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java index fbb82d15b3..63b11f310e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/InsertQueryImpl.java @@ -37,35 +37,21 @@ package org.jooq.impl; import static org.jooq.SQLDialect.MYSQL; -import static org.jooq.impl.Utils.fieldArray; -import static org.jooq.util.sqlite.SQLiteFactory.rowid; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import org.jooq.BindContext; import org.jooq.Condition; import org.jooq.Configuration; -import org.jooq.ExecuteContext; -import org.jooq.ExecuteListener; import org.jooq.Field; -import org.jooq.Identity; import org.jooq.InsertQuery; import org.jooq.Merge; import org.jooq.MergeNotMatchedStep; import org.jooq.MergeOnConditionStep; -import org.jooq.QueryPart; import org.jooq.Record; import org.jooq.RenderContext; -import org.jooq.Result; -import org.jooq.SQLDialect; import org.jooq.Table; import org.jooq.UpdatableTable; import org.jooq.exception.SQLDialectNotSupportedException; @@ -79,8 +65,6 @@ class InsertQueryImpl extends AbstractStoreQuery implements private final FieldMapForUpdate updateMap; private final FieldMapsForInsert insertMaps; - private final QueryPartList> returning; - private Result returned; private boolean onDuplicateKeyUpdate; private boolean onDuplicateKeyIgnore; @@ -89,7 +73,6 @@ class InsertQueryImpl extends AbstractStoreQuery implements updateMap = new FieldMapForUpdate(); insertMaps = new FieldMapsForInsert(); - returning = new QueryPartList>(); } @Override @@ -334,20 +317,7 @@ class InsertQueryImpl extends AbstractStoreQuery implements .sql(" ") .sql(insertMaps); - if (!returning.isEmpty()) { - switch (context.getDialect()) { - case FIREBIRD: - case POSTGRES: - context.formatSeparator() - .keyword("returning ") - .sql(returning); - break; - - default: - // Other dialects don't render a RETURNING clause, but - // use JDBC's Statement.RETURN_GENERATED_KEYS mode instead - } - } + toSQLReturning(context); } private final void bindInsert(BindContext context) { @@ -355,16 +325,7 @@ class InsertQueryImpl extends AbstractStoreQuery implements .bind(insertMaps) .bind(updateMap); - switch (context.getDialect()) { - case FIREBIRD: - case POSTGRES: - context.bind((QueryPart) returning); - break; - - default: - // Other dialects don't bind a RETURNING clause, but - // use JDBC's Statement.RETURN_GENERATED_KEYS mode instead - } + bindReturning(context); } @SuppressWarnings("unchecked") @@ -415,248 +376,4 @@ class InsertQueryImpl extends AbstractStoreQuery implements public final boolean isExecutable() { return insertMaps.isExecutable(); } - - @Override - protected final void prepare(ExecuteContext ctx) throws SQLException { - Connection connection = ctx.connection(); - - // Just in case, always set Sybase ASE statement mode to return - // Generated keys if client code wants to SELECT @@identity afterwards - if (ctx.getDialect() == SQLDialect.ASE) { - ctx.statement(connection.prepareStatement(ctx.sql(), Statement.RETURN_GENERATED_KEYS)); - return; - } - - // Normal statement preparing if no values should be returned - else if (returning.isEmpty()) { - super.prepare(ctx); - return; - } - - // Values should be returned from the INSERT - else { - switch (ctx.getDialect()) { - - // Postgres uses the RETURNING clause in SQL - case FIREBIRD: - case POSTGRES: - // SQLite will select last_insert_rowid() after the INSER - case SQLITE: - // Sybase will select @@identity after the INSERT - case CUBRID: - case SYBASE: - super.prepare(ctx); - return; - - // Some dialects can only return AUTO_INCREMENT values - // Other values have to be fetched in a second step - // [#1260] TODO CUBRID supports this, but there's a JDBC bug - case ASE: - case DERBY: - case H2: - case INGRES: - case MYSQL: - case SQLSERVER: - ctx.statement(connection.prepareStatement(ctx.sql(), Statement.RETURN_GENERATED_KEYS)); - return; - - // The default is to return all requested fields directly - default: { - List names = new ArrayList(); - - for (Field field : returning) { - names.add(field.getName()); - } - - ctx.statement(connection.prepareStatement(ctx.sql(), names.toArray(new String[names.size()]))); - return; - } - } - } - } - - @Override - protected final int execute(ExecuteContext ctx, ExecuteListener listener) throws SQLException { - if (returning.isEmpty()) { - return super.execute(ctx, listener); - } - else { - int result = 1; - ResultSet rs; - switch (ctx.getDialect()) { - - // SQLite can select _rowid_ after the insert - case SQLITE: { - listener.executeStart(ctx); - result = ctx.statement().executeUpdate(); - listener.executeEnd(ctx); - - Executor create = new Executor(ctx.connection(), SQLDialect.SQLITE, ctx.getSettings()); - returned = - create.select(returning) - .from(getInto()) - .where(rowid().equal(rowid().getDataType().convert(create.lastID()))) - .fetchInto(getInto()); - - return result; - } - - // Sybase can select @@identity after the insert - // TODO [#832] Fix this. This might be a driver issue. JDBC - // Generated keys don't work with jconn3, but they seem to work - // with jTDS (which is used for Sybase ASE integration) - case CUBRID: - case SYBASE: { - listener.executeStart(ctx); - result = ctx.statement().executeUpdate(); - listener.executeEnd(ctx); - - selectReturning(ctx.configuration(), create(ctx).lastID()); - return result; - } - - // Some dialects can only retrieve "identity" (AUTO_INCREMENT) values - // Additional values have to be fetched explicitly - // [#1260] TODO CUBRID supports this, but there's a JDBC bug - case ASE: - case DERBY: - case H2: - case INGRES: - case MYSQL: - case SQLSERVER: { - listener.executeStart(ctx); - result = ctx.statement().executeUpdate(); - listener.executeEnd(ctx); - - rs = ctx.statement().getGeneratedKeys(); - - try { - List list = new ArrayList(); - while (rs.next()) { - list.add(rs.getObject(1)); - } - - selectReturning(ctx, list.toArray()); - return result; - } - finally { - rs.close(); - } - } - - // Firebird and Postgres can execute the INSERT .. RETURNING - // clause like a select clause. JDBC support is not implemented - // in the Postgres JDBC driver - case FIREBIRD: - case POSTGRES: { - listener.executeStart(ctx); - rs = ctx.statement().executeQuery(); - listener.executeEnd(ctx); - - break; - } - - // These dialects have full JDBC support - case DB2: - case HSQLDB: - case ORACLE: - default: { - listener.executeStart(ctx); - result = ctx.statement().executeUpdate(); - listener.executeEnd(ctx); - - rs = ctx.statement().getGeneratedKeys(); - break; - } - } - - ExecuteContext ctx2 = new DefaultExecuteContext(ctx.configuration()); - ExecuteListener listener2 = new ExecuteListeners(ctx2); - - ctx2.resultSet(rs); - returned = new CursorImpl(ctx2, listener2, fieldArray(returning), null, false).fetch().into(getInto()); - return result; - } - } - - /** - * Get the returning record in those dialects that do not support fetching - * arbitrary fields from JDBC's {@link Statement#getGeneratedKeys()} method. - */ - @SuppressWarnings("unchecked") - private final void selectReturning(Configuration configuration, Object... values) { - if (values != null && values.length > 0) { - Table into = getInto(); - - // This shouldn't be null, as relevant dialects should - // return empty generated keys ResultSet - if (into.getIdentity() != null) { - Field field = (Field) into.getIdentity().getField(); - Number[] ids = new Number[values.length]; - for (int i = 0; i < values.length; i++) { - ids[i] = field.getDataType().convert(values[i]); - } - - // Only the IDENTITY value was requested. No need for an - // additional query - if (returning.size() == 1 && returning.get(0).equals(field)) { - for (Number id : ids) { - R typed = Utils.newRecord(into, configuration); - ((AbstractRecord) typed).setValue(field, new Value(id)); - getReturnedRecords().add(typed); - } - } - - // Other values are requested, too. Run another query - else { - returned = - create(configuration).select(returning) - .from(into) - .where(field.in(ids)) - .fetchInto(into); - } - } - } - } - - @Override - public final void setReturning() { - setReturning(getInto().fields()); - } - - @Override - public final void setReturning(Identity identity) { - if (identity != null) { - setReturning(identity.getField()); - } - } - - @Override - public final void setReturning(Field... fields) { - setReturning(Arrays.asList(fields)); - } - - @Override - public final void setReturning(Collection> fields) { - returning.clear(); - returning.addAll(fields); - } - - @Override - public final R getReturnedRecord() { - if (getReturnedRecords().size() == 0) { - return null; - } - - return getReturnedRecords().get(0); - } - - @Override - public final Result getReturnedRecords() { - if (returned == null) { - returned = new ResultImpl(getConfiguration(), returning); - } - - return returned; - } } diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java index 5de264fb08..9a441059dc 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdateImpl.java @@ -72,6 +72,7 @@ import org.jooq.Record6; import org.jooq.Record7; import org.jooq.Record8; import org.jooq.Record9; +import org.jooq.Result; import org.jooq.Row1; import org.jooq.Row10; import org.jooq.Row11; @@ -98,6 +99,7 @@ import org.jooq.Select; import org.jooq.Table; import org.jooq.UpdateConditionStep; import org.jooq.UpdateQuery; +import org.jooq.UpdateResultStep; import org.jooq.UpdateSetFirstStep; import org.jooq.UpdateSetMoreStep; import org.jooq.UpdateWhereStep; @@ -114,7 +116,8 @@ final class UpdateImpl // Cascading interface implementations for Update behaviour UpdateSetFirstStep, UpdateSetMoreStep, - UpdateConditionStep { + UpdateConditionStep, + UpdateResultStep { /** * Generated UID @@ -567,4 +570,34 @@ final class UpdateImpl public final UpdateImpl orNotExists(Select select) { return or(notExists(select)); } + + @Override + public final UpdateImpl returning() { + getDelegate().setReturning(); + return this; + } + + @Override + public final UpdateImpl returning(Field... f) { + getDelegate().setReturning(f); + return this; + } + + @Override + public final UpdateImpl returning(Collection> f) { + getDelegate().setReturning(f); + return this; + } + + @Override + public final Result fetch() { + getDelegate().execute(); + return getDelegate().getReturnedRecords(); + } + + @Override + public final R fetchOne() { + getDelegate().execute(); + return getDelegate().getReturnedRecord(); + } } diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java index 5f43fb5814..2c259fc335 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdateQueryImpl.java @@ -499,6 +499,8 @@ class UpdateQueryImpl extends AbstractStoreQuery implements .keyword("where ") .sql(getWhere()); } + + toSQLReturning(context); } @Override @@ -525,6 +527,7 @@ class UpdateQueryImpl extends AbstractStoreQuery implements } context.bind(condition); + bindReturning(context); } @Override