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<DailyLeaderRecord> 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<Tuple2<Integer, Integer>>

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<String> 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<Integer>

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.
This commit is contained in:
Michael Bayne
2010-12-07 22:49:28 +00:00
parent 44a8fccc86
commit f48a688d99
8 changed files with 354 additions and 164 deletions
@@ -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:
* <pre>
* return load(CountRecord.class, from(ForumThreadRecord.class),
* where(ForumThreadRecord.GROUP_ID.eq(groupId)).count;
* </pre>
* 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
@@ -337,6 +337,15 @@ public abstract class DepotRepository
return _ctx.invoke(new FindAllKeysQuery<T>(_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 <T extends PersistentRecord> QueryBuilder<T> from (Class<T> type)
{
return new QueryBuilder<T>(this, type);
}
/**
* Inserts the supplied persistent object into the database, assigning its primary key (if it
* has one) in the process.
@@ -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<? extends PersistentRecord> fromClass)
{
return new FromOverride(fromClass);
}
/**
* Returns a {@link FromOverride} clause configured with the supplied override classes.
*/
protected static FromOverride from (Class<? extends PersistentRecord> fromClass1,
Class<? extends PersistentRecord> 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();
}
}
@@ -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<T extends PersistentRecord>
{
public QueryBuilder (DepotRepository repo, Class<T> pclass)
{
_repo = repo;
_pclass = pclass;
}
/** Disables the use of the cache for this query. */
public QueryBuilder<T> noCache () {
_cache = DepotRepository.CacheStrategy.BEST;
return this;
}
/** Configures the use of {@link CacheStrategy#BEST} for this query. */
public QueryBuilder<T> cacheBest () {
_cache = DepotRepository.CacheStrategy.BEST;
return this;
}
/** Configures the use of {@link CacheStrategy#RECORDS} for this query. */
public QueryBuilder<T> cacheRecords () {
_cache = DepotRepository.CacheStrategy.RECORDS;
return this;
}
/** Configures the use of {@link CacheStrategy#SHORT_KEYS} for this query. */
public QueryBuilder<T> cacheShortKeys () {
_cache = DepotRepository.CacheStrategy.SHORT_KEYS;
return this;
}
/** Configures the use of {@link CacheStrategy#LONG_KEYS} for this query. */
public QueryBuilder<T> cacheLongKeys () {
_cache = DepotRepository.CacheStrategy.LONG_KEYS;
return this;
}
/** Configures the use of {@link CacheStrategy#CONTENTS} for this query. */
public QueryBuilder<T> cacheContents () {
_cache = DepotRepository.CacheStrategy.CONTENTS;
return this;
}
/**
* Configures a {@link Where} clause that ANDs together all of the supplied expressions.
*/
public QueryBuilder<T> 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<T> 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<T> 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<T> join (ColumnExp left, ColumnExp right)
{
_join = new Join(left, right);
return this;
}
/**
* Configures a {@link GroupBy} clause on the supplied group expressions.
*/
public QueryBuilder<T> groupBy (SQLExpression... exprs)
{
_groupBy = new GroupBy(exprs);
return this;
}
/**
* Configures an {@link OrderBy} clause configured to randomly order the results.
*/
public QueryBuilder<T> randomOrder ()
{
_orderBy = OrderBy.random();
return this;
}
/**
* Configures an {@link OrderBy} clause that ascends on the supplied expression.
*/
public QueryBuilder<T> ascending (SQLExpression value)
{
_orderBy = OrderBy.ascending(value);
return this;
}
/**
* Configures an {@link OrderBy} clause that descends on the supplied expression.
*/
public QueryBuilder<T> descending (SQLExpression value)
{
_orderBy = OrderBy.descending(value);
return this;
}
/**
* Configures a {@link Limit} clause configured with the supplied offset and count.
*/
public QueryBuilder<T> 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<T> override (Class<? extends PersistentRecord> fromClass)
{
_fromOverride = new FromOverride(fromClass);
return this;
}
/**
* Configures a {@link FromOverride} clause configured with the supplied override classes.
*/
public QueryBuilder<T> override (Class<? extends PersistentRecord> fromClass1,
Class<? extends PersistentRecord> fromClass2)
{
_fromOverride = new FromOverride(fromClass1, fromClass2);
return this;
}
/**
* Configures a {@link ForUpdate} clause which marks this query as selecting for update.
*/
public QueryBuilder<T> 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<T> 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<Key<T>> 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<QueryClause> getClauses ()
{
List<QueryClause> 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<QueryClause> clauses = getClauses();
return clauses.toArray(new QueryClause[clauses.size()]);
}
protected void addIfNotNull (List<QueryClause> clauses, QueryClause clause)
{
if (clause != null) {
clauses.add(clause);
}
}
protected final DepotRepository _repo;
protected final Class<T> _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;
}
@@ -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();
}
@@ -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<GeneratedValueRecord> recs =
dr.findAll(GeneratedValueRecord.class, new Where(GeneratedValueRecord.VALUE.eq(2)));
List<GeneratedValueRecord> recs = dr.from(GeneratedValueRecord.class).
where(GeneratedValueRecord.VALUE.eq(2)).select();
assertEquals(1, Iterables.getOnlyElement(recs).recordId);
}
}
@@ -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<TestRecord> some = KeySet.newSimpleKeySet(
@@ -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<EnumKeyRecord> loadEnums (Set<EnumKeyRecord.Type> 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)