From f48a688d99ee93a8727f6353e41130f0f5c24223 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 7 Dec 2010 22:49:28 +0000 Subject: [PATCH] Scratch the FluentRepository, let's do things properly with a QueryBuilder. With this you can write queries like: MaxPointsRecord max = from(MaxPointsRecord.class). override(DailyScoreRecord.class), where(DailyScoreRecord.KEY.eq(key)).load(); List top = from(DailyLeaderRecord.class). override(DailyScoreRecord.class). join(DailyScoreRecord.KNIGHT_ID, KnightRecord.KNIGHT_ID). where(DailyScoreRecord.KEY.eq(key), DailyScoreRecord.POINTS.eq(max.maxPoints)). select(); After configuring your query, you can load(), you can select(), you can selectKeys() and you can selectCount(). Other improvements include configuring cache behavior (from(FooRecord.class).noCache().where(...).select()). random() became randomOrder() because I think that seeing random() stuffed in the middle of a query is insufficiently communicative. Next, I'm going to look into extending the query builder to allow things like: from(MemberRecord.class). where(MemberRecord.CREATED.greaterThan(date)). select(MemberRecord.AVATAR_ID, MemberRecord.HOME_SCENE_ID); // yields List> which will require adding types to ColumnExp, which I hope to do in a moderately backwards compatible way. If I can, I may use those types to enforce type correctness and turn things like the following into a compiler error: MemberRecord.MEMBER_ID.greaterThan("bob") If I had a more sophisticated type system, I could even make an attempt to call greaterThan() on a ColumnExp a compilation error. If the above doesn't turn out to be a rabbit hole extraordinairre (it probably will), I'd like to look into obviating the need for most of the FieldOverride and FromOverride fiddling, and support things like: from(MemberRecord.class). where(MemberRecord.CREATED.greaterThan(date)). select(Exps.max(MemberRecord.SESSIONS)); // yields List which would take Depot in the direction of being a (somewhat) type safe wrapper around SQL. This would go a long way toward bridging the conceptual gap between "the SQL I know I want to write" and "how the fuck do I do this with this Java API". I don't think I'm going to be able to get too far with the type-safe wrapper approach without propagating types all over the place, which may result in backwards compatibility breakage. If things get really bad, I may just have to pinch off the Depot 1.x series and venture into the green fields of Depot 2.x. I'll see how far I can get before taking such drastic action, however. --- .../com/samskivert/depot/CountRecord.java | 8 +- .../com/samskivert/depot/DepotRepository.java | 9 + .../samskivert/depot/FluentRepository.java | 150 ---------- .../com/samskivert/depot/QueryBuilder.java | 280 ++++++++++++++++++ .../java/com/samskivert/depot/CountTest.java | 54 ++++ .../samskivert/depot/GeneratedValueTest.java | 6 +- .../java/com/samskivert/depot/QueryTest.java | 7 +- .../com/samskivert/depot/TestRepository.java | 4 +- 8 files changed, 354 insertions(+), 164 deletions(-) delete mode 100644 src/main/java/com/samskivert/depot/FluentRepository.java create mode 100644 src/main/java/com/samskivert/depot/QueryBuilder.java create mode 100644 src/test/java/com/samskivert/depot/CountTest.java diff --git a/src/main/java/com/samskivert/depot/CountRecord.java b/src/main/java/com/samskivert/depot/CountRecord.java index 2f568ea..6743fc8 100644 --- a/src/main/java/com/samskivert/depot/CountRecord.java +++ b/src/main/java/com/samskivert/depot/CountRecord.java @@ -25,11 +25,9 @@ import com.samskivert.depot.annotation.Entity; import com.samskivert.depot.expression.ColumnExp; /** - * Handy record for computing the count of something. For example: - *
- * return load(CountRecord.class, from(ForumThreadRecord.class),
- *             where(ForumThreadRecord.GROUP_ID.eq(groupId)).count;
- * 
+ * Handy record for computing the count of something. In general, you need not use this directly, + * but should instead use {@link QueryBuilder#selectCount}. For example: {@code + * from(ForumThreadRecord.class).where(ForumThreadRecord.GROUP_ID.eq(groupId)).selectCount()} */ @Computed @Entity public class CountRecord extends PersistentRecord diff --git a/src/main/java/com/samskivert/depot/DepotRepository.java b/src/main/java/com/samskivert/depot/DepotRepository.java index 0dd2953..778107a 100644 --- a/src/main/java/com/samskivert/depot/DepotRepository.java +++ b/src/main/java/com/samskivert/depot/DepotRepository.java @@ -337,6 +337,15 @@ public abstract class DepotRepository return _ctx.invoke(new FindAllKeysQuery(_ctx, type, forUpdate, clauses)); } + /** + * Returns a builder that can be used to construct a query in a fluent style. For example: + * {@code from(FooRecord.class).where(ID.greaterThan(25)).ascending(SIZE).select()} + */ + public QueryBuilder from (Class type) + { + return new QueryBuilder(this, type); + } + /** * Inserts the supplied persistent object into the database, assigning its primary key (if it * has one) in the process. diff --git a/src/main/java/com/samskivert/depot/FluentRepository.java b/src/main/java/com/samskivert/depot/FluentRepository.java deleted file mode 100644 index e5d03f1..0000000 --- a/src/main/java/com/samskivert/depot/FluentRepository.java +++ /dev/null @@ -1,150 +0,0 @@ -// -// $Id$ -// -// Depot library - a Java relational persistence library -// Copyright (C) 2006-2010 Michael Bayne and Pär Winzell -// -// This library is free software; you can redistribute it and/or modify it -// under the terms of the GNU Lesser General Public License as published -// by the Free Software Foundation; either version 2.1 of the License, or -// (at your option) any later version. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -package com.samskivert.depot; - -import com.samskivert.depot.clause.ForUpdate; -import com.samskivert.depot.clause.FromOverride; -import com.samskivert.depot.clause.GroupBy; -import com.samskivert.depot.clause.Join; -import com.samskivert.depot.clause.Limit; -import com.samskivert.depot.clause.OrderBy; -import com.samskivert.depot.clause.Where; -import com.samskivert.depot.expression.ColumnExp; -import com.samskivert.depot.expression.SQLExpression; - -/** - * Provides a variety of helper methods for constructing queries more concisely (and in some case - * using a more fluent style). - */ -public abstract class FluentRepository extends DepotRepository -{ - protected FluentRepository (PersistenceContext context) - { - super(context); - } - - /** - * Returns a {@link Where} clause that ANDs together all of the supplied expressions. - */ - protected static Where where (SQLExpression... exprs) - { - switch (exprs.length) { - case 0: - throw new IllegalArgumentException("Must supply at least one expression."); - case 1: - return new Where(exprs[0]); - default: - return new Where(Ops.and(exprs)); - } - } - - /** - * Returns a {@link Where} clause that selects rows where the supplied column equals the - * supplied value. - */ - protected static Where where (ColumnExp column, Comparable value) - { - return new Where(column, value); - } - - /** - * Returns a {@link Where} clause that selects rows where both supplied columns equal both - * supplied values. - */ - protected static Where where (ColumnExp index1, Comparable value1, - ColumnExp index2, Comparable value2) - { - return new Where(index1, value1, index2, value2); - } - - /** - * Returns a {@link Join} clause configured with the supplied left and right columns. - */ - protected static Join join (ColumnExp left, ColumnExp right) - { - return new Join(left, right); - } - - /** - * Returns a {@link GroupBy} clause on the supplied group expressions. - */ - protected static GroupBy groupBy (SQLExpression... exprs) - { - return new GroupBy(exprs); - } - - /** - * Returns an {@link OrderBy} clause configured to randomly order the results. - */ - protected static OrderBy random () - { - return OrderBy.random(); - } - - /** - * Returns an {@link OrderBy} clause that ascends on the supplied expression. - */ - protected static OrderBy ascending (SQLExpression value) - { - return OrderBy.ascending(value); - } - - /** - * Returns an {@link OrderBy} clause that descends on the supplied expression. - */ - protected static OrderBy descending (SQLExpression value) - { - return OrderBy.descending(value); - } - - /** - * Returns a {@link Limit} clause configured with the supplied offset and count. - */ - protected static Limit limit (int offset, int count) - { - return new Limit(offset, count); - } - - /** - * Returns a {@link FromOverride} clause configured with the supplied override class. - */ - protected static FromOverride from (Class fromClass) - { - return new FromOverride(fromClass); - } - - /** - * Returns a {@link FromOverride} clause configured with the supplied override classes. - */ - protected static FromOverride from (Class fromClass1, - Class fromClass2) - { - return new FromOverride(fromClass1, fromClass2); - } - - /** - * Returns a {@link ForUpdate} clause which marks this query as selecting for update. - */ - protected static ForUpdate forUpdate () - { - return new ForUpdate(); - } -} diff --git a/src/main/java/com/samskivert/depot/QueryBuilder.java b/src/main/java/com/samskivert/depot/QueryBuilder.java new file mode 100644 index 0000000..70f12e6 --- /dev/null +++ b/src/main/java/com/samskivert/depot/QueryBuilder.java @@ -0,0 +1,280 @@ +// +// $Id$ +// +// Depot library - a Java relational persistence library +// Copyright (C) 2006-2010 Michael Bayne and Pär Winzell +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.depot; + +import java.util.List; + +import com.google.common.collect.Lists; + +import com.samskivert.depot.clause.*; +import com.samskivert.depot.expression.ColumnExp; +import com.samskivert.depot.expression.SQLExpression; + +/** + * The root of a fluent mechanism for constructing queries. Obtain an instance via {@link + * DepotRepository#from}. + */ +public class QueryBuilder +{ + public QueryBuilder (DepotRepository repo, Class pclass) + { + _repo = repo; + _pclass = pclass; + } + + /** Disables the use of the cache for this query. */ + public QueryBuilder noCache () { + _cache = DepotRepository.CacheStrategy.BEST; + return this; + } + + /** Configures the use of {@link CacheStrategy#BEST} for this query. */ + public QueryBuilder cacheBest () { + _cache = DepotRepository.CacheStrategy.BEST; + return this; + } + + /** Configures the use of {@link CacheStrategy#RECORDS} for this query. */ + public QueryBuilder cacheRecords () { + _cache = DepotRepository.CacheStrategy.RECORDS; + return this; + } + + /** Configures the use of {@link CacheStrategy#SHORT_KEYS} for this query. */ + public QueryBuilder cacheShortKeys () { + _cache = DepotRepository.CacheStrategy.SHORT_KEYS; + return this; + } + + /** Configures the use of {@link CacheStrategy#LONG_KEYS} for this query. */ + public QueryBuilder cacheLongKeys () { + _cache = DepotRepository.CacheStrategy.LONG_KEYS; + return this; + } + + /** Configures the use of {@link CacheStrategy#CONTENTS} for this query. */ + public QueryBuilder cacheContents () { + _cache = DepotRepository.CacheStrategy.CONTENTS; + return this; + } + + /** + * Configures a {@link Where} clause that ANDs together all of the supplied expressions. + */ + public QueryBuilder where (SQLExpression... exprs) + { + switch (exprs.length) { + case 0: + throw new IllegalArgumentException("Must supply at least one expression."); + case 1: + _where = new Where(exprs[0]); + break; + default: + _where = new Where(Ops.and(exprs)); + break; + } + return this; + } + + /** + * Configures a {@link Where} clause that selects rows where the supplied column equals the + * supplied value. + */ + public QueryBuilder where (ColumnExp column, Comparable value) + { + _where = new Where(column, value); + return this; + } + + /** + * Configures a {@link Where} clause that selects rows where both supplied columns equal both + * supplied values. + */ + public QueryBuilder where (ColumnExp index1, Comparable value1, + ColumnExp index2, Comparable value2) + { + _where = new Where(index1, value1, index2, value2); + return this; + } + + /** + * Configures a {@link Join} clause configured with the supplied left and right columns. + */ + public QueryBuilder join (ColumnExp left, ColumnExp right) + { + _join = new Join(left, right); + return this; + } + + /** + * Configures a {@link GroupBy} clause on the supplied group expressions. + */ + public QueryBuilder groupBy (SQLExpression... exprs) + { + _groupBy = new GroupBy(exprs); + return this; + } + + /** + * Configures an {@link OrderBy} clause configured to randomly order the results. + */ + public QueryBuilder randomOrder () + { + _orderBy = OrderBy.random(); + return this; + } + + /** + * Configures an {@link OrderBy} clause that ascends on the supplied expression. + */ + public QueryBuilder ascending (SQLExpression value) + { + _orderBy = OrderBy.ascending(value); + return this; + } + + /** + * Configures an {@link OrderBy} clause that descends on the supplied expression. + */ + public QueryBuilder descending (SQLExpression value) + { + _orderBy = OrderBy.descending(value); + return this; + } + + /** + * Configures a {@link Limit} clause configured with the supplied offset and count. + */ + public QueryBuilder limit (int offset, int count) + { + _limit = new Limit(offset, count); + return this; + } + + /** + * Configures a {@link FromOverride} clause configured with the supplied override class. + */ + public QueryBuilder override (Class fromClass) + { + _fromOverride = new FromOverride(fromClass); + return this; + } + + /** + * Configures a {@link FromOverride} clause configured with the supplied override classes. + */ + public QueryBuilder override (Class fromClass1, + Class fromClass2) + { + _fromOverride = new FromOverride(fromClass1, fromClass2); + return this; + } + + /** + * Configures a {@link ForUpdate} clause which marks this query as selecting for update. + */ + public QueryBuilder forUpdate () + { + _forUpdate = new ForUpdate(); + return this; + } + + /** + * Loads the first persistent object that matches the configured query clauses. + */ + public T load () + { + return _repo.load(_pclass, _cache, getClauseArray()); + } + + /** + * Loads all persistent objects that match the configured query clauses. + */ + public List select () + throws DatabaseException + { + return _repo.findAll(_pclass, _cache, getClauses()); + } + + /** + * Loads the keys of all persistent objects that match the configured query clauses. Note that + * cache configuration is ignored for key-only queries. + * + * @param useMaster if true, the query will be run using a read-write connection to ensure that + * it talks to the master database, if false, the query will be run on a read-only connection + * and may load keys from a slave. For performance reasons, you should always pass false unless + * you know you will be modifying the database as a result of this query and absolutely need + * the latest data. + */ + public List> selectKeys (boolean useMaster) + throws DatabaseException + { + return _repo.findAllKeys(_pclass, useMaster, getClauses()); + } + + /** + * Returns the count of rows that match the configured query clauses. + */ + public int selectCount () + { + _fromOverride = new FromOverride(_pclass); + return _repo.load(CountRecord.class, _cache, getClauseArray()).count; + } + + protected List getClauses () + { + List clauses = Lists.newArrayList(); + addIfNotNull(clauses, _where); + addIfNotNull(clauses, _join); + addIfNotNull(clauses, _orderBy); + addIfNotNull(clauses, _groupBy); + addIfNotNull(clauses, _limit); + addIfNotNull(clauses, _fromOverride); + addIfNotNull(clauses, _forUpdate); + return clauses; + } + + protected QueryClause[] getClauseArray () + { + List clauses = getClauses(); + return clauses.toArray(new QueryClause[clauses.size()]); + } + + protected void addIfNotNull (List clauses, QueryClause clause) + { + if (clause != null) { + clauses.add(clause); + } + } + + protected final DepotRepository _repo; + protected final Class _pclass; + + protected DepotRepository.CacheStrategy _cache = DepotRepository.CacheStrategy.BEST; + + protected Where _where; + protected Join _join; + protected OrderBy _orderBy; + protected GroupBy _groupBy; + protected Limit _limit; + protected FromOverride _fromOverride; + protected ForUpdate _forUpdate; +} diff --git a/src/test/java/com/samskivert/depot/CountTest.java b/src/test/java/com/samskivert/depot/CountTest.java new file mode 100644 index 0000000..51e05a8 --- /dev/null +++ b/src/test/java/com/samskivert/depot/CountTest.java @@ -0,0 +1,54 @@ +// +// $Id$ +// +// Depot library - a Java relational persistence library +// Copyright (C) 2006-2010 Michael Bayne and Pär Winzell +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.depot; + +import org.junit.Test; +import static org.junit.Assert.*; + +import com.samskivert.depot.clause.Where; + +/** + * Tests row counting. + */ +public class CountTest extends TestBase +{ + @Test + public void testCount () + { + for (int ii = 1; ii < 100; ii++) { + _repo.insert(createTestRecord(ii)); + } + + assertEquals(99, _repo.from(TestRecord.class).selectCount()); + assertEquals(49, _repo.from(TestRecord.class). + where(TestRecord.RECORD_ID.lessThan(50)).selectCount()); + + _repo.deleteAll(TestRecord.class, new Where(Exps.literal("true"))); + + assertEquals(0, _repo.from(TestRecord.class). + where(TestRecord.RECORD_ID.lessThan(50)).selectCount()); + assertEquals(0, _repo.from(TestRecord.class).selectCount()); + } + + // the HSQL in-memory database persists for the lifetime of the VM, which means we have to + // clean up after ourselves in every test; thus we go ahead and share a repository + protected TestRepository _repo = createTestRepository(); +} diff --git a/src/test/java/com/samskivert/depot/GeneratedValueTest.java b/src/test/java/com/samskivert/depot/GeneratedValueTest.java index b419107..217a079 100644 --- a/src/test/java/com/samskivert/depot/GeneratedValueTest.java +++ b/src/test/java/com/samskivert/depot/GeneratedValueTest.java @@ -27,8 +27,6 @@ import org.junit.Test; import com.google.common.collect.Iterables; -import com.samskivert.depot.clause.Where; - import static org.junit.Assert.assertEquals; public class GeneratedValueTest @@ -47,8 +45,8 @@ public class GeneratedValueTest assertEquals(0, rec.recordId); assertEquals(1, dr.insert(rec)); assertEquals(1, rec.recordId); - List recs = - dr.findAll(GeneratedValueRecord.class, new Where(GeneratedValueRecord.VALUE.eq(2))); + List recs = dr.from(GeneratedValueRecord.class). + where(GeneratedValueRecord.VALUE.eq(2)).select(); assertEquals(1, Iterables.getOnlyElement(recs).recordId); } } diff --git a/src/test/java/com/samskivert/depot/QueryTest.java b/src/test/java/com/samskivert/depot/QueryTest.java index b16c631..ab70290 100644 --- a/src/test/java/com/samskivert/depot/QueryTest.java +++ b/src/test/java/com/samskivert/depot/QueryTest.java @@ -29,6 +29,7 @@ import org.junit.Test; import static org.junit.Assert.*; import com.samskivert.depot.annotation.Computed; +import com.samskivert.depot.expression.SQLExpression; import com.samskivert.depot.clause.Where; /** @@ -63,9 +64,9 @@ public class QueryTest extends TestBase assertEquals(0, _repo.deleteAll(TestRecord.class, none)); // test collection caching (TODO: check that the records are ==) - Where where = new Where(TestRecord.RECORD_ID.greaterThan(CREATE_RECORDS-50)); - assertEquals(50, _repo.findAll(TestRecord.class, where).size()); - assertEquals(50, _repo.findAll(TestRecord.class, where).size()); + SQLExpression where = TestRecord.RECORD_ID.greaterThan(CREATE_RECORDS-50); + assertEquals(50, _repo.from(TestRecord.class).where(where).select().size()); + assertEquals(50, _repo.from(TestRecord.class).where(where).select().size()); // test a partial key set KeySet some = KeySet.newSimpleKeySet( diff --git a/src/test/java/com/samskivert/depot/TestRepository.java b/src/test/java/com/samskivert/depot/TestRepository.java index 0a787bd..d3f74b7 100644 --- a/src/test/java/com/samskivert/depot/TestRepository.java +++ b/src/test/java/com/samskivert/depot/TestRepository.java @@ -26,7 +26,7 @@ import java.util.Set; /** * A test tool for the Depot repository services. */ -public class TestRepository extends FluentRepository +public class TestRepository extends DepotRepository { public TestRecord loadNoCache (int recordId) { @@ -45,7 +45,7 @@ public class TestRepository extends FluentRepository public List loadEnums (Set types) { - return findAll(EnumKeyRecord.class, where(EnumKeyRecord.TYPE.in(types))); + return from(EnumKeyRecord.class).where(EnumKeyRecord.TYPE.in(types)).select(); } public void storeEnum (EnumKeyRecord record)