parent
6f6dad4bf3
commit
ce7a71b346
@ -1,441 +0,0 @@
|
||||
/**
|
||||
* 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.test._.testcases;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.jooq.KeepResultSetMode.CLOSE_AFTER_FETCH;
|
||||
import static org.jooq.KeepResultSetMode.KEEP_AFTER_FETCH;
|
||||
import static org.jooq.KeepResultSetMode.UPDATE_ON_CHANGE;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.jooq.Cursor;
|
||||
import org.jooq.DSLContext;
|
||||
import org.jooq.Delete;
|
||||
import org.jooq.ExecuteContext;
|
||||
import org.jooq.Query;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.Record1;
|
||||
import org.jooq.Record2;
|
||||
import org.jooq.Record3;
|
||||
import org.jooq.Record6;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.Select;
|
||||
import org.jooq.TableRecord;
|
||||
import org.jooq.UpdatableRecord;
|
||||
import org.jooq.exception.DataAccessException;
|
||||
import org.jooq.impl.DefaultExecuteListener;
|
||||
import org.jooq.test.BaseTest;
|
||||
import org.jooq.test.jOOQAbstractTest;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class KeepResultSetTests<
|
||||
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>,
|
||||
UU extends UpdatableRecord<UU>,
|
||||
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, UU, I, IPK, T725, T639, T785> {
|
||||
|
||||
public KeepResultSetTests(jOOQAbstractTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, UU, I, IPK, T725, T639, T785> delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* This listener is used to check if a <code>SELECT</code> statement is
|
||||
* issued after a call to {@link Record#refresh()}.
|
||||
*/
|
||||
private static class NoStatementAfterCRUDListener extends DefaultExecuteListener {
|
||||
|
||||
/**
|
||||
* Default UID
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Class<? extends Query> type;
|
||||
private final String crudMethod;
|
||||
|
||||
NoStatementAfterCRUDListener(Class<? extends Query> type, String crudMethod) {
|
||||
this.type = type;
|
||||
this.crudMethod = crudMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ExecuteContext ctx) {
|
||||
super.start(ctx);
|
||||
|
||||
if (ctx.query() != null) {
|
||||
if (type.isAssignableFrom(ctx.query().getClass())) {
|
||||
for (StackTraceElement e : Thread.currentThread().getStackTrace()) {
|
||||
if (e.getMethodName().equals(crudMethod)) {
|
||||
fail("Record." + crudMethod + "() should not execute any " + type.getName() + " queries");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void testFailUpdateRow(ResultSet rs) {
|
||||
try {
|
||||
rs.updateRow();
|
||||
fail();
|
||||
}
|
||||
catch (SQLException expected) {}
|
||||
|
||||
}
|
||||
|
||||
private void testFailRefresh(Record record) {
|
||||
try {
|
||||
record.refresh();
|
||||
fail();
|
||||
}
|
||||
catch (DataAccessException expected) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithCloseAfterFetch() throws Exception {
|
||||
DSLContext create = create(new NoStatementAfterCRUDListener(Select.class, "refresh"));
|
||||
|
||||
Result<B> b1 = create.selectFrom(TBook()).fetch();
|
||||
assertNull(b1.resultSet());
|
||||
|
||||
// Use plain SQL to prevent fetching of UpdatableRecord
|
||||
Result<Record> b2 = create.select().from(TBook().getName()).keepResultSet(CLOSE_AFTER_FETCH).fetch();
|
||||
assertNull(b2.resultSet());
|
||||
|
||||
// Changing a TITLE has no effect
|
||||
Record r = b2.get(0);
|
||||
r.setValue(TBook_TITLE(), "XX");
|
||||
assertTrue(r.changed());
|
||||
assertFalse(r.original().equals(r));
|
||||
assertEquals(BOOK_TITLES.get(0), getBook(1).getValue(TBook_TITLE()));
|
||||
testFailRefresh(r);
|
||||
|
||||
Cursor<Record> c1 = create.select().from(TBook().getName()).keepResultSet(CLOSE_AFTER_FETCH).fetchLazy();
|
||||
assertTrue(c1.closesAfterFetch());
|
||||
while (c1.hasNext()) {
|
||||
Result<Record> result = c1.fetch(1);
|
||||
assertNull(result.get(0).resultSet());
|
||||
assertNull(result.resultSet());
|
||||
assertNotNull(c1.resultSet());
|
||||
}
|
||||
|
||||
assertNull(c1.resultSet());
|
||||
}
|
||||
|
||||
private void testOriginalBook1(B book) {
|
||||
assertEquals(BOOK_TITLES.get(0), book.getValue(TBook_TITLE()));
|
||||
assertEquals(BOOK_AUTHOR_IDS.get(0), book.getValue(TBook_AUTHOR_ID()));
|
||||
assertFalse(book.changed());
|
||||
assertEquals(book.original(), book);
|
||||
assertNotNull(book.resultSet());
|
||||
}
|
||||
|
||||
private void testModifiedBook1(B book) {
|
||||
book.setValue(TBook_TITLE(), "XX");
|
||||
book.setValue(TBook_AUTHOR_ID(), 15);
|
||||
assertEquals("XX", book.getValue(TBook_TITLE()));
|
||||
assertEquals(15, (int) book.getValue(TBook_AUTHOR_ID()));
|
||||
assertTrue(book.changed());
|
||||
assertFalse(book.original().equals(book));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithKeepAfterFetch() throws Exception {
|
||||
switch (dialect()) {
|
||||
case SQLITE:
|
||||
log.info("SKIPPING", "KeepResultSet tests");
|
||||
return;
|
||||
}
|
||||
|
||||
DSLContext create = create(new NoStatementAfterCRUDListener(Select.class, "refresh"));
|
||||
Result<B> b2 = create.selectFrom(TBook()).keepResultSet(KEEP_AFTER_FETCH).fetch();
|
||||
B r = b2.get(0);
|
||||
assertNotNull(b2.resultSet());
|
||||
assertNotNull(r.resultSet());
|
||||
testFailUpdateRow(b2.resultSet());
|
||||
|
||||
testModifiedBook1(r);
|
||||
B dbBook = getBook(1);
|
||||
assertEquals(BOOK_TITLES.get(0), dbBook.getValue(TBook_TITLE()));
|
||||
assertEquals(BOOK_AUTHOR_IDS.get(0), dbBook.getValue(TBook_AUTHOR_ID()));
|
||||
|
||||
// Refresh the record
|
||||
r.refresh(TBook_TITLE());
|
||||
assertEquals(BOOK_TITLES.get(0), r.getValue(TBook_TITLE()));
|
||||
assertEquals(15, (int) r.getValue(TBook_AUTHOR_ID()));
|
||||
assertTrue(r.changed());
|
||||
assertFalse(r.original().equals(r));
|
||||
|
||||
r.refresh();
|
||||
testOriginalBook1(r);
|
||||
|
||||
b2.close();
|
||||
assertNull(b2.resultSet());
|
||||
|
||||
// Changing a TITLE still has no effect
|
||||
testModifiedBook1(r);
|
||||
|
||||
// But refreshing should work through a new query (UpdatableRecord)
|
||||
// For this, remove the NoSelectAfterRefreshListener
|
||||
r.attach(create().configuration());
|
||||
r.refresh();
|
||||
testOriginalBook1(r);
|
||||
|
||||
// Further refreshing should again not trigger new SQL statements
|
||||
r.attach(create.configuration());
|
||||
testModifiedBook1(r);
|
||||
|
||||
r.refresh();
|
||||
testOriginalBook1(r);
|
||||
r.close();
|
||||
|
||||
Cursor<Record> c1 = create.select().from(TBook().getName()).keepResultSet(KEEP_AFTER_FETCH).fetchLazy();
|
||||
assertFalse(c1.closesAfterFetch());
|
||||
while (c1.hasNext()) {
|
||||
Result<Record> result = c1.fetch(1);
|
||||
assertNotNull(result.get(0).resultSet());
|
||||
assertNotNull(result.resultSet());
|
||||
assertNotNull(c1.resultSet());
|
||||
}
|
||||
|
||||
assertNotNull(c1.resultSet());
|
||||
c1.close();
|
||||
assertNull(c1.resultSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChange() throws Exception {
|
||||
switch (dialect()) {
|
||||
// There's a critical bug in Derby. When this test is executed, the
|
||||
// connection is killed
|
||||
// https://issues.apache.org/jira/browse/DERBY-6228
|
||||
case DERBY:
|
||||
case SQLITE:
|
||||
log.info("SKIPPING", "KeepResultSet tests");
|
||||
return;
|
||||
}
|
||||
|
||||
jOOQAbstractTest.reset = false;
|
||||
DSLContext create = create(new NoStatementAfterCRUDListener(Select.class, "refresh"));
|
||||
|
||||
// Use plain SQL to prevent fetching of UpdatableRecord
|
||||
Result<B> books =
|
||||
create.selectFrom(TBook())
|
||||
.orderBy(TBook_ID())
|
||||
.keepResultSet(UPDATE_ON_CHANGE)
|
||||
.fetch();
|
||||
|
||||
assertNotNull(books.resultSet());
|
||||
for (int i = 0; i < books.size(); i++) {
|
||||
assertNotNull(books.get(i).resultSet());
|
||||
books.get(i).setValue(TBook_TITLE(), "Title " + i);
|
||||
}
|
||||
|
||||
Result<B> booksTest = getBooks();
|
||||
assertEquals(
|
||||
asList("Title 0", "Title 1", "Title 2", "Title 3"),
|
||||
booksTest.getValues(TBook_TITLE()));
|
||||
|
||||
// After closing, setting values to records should no longer have any
|
||||
// effect
|
||||
assertNotNull(books.resultSet());
|
||||
books.close();
|
||||
assertNull(books.resultSet());
|
||||
books.get(0).setValue(TBook_TITLE(), "XX");
|
||||
assertEquals("Title 0", getBook(1).getValue(TBook_TITLE()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChangeLazy() throws Exception {
|
||||
switch (dialect()) {
|
||||
// There's a critical bug in Derby. When this test is executed, the
|
||||
// connection is killed
|
||||
// https://issues.apache.org/jira/browse/DERBY-6228
|
||||
case DERBY:
|
||||
case SQLITE:
|
||||
log.info("SKIPPING", "KeepResultSet tests");
|
||||
return;
|
||||
}
|
||||
|
||||
jOOQAbstractTest.reset = false;
|
||||
DSLContext create = create(new NoStatementAfterCRUDListener(Select.class, "refresh"));
|
||||
|
||||
Cursor<B> books =
|
||||
create.selectFrom(TBook())
|
||||
.orderBy(TBook_ID())
|
||||
.keepResultSet(UPDATE_ON_CHANGE)
|
||||
.fetchLazy();
|
||||
|
||||
assertNotNull(books.resultSet());
|
||||
assertFalse(books.closesAfterFetch());
|
||||
while (books.hasNext()) {
|
||||
B book = books.fetchOne();
|
||||
assertNotNull(book.resultSet());
|
||||
book.setValue(TBook_TITLE(), "Title X");
|
||||
}
|
||||
|
||||
Result<B> booksTest = getBooks();
|
||||
assertEquals(
|
||||
Collections.nCopies(4, "Title X"),
|
||||
booksTest.getValues(TBook_TITLE()));
|
||||
|
||||
// After closing, setting values to records should no longer have any
|
||||
// effect
|
||||
assertNotNull(books.resultSet());
|
||||
assertFalse(books.isClosed());
|
||||
books.close();
|
||||
assertNull(books.resultSet());
|
||||
assertTrue(books.isClosed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChangeFetchOne() throws Exception {
|
||||
switch (dialect()) {
|
||||
case SQLITE:
|
||||
log.info("SKIPPING", "KeepResultSet tests");
|
||||
return;
|
||||
}
|
||||
|
||||
jOOQAbstractTest.reset = false;
|
||||
|
||||
DSLContext create = create(new NoStatementAfterCRUDListener(Select.class, "refresh"));
|
||||
Record book =
|
||||
create.select()
|
||||
.from(TBook().getName())
|
||||
.where(TBook_ID().eq(1))
|
||||
.keepResultSet(UPDATE_ON_CHANGE)
|
||||
.fetchOne();
|
||||
|
||||
assertNotNull(book.resultSet());
|
||||
assertFalse(book.changed());
|
||||
assertEquals(book, book.original());
|
||||
|
||||
book.setValue(TBook_AUTHOR_ID(), 2);
|
||||
assertEquals(2, (int) book.getValue(TBook_AUTHOR_ID()));
|
||||
assertFalse(book.changed());
|
||||
assertEquals(book, book.original());
|
||||
|
||||
book.refresh();
|
||||
assertEquals(2, (int) book.getValue(TBook_AUTHOR_ID()));
|
||||
assertFalse(book.changed());
|
||||
assertEquals(book, book.original());
|
||||
|
||||
try {
|
||||
book.setValue(TBook_AUTHOR_ID(), -1);
|
||||
fail();
|
||||
}
|
||||
catch (DataAccessException expected) {}
|
||||
|
||||
assertEquals(2, (int) book.getValue(TBook_AUTHOR_ID()));
|
||||
assertFalse(book.changed());
|
||||
assertEquals(book, book.original());
|
||||
|
||||
book.close();
|
||||
assertNull(book.resultSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChangeDelete() throws Exception {
|
||||
switch (dialect()) {
|
||||
// There's a critical bug in Derby. When this test is executed, the
|
||||
// connection is killed
|
||||
// https://issues.apache.org/jira/browse/DERBY-6228
|
||||
case DERBY:
|
||||
case SQLITE:
|
||||
log.info("SKIPPING", "KeepResultSet tests");
|
||||
return;
|
||||
}
|
||||
|
||||
jOOQAbstractTest.reset = false;
|
||||
|
||||
DSLContext create = create(new NoStatementAfterCRUDListener(Delete.class, "delete"));
|
||||
Result<B> books =
|
||||
create.selectFrom(TBook())
|
||||
.orderBy(TBook_ID())
|
||||
.keepResultSet(UPDATE_ON_CHANGE)
|
||||
.fetch();
|
||||
|
||||
B b2 = books.get(1);
|
||||
assertEquals(1, b2.delete());
|
||||
ResultSet rs = b2.resultSet();
|
||||
rs.beforeFirst();
|
||||
assertTrue(rs.next());
|
||||
assertTrue(rs.next());
|
||||
assertTrue(rs.next());
|
||||
assertFalse(rs.next());
|
||||
|
||||
System.out.println(books);
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: More tests:
|
||||
* -----------------
|
||||
*
|
||||
* [#2265] Pull up store(), delete(), refresh() from UpdatableRecord
|
||||
* - store() will perform a scan and update if UPDATE_ON_STORE is set. Otherwise: no-op
|
||||
* - delete() will remove the record
|
||||
*
|
||||
* [#1846] Add ResultQuery.keepResultSet() with UPDATE_ON_CHANGE
|
||||
* - Implement all data types from ResultSet.updateXXX() (e.g. updateInt(), etc)
|
||||
* - Implement UPDATE_ON_STORE
|
||||
*/
|
||||
}
|
||||
@ -121,7 +121,6 @@ import org.jooq.test._.testcases.GroupByTests;
|
||||
import org.jooq.test._.testcases.InsertUpdateTests;
|
||||
import org.jooq.test._.testcases.JDBCTests;
|
||||
import org.jooq.test._.testcases.JoinTests;
|
||||
import org.jooq.test._.testcases.KeepResultSetTests;
|
||||
import org.jooq.test._.testcases.LoaderTests;
|
||||
import org.jooq.test._.testcases.MetaDataTests;
|
||||
import org.jooq.test._.testcases.OrderByTests;
|
||||
@ -2225,36 +2224,6 @@ public abstract class jOOQAbstractTest<
|
||||
new BenchmarkTests(this).testBenchmarkSelect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithCloseAfterFetch() throws Exception {
|
||||
new KeepResultSetTests(this).testKeepRSWithCloseAfterFetch();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithKeepAfterFetch() throws Exception {
|
||||
new KeepResultSetTests(this).testKeepRSWithKeepAfterFetch();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChange() throws Exception {
|
||||
new KeepResultSetTests(this).testKeepRSWithUpdateOnChange();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChangeLazy() throws Exception {
|
||||
new KeepResultSetTests(this).testKeepRSWithUpdateOnChangeLazy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChangeFetchOne() throws Exception {
|
||||
new KeepResultSetTests(this).testKeepRSWithUpdateOnChangeFetchOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepRSWithUpdateOnChangeDelete() throws Exception {
|
||||
new KeepResultSetTests(this).testKeepRSWithUpdateOnChangeDelete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeepStatement() throws Exception {
|
||||
new StatementTests(this).testKeepStatement();
|
||||
|
||||
@ -275,15 +275,6 @@ public interface Cursor<R extends Record> extends Iterable<R> {
|
||||
*/
|
||||
<Z extends Record> Result<Z> fetchInto(Table<Z> table) throws DataAccessException, MappingException;
|
||||
|
||||
/**
|
||||
* Whether this <code>Cursor</code> closes itself after fetching all data.
|
||||
* <p>
|
||||
* By default, a <code>Cursor</code> will close itself after fetching all
|
||||
* data. This behaviour can be overridden by
|
||||
* {@link ResultQuery#keepResultSet(KeepResultSetMode)}, though.
|
||||
*/
|
||||
boolean closesAfterFetch();
|
||||
|
||||
/**
|
||||
* Explicitly close the underlying {@link PreparedStatement} and
|
||||
* {@link ResultSet}.
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
/**
|
||||
* 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 java.sql.ResultSet;
|
||||
|
||||
/**
|
||||
* A {@link ResultQuery}'s execution mode with respect to keeping open JDBC
|
||||
* {@link ResultSet} references after fetching.
|
||||
* <p>
|
||||
* This mode type is used with
|
||||
* {@link ResultQuery#keepResultSet(KeepResultSetMode)} to indicate how to deal
|
||||
* with JDBC {@link ResultSet} references after fetching them into jOOQ
|
||||
* {@link Result} objects.
|
||||
* <p>
|
||||
* See the various modes for details.
|
||||
*
|
||||
* @author Lukas Eder
|
||||
*/
|
||||
public enum KeepResultSetMode {
|
||||
|
||||
/**
|
||||
* Close the JDBC {@link ResultSet} after consuming it (this is the
|
||||
* default).
|
||||
* <p>
|
||||
* If not explicitly overridden by
|
||||
* {@link ResultQuery#resultSetConcurrency(int)} or
|
||||
* {@link ResultQuery#resultSetType(int)}, this will apply
|
||||
* {@link ResultSet#CONCUR_READ_ONLY} and
|
||||
* {@link ResultSet#TYPE_FORWARD_ONLY}.
|
||||
*/
|
||||
CLOSE_AFTER_FETCH,
|
||||
|
||||
/**
|
||||
* Keep the JDBC {@link ResultSet} after consuming it.
|
||||
* <p>
|
||||
* If not explicitly overridden by
|
||||
* {@link ResultQuery#resultSetConcurrency(int)} or
|
||||
* {@link ResultQuery#resultSetType(int)}, this will apply
|
||||
* {@link ResultSet#CONCUR_READ_ONLY} and
|
||||
* {@link ResultSet#TYPE_SCROLL_SENSITIVE} (allowing for calls to
|
||||
* {@link Record#refresh()} and {@link Result#refresh()}).
|
||||
* <p>
|
||||
* Client code must assure that the {@link ResultSet} is closed explicitly
|
||||
* to free database resources. Closing can be done through any of these
|
||||
* methods:
|
||||
* <ul>
|
||||
* <li>{@link ResultSet#close()}</li>
|
||||
* <li>{@link Result#close()}</li>
|
||||
* <li>{@link Cursor#close()}</li>
|
||||
* </ul>
|
||||
*/
|
||||
KEEP_AFTER_FETCH,
|
||||
|
||||
/**
|
||||
* Keep the JDBC {@link ResultSet} after consuming it, updating the
|
||||
* <code>ResultSet</code> at every change of a {@link Record}.
|
||||
* <p>
|
||||
* If not explicitly overridden by
|
||||
* {@link ResultQuery#resultSetConcurrency(int)} or
|
||||
* {@link ResultQuery#resultSetType(int)}, this will apply
|
||||
* {@link ResultSet#CONCUR_UPDATABLE} and
|
||||
* {@link ResultSet#TYPE_SCROLL_SENSITIVE}.
|
||||
* <p>
|
||||
* TODO: More details here
|
||||
* <p>
|
||||
* Client code must assure that the {@link ResultSet} is closed explicitly
|
||||
* to free database resources. Closing can be done through any of these
|
||||
* methods:
|
||||
* <ul>
|
||||
* <li>{@link ResultSet#close()}</li>
|
||||
* <li>{@link Result#close()}</li>
|
||||
* <li>{@link Cursor#close()}</li>
|
||||
* </ul>
|
||||
*/
|
||||
UPDATE_ON_CHANGE,
|
||||
|
||||
/**
|
||||
* Keep the JDBC {@link ResultSet} after consuming it, updating the
|
||||
* <code>ResultSet</code> at every call to {@link Record#store()}, or
|
||||
* {@link Result#store()} (<strong>This is not yet supported</strong>).
|
||||
* <p>
|
||||
* If not explicitly overridden by
|
||||
* {@link ResultQuery#resultSetConcurrency(int)} or
|
||||
* {@link ResultQuery#resultSetType(int)}, this will apply
|
||||
* {@link ResultSet#CONCUR_UPDATABLE} and
|
||||
* {@link ResultSet#TYPE_SCROLL_SENSITIVE}.
|
||||
* <p>
|
||||
* TODO: More details here
|
||||
* <p>
|
||||
* Client code must assure that the {@link ResultSet} is closed explicitly
|
||||
* to free database resources. Closing can be done through any of these
|
||||
* methods:
|
||||
* <ul>
|
||||
* <li>{@link ResultSet#close()}</li>
|
||||
* <li>{@link Result#close()}</li>
|
||||
* <li>{@link Cursor#close()}</li>
|
||||
* </ul>
|
||||
*/
|
||||
UPDATE_ON_STORE
|
||||
}
|
||||
@ -48,7 +48,6 @@ import java.util.Map;
|
||||
|
||||
import javax.persistence.Column;
|
||||
|
||||
import org.jooq.exception.DataAccessException;
|
||||
import org.jooq.exception.DataTypeException;
|
||||
import org.jooq.exception.MappingException;
|
||||
import org.jooq.tools.Convert;
|
||||
@ -1038,108 +1037,4 @@ public interface Record extends Attachable, Comparable<Record> {
|
||||
@Override
|
||||
int compareTo(Record record);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods related to the underlying ResultSet (if applicable)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Refresh this record from the database.
|
||||
* <p>
|
||||
* A successful refresh results in the following:
|
||||
* <ul>
|
||||
* <li>{@link #valuesRow()} will have been restored to the respective values
|
||||
* from the database</li>
|
||||
* <li>{@link #original()} will match this record</li>
|
||||
* <li>{@link #changed()} will be <code>false</code></li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Refreshing can trigger any of the following actions:
|
||||
* <ul>
|
||||
* <li>Re-reading the underlying {@link #resultSet()}, if that
|
||||
* <code>ResultSet</code> is available.</li>
|
||||
* <li>Executing a new <code>SELECT</code> statement, if this is an
|
||||
* {@link UpdatableRecord}.</li>
|
||||
* <li>Failing, otherwise</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This is the same as calling <code>record.refresh(record.fields())</code>
|
||||
*
|
||||
* @throws DataAccessException This exception is thrown if
|
||||
* <ul>
|
||||
* <li>the underlying {@link #resultSet()} is in
|
||||
* {@link ResultSet#TYPE_FORWARD_ONLY} mode, such that
|
||||
* refreshing is not possible.</li> <li>something went wrong
|
||||
* executing the refresh <code>SELECT</code> statement, if this
|
||||
* is an {@link UpdatableRecord}.</li> <li>the record does not
|
||||
* exist anymore in the database</li>
|
||||
* </ul>
|
||||
* @see UpdatableRecord#refresh()
|
||||
* @see ResultQuery#keepResultSet(KeepResultSetMode)
|
||||
*/
|
||||
void refresh() throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Refresh parts of this record from the database.
|
||||
* <p>
|
||||
* A successful refresh results in the following:
|
||||
* <ul>
|
||||
* <li>{@link #valuesRow()} will have been restored to the respective values
|
||||
* from the database</li>
|
||||
* <li>{@link #original()} will match this record</li>
|
||||
* <li>{@link #changed()} will be <code>false</code></li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Refreshing can trigger any of the following actions:
|
||||
* <ul>
|
||||
* <li>Re-reading the underlying {@link #resultSet()}, if that
|
||||
* <code>ResultSet</code> is available.</li>
|
||||
* <li>Executing a new <code>SELECT</code> statement, if this is an
|
||||
* {@link UpdatableRecord}.</li>
|
||||
* <li>Failing, otherwise</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This is the same as calling <code>record.refresh(record.fields())</code>
|
||||
*
|
||||
* @throws DataAccessException This exception is thrown if
|
||||
* <ul>
|
||||
* <li>the underlying {@link #resultSet()} is in
|
||||
* {@link ResultSet#TYPE_FORWARD_ONLY} mode, such that
|
||||
* refreshing is not possible.</li> <li>something went wrong
|
||||
* executing the refresh <code>SELECT</code> statement, if this
|
||||
* is an {@link UpdatableRecord}.</li> <li>the record does not
|
||||
* exist anymore in the database</li>
|
||||
* </ul>
|
||||
* @see UpdatableRecord#refresh()
|
||||
* @see ResultQuery#keepResultSet(KeepResultSetMode)
|
||||
*/
|
||||
void refresh(Field<?>... fields) throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Close the underlying JDBC {@link ResultSet}, if applicable.
|
||||
* <p>
|
||||
* If this <code>Record</code> was created using
|
||||
* {@link ResultQuery#keepResultSet(KeepResultSetMode)}, then it closes the
|
||||
* underlying JDBC {@link ResultSet}. Otherwise, this method has no effect.
|
||||
* <p>
|
||||
* Note, this will close the <code>ResultSet</code> for all records that
|
||||
* were fetched from the same <code>ResultQuery</code>.
|
||||
*
|
||||
* @throws DataAccessException If something went wrong closing the
|
||||
* underlying {@link ResultSet}
|
||||
* @see #resultSet()
|
||||
*/
|
||||
void close() throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Get the underlying JDBC {@link ResultSet}, if applicable.
|
||||
* <p>
|
||||
* This method returns the underlying JDBC {@link ResultSet}, if this
|
||||
* <code>Record</code> was created using
|
||||
* {@link ResultQuery#keepResultSet(KeepResultSetMode)}. Otherwise, this
|
||||
* method returns <code>null</code>.
|
||||
*
|
||||
* @return The underlying JDBC <code>ResultSet</code>, or <code>null</code>
|
||||
*/
|
||||
ResultSet resultSet();
|
||||
|
||||
}
|
||||
|
||||
@ -41,7 +41,6 @@ import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jooq.exception.DataAccessException;
|
||||
import org.jooq.exception.DataTypeException;
|
||||
import org.jooq.exception.InvalidResultException;
|
||||
import org.jooq.exception.MappingException;
|
||||
@ -1082,33 +1081,4 @@ public interface Result<R extends Record> extends List<R>, Attachable {
|
||||
* @see String#intern()
|
||||
*/
|
||||
Result<R> intern(String... fieldNames);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Methods related to the underlying ResultSet (if applicable)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Close the underlying JDBC {@link ResultSet}, if applicable.
|
||||
* <p>
|
||||
* If this <code>Result</code> was created using
|
||||
* {@link ResultQuery#keepResultSet(KeepResultSetMode)}, then it closes the
|
||||
* underlying JDBC {@link ResultSet}. Otherwise, this method has no effect.
|
||||
*
|
||||
* @throws DataAccessException If something went wrong closing the
|
||||
* underlying {@link ResultSet}
|
||||
* @see #resultSet()
|
||||
*/
|
||||
void close() throws DataAccessException;
|
||||
|
||||
/**
|
||||
* Get the underlying JDBC {@link ResultSet}, if applicable.
|
||||
* <p>
|
||||
* This method returns the underlying JDBC {@link ResultSet}, if this
|
||||
* <code>Result</code> was created using
|
||||
* {@link ResultQuery#keepResultSet(KeepResultSetMode)}. Otherwise, this
|
||||
* method returns <code>null</code>.
|
||||
*
|
||||
* @return The underlying JDBC <code>ResultSet</code>, or <code>null</code>
|
||||
*/
|
||||
ResultSet resultSet();
|
||||
}
|
||||
|
||||
@ -991,18 +991,6 @@ public interface ResultQuery<R extends Record> extends Query {
|
||||
@Override
|
||||
ResultQuery<R> keepStatement(boolean keepStatement);
|
||||
|
||||
/**
|
||||
* Indicate how to deal with the JDBC {@link ResultSet} when fetching data
|
||||
* into jOOQ {@link Result} or {@link Cursor} objects.
|
||||
* <p>
|
||||
* TODO: More info here.
|
||||
* <p>
|
||||
* <strong>Note:</strong> If JDBC <code>ResultSet</code> references are kept
|
||||
* open after fetching data through jOOQ, you must explicitly close them
|
||||
* using either {@link Result#close()}, or {@link Cursor#close()}
|
||||
*/
|
||||
ResultQuery<R> keepResultSet(KeepResultSetMode mode);
|
||||
|
||||
/**
|
||||
* Specify the maximum number of rows returned by the underlying
|
||||
* {@link Statement}.
|
||||
|
||||
@ -301,15 +301,63 @@ public interface UpdatableRecord<R extends UpdatableRecord<R>> extends TableReco
|
||||
int delete() throws DataAccessException, DataChangedException;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Refresh this record from the database.
|
||||
* <p>
|
||||
* A successful refresh results in the following:
|
||||
* <ul>
|
||||
* <li>{@link #valuesRow()} will have been restored to the respective values
|
||||
* from the database</li>
|
||||
* <li>{@link #original()} will match this record</li>
|
||||
* <li>{@link #changed()} will be <code>false</code></li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Refreshing can trigger any of the following actions:
|
||||
* <ul>
|
||||
* <li>Executing a new <code>SELECT</code> statement, if this is an
|
||||
* {@link UpdatableRecord}.</li>
|
||||
* <li>Failing, otherwise</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This is the same as calling <code>record.refresh(record.fields())</code>
|
||||
*
|
||||
* @throws DataAccessException This exception is thrown if
|
||||
* <ul>
|
||||
* <li>something went wrong executing the refresh <code>SELECT
|
||||
* </code> statement, if this is an {@link UpdatableRecord}.
|
||||
* </li> <li>the record does not exist anymore in the database
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
void refresh() throws DataAccessException;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* Refresh parts of this record from the database.
|
||||
* <p>
|
||||
* A successful refresh results in the following:
|
||||
* <ul>
|
||||
* <li>{@link #valuesRow()} will have been restored to the respective values
|
||||
* from the database</li>
|
||||
* <li>{@link #original()} will match this record</li>
|
||||
* <li>{@link #changed()} will be <code>false</code></li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Refreshing can trigger any of the following actions:
|
||||
* <ul>
|
||||
* <li>Executing a new <code>SELECT</code> statement, if this is an
|
||||
* {@link UpdatableRecord}.</li>
|
||||
* <li>Failing, otherwise</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This is the same as calling <code>record.refresh(record.fields())</code>
|
||||
*
|
||||
* @throws DataAccessException This exception is thrown if
|
||||
* <ul>
|
||||
* <li>something went wrong
|
||||
* executing the refresh <code>SELECT</code> statement, if this
|
||||
* is an {@link UpdatableRecord}.</li> <li>the record does not
|
||||
* exist anymore in the database</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
void refresh(Field<?>... fields) throws DataAccessException;
|
||||
|
||||
/**
|
||||
|
||||
@ -37,17 +37,14 @@
|
||||
package org.jooq.impl;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.jooq.KeepResultSetMode.UPDATE_ON_CHANGE;
|
||||
import static org.jooq.impl.Utils.getAnnotatedGetter;
|
||||
import static org.jooq.impl.Utils.getAnnotatedMembers;
|
||||
import static org.jooq.impl.Utils.getMatchingGetter;
|
||||
import static org.jooq.impl.Utils.getMatchingMembers;
|
||||
import static org.jooq.impl.Utils.hasColumnAnnotations;
|
||||
import static org.jooq.impl.Utils.translate;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
@ -57,16 +54,13 @@ import java.util.Map;
|
||||
import org.jooq.Attachable;
|
||||
import org.jooq.Converter;
|
||||
import org.jooq.Field;
|
||||
import org.jooq.KeepResultSetMode;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.RecordMapper;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.Table;
|
||||
import org.jooq.UniqueKey;
|
||||
import org.jooq.exception.DataAccessException;
|
||||
import org.jooq.exception.InvalidResultException;
|
||||
import org.jooq.exception.MappingException;
|
||||
import org.jooq.impl.CursorImpl.CursorResultSet;
|
||||
import org.jooq.tools.Convert;
|
||||
|
||||
/**
|
||||
@ -84,9 +78,6 @@ abstract class AbstractRecord extends AbstractStore implements Record {
|
||||
|
||||
final RowImpl fields;
|
||||
final Value<?>[] values;
|
||||
transient KeepResultSetMode keepResultSetMode;
|
||||
transient CursorResultSet rs;
|
||||
transient int rsIndex;
|
||||
|
||||
AbstractRecord(Collection<? extends Field<?>> fields) {
|
||||
this(new RowImpl(fields));
|
||||
@ -276,53 +267,26 @@ abstract class AbstractRecord extends AbstractStore implements Record {
|
||||
@Override
|
||||
public final <T> void setValue(Field<T> field, T value) {
|
||||
Value<T> val = getValue0(field);
|
||||
UniqueKey<?> key = getPrimaryKey();
|
||||
|
||||
// [#1846] Execute this first to fail early, when UPDATE_ON_CHANGE fails
|
||||
if (rs != null && keepResultSetMode == UPDATE_ON_CHANGE) {
|
||||
int index = fieldsRow().indexOf(field);
|
||||
int columnIndex = index + 1;
|
||||
|
||||
try {
|
||||
if (rs.getRow() != rsIndex) {
|
||||
rs.absolute(rsIndex);
|
||||
}
|
||||
|
||||
// [#1846] TODO: Add more typesafety here
|
||||
rs.updateObject(columnIndex, value);
|
||||
|
||||
// [#1846] TODO: Update only in case of KeepResultSetMode.UPDATE_ON_CHANGE
|
||||
rs.updateRow();
|
||||
}
|
||||
catch (SQLException e) {
|
||||
throw translate("Error when updating ResultSet", e);
|
||||
}
|
||||
|
||||
setValue(index, new Value<Object>(value));
|
||||
// Normal fields' changed flag is always set to true
|
||||
if (key == null || !key.getFields().contains(field)) {
|
||||
val.setValue(value);
|
||||
}
|
||||
|
||||
// [#1846] In all other cases, correctly handle changed flags
|
||||
// The primary key's changed flag might've been set previously
|
||||
else if (val.isChanged()) {
|
||||
val.setValue(value);
|
||||
}
|
||||
|
||||
// [#979] If the primary key is being changed, all other fields' flags
|
||||
// need to be set to true for in case this record is stored again, an
|
||||
// INSERT statement will thus be issued
|
||||
else {
|
||||
UniqueKey<?> key = getPrimaryKey();
|
||||
val.setValue(value, true);
|
||||
|
||||
// Normal fields' changed flag is always set to true
|
||||
if (key == null || !key.getFields().contains(field)) {
|
||||
val.setValue(value);
|
||||
}
|
||||
|
||||
// The primary key's changed flag might've been set previously
|
||||
else if (val.isChanged()) {
|
||||
val.setValue(value);
|
||||
}
|
||||
|
||||
// [#979] If the primary key is being changed, all other fields' flags
|
||||
// need to be set to true for in case this record is stored again, an
|
||||
// INSERT statement will thus be issued
|
||||
else {
|
||||
val.setValue(value, true);
|
||||
|
||||
if (val.isChanged()) {
|
||||
changed(true);
|
||||
}
|
||||
if (val.isChanged()) {
|
||||
changed(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -553,7 +517,7 @@ abstract class AbstractRecord extends AbstractStore implements Record {
|
||||
|
||||
@Override
|
||||
public final ResultSet intoResultSet() {
|
||||
ResultImpl<Record> result = new ResultImpl<Record>(configuration(), rs, fields.fields.fields);
|
||||
ResultImpl<Record> result = new ResultImpl<Record>(configuration(), fields.fields.fields);
|
||||
result.add(this);
|
||||
return result.intoResultSet();
|
||||
}
|
||||
@ -690,68 +654,13 @@ abstract class AbstractRecord extends AbstractStore implements Record {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// XXX: Methods related to the underlying ResultSet (if applicable)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public final void refresh() {
|
||||
refresh(fields.fields.fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* <p>
|
||||
* Subclasses may override this
|
||||
*/
|
||||
@Override
|
||||
public void refresh(Field<?>... f) {
|
||||
checkRsAvailable("Cannot refresh record. No ResultSet available");
|
||||
|
||||
try {
|
||||
|
||||
// [#2265] TODO: This code is prototypical. fetchLazy() is not
|
||||
// the best way to fetch a record
|
||||
rs.absolute(rsIndex - 1);
|
||||
AbstractRecord record = (AbstractRecord) create().fetchLazy(rs).fetchOne();
|
||||
setValues(f, record);
|
||||
}
|
||||
catch (SQLException e) {
|
||||
throw translate("Cannot refresh record", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final void checkRsAvailable(String message) {
|
||||
if (rs == null) {
|
||||
throw new DataAccessException(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
try {
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
rs = null;
|
||||
}
|
||||
}
|
||||
catch (SQLException e) {
|
||||
throw translate("Cannot close ResultSet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ResultSet resultSet() {
|
||||
return rs;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// XXX: Object and Comparable API
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
Result<AbstractRecord> result = new ResultImpl<AbstractRecord>(configuration(), null, fields.fields.fields);
|
||||
Result<AbstractRecord> result = new ResultImpl<AbstractRecord>(configuration(), fields.fields.fields);
|
||||
result.add(this);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@ -35,15 +35,10 @@
|
||||
*/
|
||||
package org.jooq.impl;
|
||||
|
||||
import static java.sql.ResultSet.CONCUR_READ_ONLY;
|
||||
import static java.sql.ResultSet.CONCUR_UPDATABLE;
|
||||
import static java.sql.ResultSet.TYPE_SCROLL_SENSITIVE;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.concurrent.Executors.newSingleThreadExecutor;
|
||||
import static org.jooq.KeepResultSetMode.CLOSE_AFTER_FETCH;
|
||||
import static org.jooq.KeepResultSetMode.KEEP_AFTER_FETCH;
|
||||
import static org.jooq.KeepResultSetMode.UPDATE_ON_CHANGE;
|
||||
import static org.jooq.KeepResultSetMode.UPDATE_ON_STORE;
|
||||
import static org.jooq.SQLDialect.ASE;
|
||||
import static org.jooq.SQLDialect.CUBRID;
|
||||
import static org.jooq.SQLDialect.SQLSERVER;
|
||||
@ -67,7 +62,6 @@ import org.jooq.ExecuteContext;
|
||||
import org.jooq.ExecuteListener;
|
||||
import org.jooq.Field;
|
||||
import org.jooq.FutureResult;
|
||||
import org.jooq.KeepResultSetMode;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.RecordHandler;
|
||||
import org.jooq.RecordMapper;
|
||||
@ -92,7 +86,6 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
private static final JooqLogger log = JooqLogger.getLogger(AbstractResultQuery.class);
|
||||
|
||||
private int maxRows;
|
||||
private KeepResultSetMode keepResultSetMode;
|
||||
private int resultSetConcurrency;
|
||||
private int resultSetType;
|
||||
private int resultSetHoldability;
|
||||
@ -141,15 +134,6 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
return (ResultQuery<R>) super.keepStatement(k);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ResultQuery<R> keepResultSet(KeepResultSetMode mode) {
|
||||
if (mode == UPDATE_ON_STORE)
|
||||
throw new UnsupportedOperationException("UPDATE_ON_STORE is not yet supported");
|
||||
|
||||
this.keepResultSetMode = mode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ResultQuery<R> maxRows(int rows) {
|
||||
this.maxRows = rows;
|
||||
@ -224,17 +208,6 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
}
|
||||
}
|
||||
|
||||
// [#1846] When scrollable Results are fetched
|
||||
else if (keepResultSetMode == KEEP_AFTER_FETCH) {
|
||||
ctx.statement(ctx.connection().prepareStatement(ctx.sql(), TYPE_SCROLL_SENSITIVE, CONCUR_READ_ONLY));
|
||||
}
|
||||
|
||||
// [#1846] When scrollable and updatable Results are fetched
|
||||
else if (keepResultSetMode == UPDATE_ON_CHANGE ||
|
||||
keepResultSetMode == UPDATE_ON_STORE) {
|
||||
ctx.statement(ctx.connection().prepareStatement(ctx.sql(), TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE));
|
||||
}
|
||||
|
||||
// [#1296] These dialects do not implement FOR UPDATE. But the same
|
||||
// effect can be achieved using ResultSet.CONCUR_UPDATABLE
|
||||
else if (isForUpdate() && asList(CUBRID, SQLSERVER).contains(ctx.configuration().dialect().family())) {
|
||||
@ -284,7 +257,7 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
if (!many) {
|
||||
if (ctx.resultSet() != null) {
|
||||
Field<?>[] fields = getFields(ctx.resultSet().getMetaData());
|
||||
cursor = new CursorImpl<R>(ctx, listener, fields, internIndexes(fields), keepStatement(), keepResultSet(), keepResultSetMode, getRecordType());
|
||||
cursor = new CursorImpl<R>(ctx, listener, fields, internIndexes(fields), keepStatement(), keepResultSet(), getRecordType());
|
||||
|
||||
if (!lazy) {
|
||||
result = cursor.fetch();
|
||||
@ -292,7 +265,7 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
}
|
||||
}
|
||||
else {
|
||||
result = new ResultImpl<R>(ctx.configuration(), null);
|
||||
result = new ResultImpl<R>(ctx.configuration());
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,7 +278,7 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
anyResults = true;
|
||||
|
||||
Field<?>[] fields = new MetaDataFieldProvider(ctx.configuration(), ctx.resultSet().getMetaData()).getFields();
|
||||
Cursor<Record> c = new CursorImpl<Record>(ctx, listener, fields, internIndexes(fields), true, false, CLOSE_AFTER_FETCH);
|
||||
Cursor<Record> c = new CursorImpl<Record>(ctx, listener, fields, internIndexes(fields), true, false);
|
||||
results.add(c.fetch());
|
||||
|
||||
if (ctx.statement().getMoreResults()) {
|
||||
@ -328,10 +301,7 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
|
||||
@Override
|
||||
protected final boolean keepResultSet() {
|
||||
return lazy
|
||||
|| keepResultSetMode == KEEP_AFTER_FETCH
|
||||
|| keepResultSetMode == UPDATE_ON_CHANGE
|
||||
|| keepResultSetMode == UPDATE_ON_STORE;
|
||||
return lazy;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -496,9 +466,7 @@ abstract class AbstractResultQuery<R extends Record> extends AbstractQuery imple
|
||||
return c.fetchOne();
|
||||
}
|
||||
finally {
|
||||
if (c.closesAfterFetch()) {
|
||||
c.close();
|
||||
}
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,6 @@
|
||||
*/
|
||||
package org.jooq.impl;
|
||||
|
||||
import static org.jooq.KeepResultSetMode.CLOSE_AFTER_FETCH;
|
||||
import static org.jooq.impl.Utils.fieldArray;
|
||||
import static org.jooq.util.sqlite.SQLiteDSL.rowid;
|
||||
|
||||
@ -143,7 +142,7 @@ abstract class AbstractStoreQuery<R extends Record> extends AbstractQuery implem
|
||||
@Override
|
||||
public final Result<R> getReturnedRecords() {
|
||||
if (returned == null) {
|
||||
returned = new ResultImpl<R>(configuration(), null, returning);
|
||||
returned = new ResultImpl<R>(configuration(), returning);
|
||||
}
|
||||
|
||||
return returned;
|
||||
@ -345,7 +344,7 @@ abstract class AbstractStoreQuery<R extends Record> extends AbstractQuery implem
|
||||
ExecuteListener listener2 = new ExecuteListeners(ctx2);
|
||||
|
||||
ctx2.resultSet(rs);
|
||||
returned = new CursorImpl<R>(ctx2, listener2, fieldArray(returning), null, false, true, CLOSE_AFTER_FETCH).fetch().into(getInto());
|
||||
returned = new CursorImpl<R>(ctx2, listener2, fieldArray(returning), null, false, true).fetch().into(getInto());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@
|
||||
package org.jooq.impl;
|
||||
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static org.jooq.KeepResultSetMode.CLOSE_AFTER_FETCH;
|
||||
import static org.jooq.impl.Utils.DATA_LOCK_ROWS_FOR_UPDATE;
|
||||
|
||||
import java.io.InputStream;
|
||||
@ -67,7 +66,6 @@ import org.jooq.Cursor;
|
||||
import org.jooq.ExecuteContext;
|
||||
import org.jooq.ExecuteListener;
|
||||
import org.jooq.Field;
|
||||
import org.jooq.KeepResultSetMode;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.RecordHandler;
|
||||
import org.jooq.RecordMapper;
|
||||
@ -90,7 +88,6 @@ class CursorImpl<R extends Record> implements Cursor<R> {
|
||||
private final Field<?>[] fields;
|
||||
private final boolean[] intern;
|
||||
private final boolean keepResultSet;
|
||||
private final KeepResultSetMode keepResultSetMode;
|
||||
private final boolean keepStatement;
|
||||
private final Class<? extends R> type;
|
||||
private boolean isClosed;
|
||||
@ -100,18 +97,17 @@ class CursorImpl<R extends Record> implements Cursor<R> {
|
||||
private transient Iterator<R> iterator;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
CursorImpl(ExecuteContext ctx, ExecuteListener listener, Field<?>[] fields, int[] internIndexes, boolean keepStatement, boolean keepResultSet, KeepResultSetMode keepResultSetMode) {
|
||||
this(ctx, listener, fields, internIndexes, keepStatement, keepResultSet, keepResultSetMode, (Class<? extends R>) RecordImpl.class);
|
||||
CursorImpl(ExecuteContext ctx, ExecuteListener listener, Field<?>[] fields, int[] internIndexes, boolean keepStatement, boolean keepResultSet) {
|
||||
this(ctx, listener, fields, internIndexes, keepStatement, keepResultSet, (Class<? extends R>) RecordImpl.class);
|
||||
}
|
||||
|
||||
CursorImpl(ExecuteContext ctx, ExecuteListener listener, Field<?>[] fields, int[] internIndexes, boolean keepStatement, boolean keepResultSet, KeepResultSetMode keepResultSetMode, Class<? extends R> type) {
|
||||
CursorImpl(ExecuteContext ctx, ExecuteListener listener, Field<?>[] fields, int[] internIndexes, boolean keepStatement, boolean keepResultSet, Class<? extends R> type) {
|
||||
this.ctx = ctx;
|
||||
this.listener = (listener != null ? listener : new ExecuteListeners(ctx));
|
||||
this.fields = fields;
|
||||
this.type = type;
|
||||
this.keepStatement = keepStatement;
|
||||
this.keepResultSet = keepResultSet;
|
||||
this.keepResultSetMode = keepResultSetMode;
|
||||
this.rs = new CursorResultSet();
|
||||
this.intern = new boolean[fields.length];
|
||||
|
||||
@ -122,11 +118,6 @@ class CursorImpl<R extends Record> implements Cursor<R> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean closesAfterFetch() {
|
||||
return keepResultSetMode == null || keepResultSetMode == CLOSE_AFTER_FETCH;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@Override
|
||||
public final Row fieldsRow() {
|
||||
@ -190,7 +181,7 @@ class CursorImpl<R extends Record> implements Cursor<R> {
|
||||
// Before listener.resultStart(ctx)
|
||||
iterator();
|
||||
|
||||
ResultImpl<R> result = new ResultImpl<R>(ctx.configuration(), closesAfterFetch() ? null : rs, fields);
|
||||
ResultImpl<R> result = new ResultImpl<R>(ctx.configuration(), fields);
|
||||
R record = null;
|
||||
|
||||
ctx.result(result);
|
||||
@ -1402,14 +1393,6 @@ class CursorImpl<R extends Record> implements Cursor<R> {
|
||||
|
||||
record = (AbstractRecord) Utils.newRecord(type, fields, ctx.configuration());
|
||||
|
||||
// [#1846] Add a reference to the Cursor's ResultSet if
|
||||
// Updatable ResultSets are requested
|
||||
if (!closesAfterFetch()) {
|
||||
record.keepResultSetMode = keepResultSetMode;
|
||||
record.rs = rs;
|
||||
record.rsIndex = rsIndex;
|
||||
}
|
||||
|
||||
ctx.record(record);
|
||||
listener.recordStart(ctx);
|
||||
|
||||
@ -1434,9 +1417,7 @@ class CursorImpl<R extends Record> implements Cursor<R> {
|
||||
// [#1868] [#2373] [#2385] This calls through to Utils.safeClose()
|
||||
// if necessary, lazy-terminating the ExecuteListener lifecycle if
|
||||
// the result is not eager-fetched.
|
||||
// [#1846] When fetching updatable Results, do not close the
|
||||
// Cursor's ResultSet!
|
||||
if (record == null && closesAfterFetch()) {
|
||||
if (record == null) {
|
||||
CursorImpl.this.close();
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,6 @@
|
||||
|
||||
package org.jooq.impl;
|
||||
|
||||
import static org.jooq.KeepResultSetMode.CLOSE_AFTER_FETCH;
|
||||
import static org.jooq.conf.ParamType.INLINED;
|
||||
import static org.jooq.conf.ParamType.NAMED;
|
||||
import static org.jooq.impl.DSL.field;
|
||||
@ -515,7 +514,7 @@ public class DefaultDSLContext implements DSLContext, Serializable {
|
||||
ExecuteListener listener = new ExecuteListeners(ctx);
|
||||
|
||||
ctx.resultSet(rs);
|
||||
return new CursorImpl<Record>(ctx, listener, fields, null, false, true, CLOSE_AFTER_FETCH);
|
||||
return new CursorImpl<Record>(ctx, listener, fields, null, false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -585,7 +584,7 @@ public class DefaultDSLContext implements DSLContext, Serializable {
|
||||
@Override
|
||||
public Result<Record> fetchFromStringData(List<String[]> data) {
|
||||
if (data.size() == 0) {
|
||||
return new ResultImpl<Record>(configuration, null);
|
||||
return new ResultImpl<Record>(configuration);
|
||||
}
|
||||
else {
|
||||
List<Field<?>> fields = new ArrayList<Field<?>>();
|
||||
@ -594,7 +593,7 @@ public class DefaultDSLContext implements DSLContext, Serializable {
|
||||
fields.add(fieldByName(String.class, name));
|
||||
}
|
||||
|
||||
Result<Record> result = new ResultImpl<Record>(configuration, null, fields);
|
||||
Result<Record> result = new ResultImpl<Record>(configuration, fields);
|
||||
|
||||
if (data.size() > 1) {
|
||||
for (String[] values : data.subList(1, data.size())) {
|
||||
@ -1456,7 +1455,7 @@ public class DefaultDSLContext implements DSLContext, Serializable {
|
||||
|
||||
@Override
|
||||
public <R extends Record> Result<R> newResult(Table<R> table) {
|
||||
return new ResultImpl<R>(configuration, null, table.fields());
|
||||
return new ResultImpl<R>(configuration, table.fields());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -104,7 +104,7 @@ class ReferenceImpl<R extends Record, O extends Record> extends AbstractKey<R> i
|
||||
@Override
|
||||
public final Result<O> fetchParents(Collection<? extends R> records) {
|
||||
if (records == null || records.size() == 0) {
|
||||
return new ResultImpl<O>(new DefaultConfiguration(), null, key.getFields());
|
||||
return new ResultImpl<O>(new DefaultConfiguration(), key.getFields());
|
||||
}
|
||||
else {
|
||||
return fetch(records, key.getTable(), key.getFieldsArray(), getFieldsArray());
|
||||
@ -114,7 +114,7 @@ class ReferenceImpl<R extends Record, O extends Record> extends AbstractKey<R> i
|
||||
@Override
|
||||
public final Result<R> fetchChildren(Collection<? extends O> records) {
|
||||
if (records == null || records.size() == 0) {
|
||||
return new ResultImpl<R>(new DefaultConfiguration(), null, getFields());
|
||||
return new ResultImpl<R>(new DefaultConfiguration(), getFields());
|
||||
}
|
||||
else {
|
||||
return fetch(records, getTable(), getFieldsArray(), key.getFieldsArray());
|
||||
|
||||
@ -38,14 +38,12 @@ package org.jooq.impl;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static org.jooq.impl.Utils.translate;
|
||||
import static org.jooq.tools.StringUtils.abbreviate;
|
||||
import static org.jooq.tools.StringUtils.leftPad;
|
||||
import static org.jooq.tools.StringUtils.rightPad;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@ -97,22 +95,20 @@ class ResultImpl<R extends Record> implements Result<R>, AttachableInternal {
|
||||
private static final long serialVersionUID = 6416154375799578362L;
|
||||
|
||||
private Configuration configuration;
|
||||
private transient ResultSet rs;
|
||||
private final Fields fields;
|
||||
private final List<R> records;
|
||||
|
||||
ResultImpl(Configuration configuration, ResultSet rs, Collection<? extends Field<?>> fields) {
|
||||
this(configuration, rs, new Fields(fields));
|
||||
ResultImpl(Configuration configuration, Collection<? extends Field<?>> fields) {
|
||||
this(configuration, new Fields(fields));
|
||||
}
|
||||
|
||||
ResultImpl(Configuration configuration, ResultSet rs, Field<?>... fields) {
|
||||
this(configuration, rs, new Fields(fields));
|
||||
ResultImpl(Configuration configuration, Field<?>... fields) {
|
||||
this(configuration, new Fields(fields));
|
||||
}
|
||||
|
||||
ResultImpl(Configuration configuration, ResultSet rs, Fields fields) {
|
||||
ResultImpl(Configuration configuration, Fields fields) {
|
||||
this.configuration = configuration;
|
||||
this.fields = fields;
|
||||
this.rs = rs;
|
||||
this.records = new ArrayList<R>();
|
||||
}
|
||||
|
||||
@ -863,7 +859,7 @@ class ResultImpl<R extends Record> implements Result<R>, AttachableInternal {
|
||||
Result<R> result = map.get(val);
|
||||
|
||||
if (result == null) {
|
||||
result = new ResultImpl<R>(configuration, rs, fields);
|
||||
result = new ResultImpl<R>(configuration, fields);
|
||||
map.put(val, result);
|
||||
}
|
||||
|
||||
@ -915,7 +911,7 @@ class ResultImpl<R extends Record> implements Result<R>, AttachableInternal {
|
||||
|
||||
Result<R> result = map.get(key);
|
||||
if (result == null) {
|
||||
result = new ResultImpl<R>(configuration(), rs, this.fields);
|
||||
result = new ResultImpl<R>(configuration(), this.fields);
|
||||
map.put(key, result);
|
||||
}
|
||||
|
||||
@ -1059,7 +1055,7 @@ class ResultImpl<R extends Record> implements Result<R>, AttachableInternal {
|
||||
|
||||
@Override
|
||||
public final <Z extends Record> Result<Z> into(Table<Z> table) {
|
||||
Result<Z> list = new ResultImpl<Z>(configuration(), rs, table.fields());
|
||||
Result<Z> list = new ResultImpl<Z>(configuration(), table.fields());
|
||||
|
||||
for (R record : this) {
|
||||
list.add(record.into(table));
|
||||
@ -1192,30 +1188,6 @@ class ResultImpl<R extends Record> implements Result<R>, AttachableInternal {
|
||||
return intern(fields.indexesOf(fieldNames));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
try {
|
||||
if (rs != null) {
|
||||
rs.close();
|
||||
rs = null;
|
||||
|
||||
for (Record record : this) {
|
||||
if (record instanceof AbstractRecord) {
|
||||
((AbstractRecord) record).rs = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SQLException e) {
|
||||
throw translate("Cannot close ResultSet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ResultSet resultSet() {
|
||||
return rs;
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparator for records, wrapping another comparator for <T>
|
||||
*/
|
||||
|
||||
@ -56,7 +56,6 @@ import org.jooq.ForeignKey;
|
||||
import org.jooq.FutureResult;
|
||||
import org.jooq.GroupField;
|
||||
import org.jooq.JoinType;
|
||||
import org.jooq.KeepResultSetMode;
|
||||
import org.jooq.Operator;
|
||||
import org.jooq.Param;
|
||||
import org.jooq.QueryPart;
|
||||
@ -969,11 +968,6 @@ class SelectImpl<R extends Record> extends AbstractDelegatingQuery<Select<R>> im
|
||||
return getDelegate().maxRows(rows);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultQuery<R> keepResultSet(KeepResultSetMode mode) {
|
||||
return getDelegate().keepResultSet(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ResultQuery<R> resultSetConcurrency(int resultSetConcurrency) {
|
||||
return getDelegate().resultSetConcurrency(resultSetConcurrency);
|
||||
|
||||
@ -334,31 +334,24 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void refresh() {
|
||||
refresh(fields.fields.fields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void refresh(Field<?>... f) {
|
||||
if (rs != null) {
|
||||
super.refresh(f);
|
||||
SelectQuery<?> select = create().selectQuery();
|
||||
select.addSelect(f);
|
||||
select.addFrom(getTable());
|
||||
Utils.addConditions(select, this, getPrimaryKey().getFieldsArray());
|
||||
|
||||
if (select.execute() == 1) {
|
||||
AbstractRecord record = (AbstractRecord) select.getResult().get(0);
|
||||
setValues(f, record);
|
||||
}
|
||||
else {
|
||||
|
||||
// [#2265] Even if rs was previously closed, re-fetch a new ResultSet
|
||||
// and assign it to this record, if requested.
|
||||
SelectQuery<?> select = create().selectQuery();
|
||||
select.addSelect(f);
|
||||
select.addFrom(getTable());
|
||||
select.keepResultSet(keepResultSetMode);
|
||||
Utils.addConditions(select, this, getPrimaryKey().getFieldsArray());
|
||||
|
||||
if (select.execute() == 1) {
|
||||
AbstractRecord record = (AbstractRecord) select.getResult().get(0);
|
||||
setValues(f, record);
|
||||
|
||||
rs = record.rs;
|
||||
rsIndex = record.rsIndex;
|
||||
}
|
||||
else {
|
||||
throw new InvalidResultException("Exactly one row expected for refresh. Record does not exist in database.");
|
||||
}
|
||||
throw new InvalidResultException("Exactly one row expected for refresh. Record does not exist in database.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -754,9 +754,7 @@ final class Utils {
|
||||
return record;
|
||||
}
|
||||
finally {
|
||||
if (cursor.closesAfterFetch()) {
|
||||
cursor.close();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user