From 634651fa5ae334c29971317497a0b3a3e0381164 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Wed, 25 Jun 2014 16:15:18 -0700 Subject: [PATCH] Moved wiki docs into main project. Why have them separate? Github allows reading docs right in the main tree. --- README.md | 10 +- docs/Caching.md | 47 +++++++++ docs/ComputedRecords.md | 66 ++++++++++++ docs/Configuration.md | 207 ++++++++++++++++++++++++++++++++++++++ docs/ExampleQueries.md | 152 ++++++++++++++++++++++++++++ docs/README.md | 15 +++ docs/SchemaMigration.md | 50 +++++++++ docs/SimpleCodeExample.md | 153 ++++++++++++++++++++++++++++ 8 files changed, 695 insertions(+), 5 deletions(-) create mode 100644 docs/Caching.md create mode 100644 docs/ComputedRecords.md create mode 100644 docs/Configuration.md create mode 100644 docs/ExampleQueries.md create mode 100644 docs/README.md create mode 100644 docs/SchemaMigration.md create mode 100644 docs/SimpleCodeExample.md diff --git a/README.md b/README.md index dd3a9d8..d1684d4 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ way as much and surprise you as little as possible. Here's are some example to give you a taste of what code using Depot looks like. - * A [simple code example](wiki/SimpleCodeExample) - * Various [example queries](wiki/ExampleQueries) + * A [simple code example](docs/SimpleCodeExample) + * Various [example queries](docs/ExampleQueries) [API docs](http://threerings.github.io/depot/apidocs/) are also available. @@ -41,8 +41,8 @@ Here's are some example to give you a taste of what code using Depot looks like. Depot supports a number of very useful features. Here are a few of the main features for which time has permitted documentation: - * [Schema and Data migration](wiki/SchemaMigration) - * [Caching](wiki/Caching) + * [Schema and Data migration](docs/SchemaMigration) + * [Caching](docs/Caching) ## Getting Started @@ -50,7 +50,7 @@ If you want to use Depot on your project, check the following page for informati and dependencies via Maven or manually, as well as what sort of configuration Depot requires to start talking to your database. - * [Configuring and integrating](wiki/Configuration) Depot with your project + * [Configuring and integrating](docs/Configuration) Depot with your project ## Discussion diff --git a/docs/Caching.md b/docs/Caching.md new file mode 100644 index 0000000..e31dd63 --- /dev/null +++ b/docs/Caching.md @@ -0,0 +1,47 @@ +_Overview of Caching._ + +Records are cached by primary key. Lookups by primary key first check the cache and on a miss, load +the record from the database and insert it into the cache. + +Queries for records that have primary keys are automatically split into phases: + + * A query is made to the database for the primary keys of all rows that match the query. + * Any records in the cache are obtained from the cache. + * All remaining records are loaded by primary key in a single additional query and placed into + the cache. + +For deletions using a `Where` clause, first the primary keys that match the deletion clause are +loaded, then those records are deleted from the database and the cache using their primary key. + +Decomposition for updates using a `Where` clause is not yet implemented, but a fallback mechanism +to invalidate the cache manually is provided for those cases. + +If one already has the primary keys for the records they desire, it is possible to avoid the first +phase of the decomposed query using `loadAll()` instead of `findAll()`: + +```java +@Entity +public class MemberNameRecord extends PersistentRecord +{ + /** This member's unique id. */ + @Id public int memberId; + + /** The name by which this member is known. */ + public String name; +} + +/** + * Looks up members' names by id. + */ +public Map loadMemberNames (final Set memberIds) +{ + final Map names = Maps.newHashMap(); + for (MemberNameRecord name : loadAll(MemberNameRecord.class, memberIds)) { + names.put(name.memberId, name.name); + } + return names; +} +``` + +This will efficiently fetch all records that it can from the cache and then load and cache any +remaining records. diff --git a/docs/ComputedRecords.md b/docs/ComputedRecords.md new file mode 100644 index 0000000..a597a46 --- /dev/null +++ b/docs/ComputedRecords.md @@ -0,0 +1,66 @@ +_Overview of Computed Records._ + +Note: computed records are largely deprecated in favor of ad-hoc queries or the use of +`selectInto`. See [the example queries page](ExampleQueries) for examples of such use. + +## Computed Records + +You can easily define record with computed fields or records that represent a join across multiple +tables (see below). It is very easy to select a subset of a record's fields: + +```java +@Entity @Computed(shadowOf=PersonRecord.class) +public PersonNameRecord extends PersistentRecord +{ + public int personId; + public String name; +} + +List allNames = findAll(PersonNameRecord.class); +List youngNames = findAll( + PersonNameRecord.class, new Where(PersistentRecord.AGE.lessEq(18))); +``` + +You can also define computed records that calculate information: + +```java +@Entity @Computed +public CountRecord extends PersistentRecord +{ + @Computed(fieldDefinition="count(*)") + public int count; +} + +int personCount = load(CountRecord.class, new FromOverride(PersonRecord.class)).count; + +// or if you want to be less general purpose + +@Entity @Computed(shadowOf=PersonRecord.class) +public PersonCountRecord extends PersistentRecord +{ + @Computed(fieldDefinition="count(*)") + public int count; +} + +int personCount = load(PersonCountRecord.class).count; + +// or something more specific + +@Entity @Computed(shadowOf=PersonRecord.class) +public PersonAvgAgeRecord extends PersistentRecord +{ + @Computed(fieldDefinition="avg(*)") + public float averageAge; +} + +float averageAge = load(PersonAvgAgeRecord.class).averageAge; +``` + +The reader may be alarmed to notice some hard-coded SQL in those classes. One is advised to stick +to very standard SQL in cases like this to avoid introducing portability problems, but we didn't +think it was worth the trouble to try to model these simple and mostly standard operations in a +more complex way. YMMV. + +## Joins with Computed Records + +TBD diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 0000000..421cbdc --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,207 @@ +_Configuration and Dependencies._ + +## JVM Version Requirement + +Depot currently requires JDK 1.6 or greater. + +### Integrate with Ivy or Maven + +Depot is published to the Maven Central repository and can be added as a dependency using the +following configuration: `com.samskivert:depot:1.6.4` + +This will automatically include the Google Guava and samskivert dependencies. You can add Ehcache +as well via `net.sf.ehcache:ehcache:1.6.0` (or a newer version, if available). + +## Manually Adding Dependencies + +Depot depends on a small number of external libraries: + + * Google Guava - http://code.google.com/p/guava-libraries/ + * samskivert - http://code.google.com/p/samskivert/ + * Ehcache (optional) - http://ehcache.sourceforge.net/ + +Depot also requires a JDBC driver for the database with which you plan to operate. Depot currently +supports three database backends: + + * Postgresql - http://jdbc.postgresql.org/ + * MySQL - http://www.mysql.com/products/connector/j/ + * HSQLDB - http://hsqldb.org/ (useful for unit testing) + +## Configuration + +The two main components that require configuration are the JDBC connection provider and the cache +implementation. + +### StaticConnectionProvider + +For testing and other simple systems that don't require connection pooling, the +`StaticConnectionProvider` is a simple way to provide JDBC connections to Depot. It is used as +follows (this example uses MySQL): + +```java +Properties props = new Properties(); +// you'd probably load these properties from a file, but for the purposes +// of this example, we'll set them directly in the code +props.setProperty("default.driver", "com.mysql.jdbc.Driver"); +props.setProperty("default.url", "jdbc:mysql://localhost:3306/dbname"); +props.setProperty("default.username", "username"); +props.setProperty("default.password", "password"); + +PersistenceContext perCtx = new PersistenceContext( + "default", new StaticConnectionProvider(props), null); +``` + +### DataSourceConnectionProvider + +Production systems are more likely to use a JDBC `DataSource` to obtain their connections as those +provide connection pooling and integrate with JNDI and such. The only non-obvious aspect of +configuring Depot with a `DataSource` is that you can provide two datasources: one for read-only +connections and one for read-write connections. Depot will obtain connections from the appropriate +source depending on whether or not it is doing a query that is safe to be performed against a +read-only mirror of your data or if it's doing a query that must talk to a database master. + +What follows is a simple example of manually creating and configuring a Postgresql pooling +`DataSource`: + +```java +PoolingDataSource readSource = new PoolingDataSource(); +readSource.setDataSourceName("MyReadSource"); +readSource.setServerName("myReadOnlyServerHost"); +readSource.setDatabaseName("myDatabaseName"); +readSource.setPortNumber(5432); +readSource.setUser("myUsername"); +readSource.setPassword("myPassword"); +readSource.setMaxConnections(4); // tune to your applications needs + +PoolingDataSource writeSource = new PoolingDataSource(); +writeSource.setDataSourceName("MyWriteSource"); +writeSource.setServerName("myReadWriteServerHost"); +writeSource.setDatabaseName("myDatabaseName"); +writeSource.setPortNumber(5432); +writeSource.setUser("myUsername"); +writeSource.setPassword("myPassword"); +writeSource.setMaxConnections(1); // tune to your applications needs + +PersistenceContext perCtx = new PersistenceContext( + "notused", new DataSourceConnectionProvider("jdbc:postgresql", readSource, writeSource), null); +``` + +See the note below on lifecycle management. + +### EHCacheAdapter + +You may have noticed the second argument to the `PersistenceContext` constructor in the above +examples was always null. That is where the `CacheAdapter` is provided. By passing null, Depot will +not use caching. Depot comes with integration for Ehcache and implementing additional cache +integrations is as simple as implementing the `CacheAdapter` interface and supplying an instance to +the `PersistenceContext` constructor. + +The following example assumes that you have an `ehcache.xml` configuration file in your classpath. +There are other ways to configure Ehcache but we'll leave that explanation to their documentation. + +```java +CacheManager cacheMgr = CacheManager.getInstance(); +ConnectionProvider conProv = // ... +PersistenceContext perCtx = new PersistenceContext("ident", conProv, new EHCacheAdapter(cacheMgr)); +``` + +See the note below on lifecycle management. + +### PersistenceContext Lifecycle + +When your application is shutting down it should shutdown its `PersistenceContext`. However, to +avoid integration headaches, Depot does not take responsibility for shutting down certain of its +dependencies as those may be used by other parts of your application and you may wish to shut Depot +down independently of these other components. + +#### ConnectionProvider + +Depot will shutdown its connection provider when the `PersistenceContext` is shutdown, however the +two `ConnectionProvider` implementations have different shutdown behavior as explained below. + + * `StaticConnectionProvider` will close all JDBC `Connection` instances it has created when it is + shutdown. If you are using Depot with `StaticConnectionProvider` you can simply shutdown your + `PersistenceContext` and you're done. + * `DataSourceConnectionProvider` will not shutdown its underlying `DataSource` implementations + (indeed there is no API for doing so). As long as no queries are executing at the time that + `PersistenceContext` is shutdown, then all JDBC `Connection` instances will have been closed + and returned to the `DataSource` connection pool, so the application can shutdown its data + sources in whatever way is appropriate. + +#### CacheAdapter + +Depot will shutdown its `CacheAdapter` when the `PersistenceContext` is shutdown, however the +`CacheAdapter` implementation is free to do nothing in its `shutdown` call. + + * `EHCacheAdapter` does not shutdown its underlying `CacheManager` when it is shutdown to avoid + conflict with other aspects of the application that may use Ehcache. Thus the application is + responsible for shutting down the `CacheManager` itself when it is known to no longer be + needed. + +## Injection + +We use Guice around these parts for dependency injection. Using injection allows you to inject the +`PersistenceContext` into your repository implementations: + +```java +@Singleton +public class FooRepository extends DepotRepository { + @Inject public FooRepository (PersistenceContext perCtx) { + super(perCtx); + } +} +``` + +and then inject your repositories wherever you need them. + +We also find the following pattern to be very effective: + +```java +public class FooModule extends AbstractModule { + @Override protected void configure () { + super.configure(); + // depot dependencies (we will initialize this persistence context later when the + // server is ready to do database operations; not initializing it now ensures that no + // one sneaks any database manipulations into the dependency resolution phase) + bind(PersistenceContext.class).toInstance(new PersistenceContext()); + } +} + +public class WhateverHandlesAppServerLifecycle { + public void init () { + // initialize our persistence context + ConnectionProvider conProv = // ... + _perCtx.init("ident", conProv, new EHCacheAdapter(_cacheMgr)); + + // initialize our depot repositories; this runs all of our schema and data migrations + _perCtx.initializeRepositories(true); + } + + public void shutdown () { + _perCtx.shutdown(); + _cacheMgr.shutdown(); + } + + @Inject protected PersistenceContext _perCtx; + protected CacheManager _cacheMgr = CacheManager.getInstance(); +} +``` + +One major benefit to the approach of delaying the initialization of your persistence context until +the dependency resolution phase is complete is to ensure that no code accidentally (or +intentionally) starts talking to the database during that phase. You almost certainly want to +resolve all of your injection dependencies and then before you turn your application server loose, +call `initializeRepositories` to cause all of your schema and data migrations to be run (or to fail +and abort the initialization of your application). + +If you don't call `initializeRepositories` then Depot will lazily initialize each +`PersistentRecord` class when it is first accessed and run any schema migrations for that record. +Data migrations will be disabled if you choose this lazily initialized approach. + +Another note on `initializeRepositories` is that this will initialize all repositories that have +been constructed with the supplied `PersistenceContext` up to that point. Any repositories +constructed after `initializeRepositories` has been called will be initialized at that time +(running schema and data migrations for their records) and a warning will be generated to alert you +to this undesirable behavior. Again, experience has shown that you generally want to get all of +your schema and data migrations out of the way immediately and before the application server starts +normal operation. diff --git a/docs/ExampleQueries.md b/docs/ExampleQueries.md new file mode 100644 index 0000000..c19cd5c --- /dev/null +++ b/docs/ExampleQueries.md @@ -0,0 +1,152 @@ +_Describes various example queries._ + +Depot queries are constructed using a builder-pattern. + +## Whole record queries + +A basic query to select all rows from a table looks like so: + +```java +from(PersonRecord.class).select(); +``` + +Note that the above query and the others in these examples will use the `PersonRecord` from the +SimpleCodeExample page. + +Various query clauses may be added to the basic query above to do filtering, ordering and the like. +Here's a query with a simple where clause: + +```java +// selects all records with age <= 25 +from(PersonRecord.class).where(PersonRecord.AGE.lessEq(25)).select(); +``` + +One can also order and limit the results: + +```java +// selects the first ten records in ascending alphabetic order on name +from(PersonRecord.class).ascending(PersonRecord.NAME).limit(10).select(); +``` + +More complex orderings are also possible: + +```java +OrderBy order = OrderBy.ascending(PersonRecord.NAME).thenDescending(PersonRecord.AGE); +from(PersonRecord.class).orderBy(order).select(); +``` + +## Ad-hoc queries + +Instead of selecting whole rows from the database, one can select individual columns, or the +results of aggregate and other functions. Here's a simple projection: + +```java +List> results = + from(PersonRecord.class).select(PersonRecord.ID, PersonRecord.NAME); +``` + +Depot annotates the `ColumnExp` constants generated in your `PersistentRecord` classes with their +type so that queries like the above can be done in a type-safe manner. `Tuple` classes are provided +up to `Tuple5` for such ad-hoc queries. + +As an alternative to a `Tuple` class, you can use a type-safe builder to receive the results of +your query like so: + +```java +public class IdName { + public static Builder2 IDNAME_BUILDER = + new Builder2() { + public IdName build (Integer a, String b) { + return new IdName(a, b); + } + }; + + public int id; + public String name; + public IdName (int id, String name) { + this.id = id; + this.name = name; + } +} + +List results = + from(PersonRecord.class).select(IDNAME_BUILDER, PersonRecord.ID, PersonRecord.NAME); +``` + +Such queries will result in compile time error if the types of the columns do not match the types +expected by the builder. The `BuilderN` interfaces are also only available up to arity-5. + +For situations where type-safety is not a major concern, and for cases where you wish to select +more than five columns, you can use `selectInto` which uses reflection to construct results: + +```java +public class NameCount { + public String name; + public int count; + public NameCount (String name, int count) { + this.name = name; + this.count = count; + } +} + +List results = + from(PersonRecord.class).groupBy(PersonRecord.NAME).selectInto( + NameCount.class, PersonRecord.NAME, Funcs.countStar()); +``` + +Note that the class supplied to the `selectInto` method must have exactly one public constructor, +and the arguments to that constructor must match __in order__, the columns specified in the +`selectInto` call. The types of the selected columns (or expressions) must be convertible to the +type needed by the constructor (which means they will be widened or unboxed, but not converted from +`float` to `int` or other non-automatic conversions). These requirements are unfortunately not +checkable at compile time, and instead result in a runtime error when violated. Fortunately, +testing tends to catch any such errors before they make it into the wild. + +## Count queries + +The `selectCount` method exists for when you wish to simply select the count of rows that match +your query. For example: + +```java +int youngins = from(PersonRecord.class).where(PersonRecord.AGE.lessEq(12)).selectCount(); +``` + +You may also wish to group by certain columns and select the counts of rows that match each group. +This is done with an ad-hoc query: + +```java +List> results = + from(PersonRecord.class).groupBy(PersonRecord.NAME). + select(PersonRecord.NAME, Funcs.countStar()); +``` + +## Other functions + +A variety of other functions are defined in +[Funcs](http://depot.googlecode.com/svn/apidocs/com/samskivert/depot/Funcs.html), +[StringFuncs](http://depot.googlecode.com/svn/apidocs/com/samskivert/depot/StringFuncs.html), +[DateFuncs](http://depot.googlecode.com/svn/apidocs/com/samskivert/depot/DateFuncs.html), and +[MathFuncs](http://depot.googlecode.com/svn/apidocs/com/samskivert/depot/MathFuncs.html). These can +be used in queries, like so: + +```java +List eldest = + from(PersonRecord.class).where(PersonRecord.AGE.eq(Funcs.max(PersonRecord.AGE))).select(); +``` + +And you can select the value of a function in an ad-hoc query: + +```java +// note that load() can be used for selections that will only ever return one row +Number maxAge = from(PersonRecord.class).load(Funcs.max(PersonRecord.AGE)); + +// alternatively +List maxAge = from(PersonRecord.class).load(Funcs.max(PersonRecord.AGE)); +assert(maxAge.size() == 1); + +// here's a more complex (if somewhat nonsensical) query that groups people by the first +// letter of their name and selects the sum of all ages of the people in those groups +SQLExpression firstLetter = StringFuncs.substring(PersonRecord.NAME, 0, 1); +List results = from(PersonRecord.class).groupBy(firstLetter). + select(firstLetter, Funcs.sum(PersonRecord.AGE)); +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ecb704b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +_Overview of the Depot documentation._ + +## Docs + +- [Simple code example](wiki/SimpleCodeExample) +- [Configuring and integrating Depot](wiki/Configuration) +- [Example queries](wiki/ExampleQueries) +- [Caching](wiki/Caching) +- [Computed records](wiki/ComputedRecords) +- [Schema migration](wiki/SchemaMigration) + +## Google Group + +If you have further questions or suggestions, please post to the +[OOO Libs Google Group](http://groups.google.com/group/ooo-libs). diff --git a/docs/SchemaMigration.md b/docs/SchemaMigration.md new file mode 100644 index 0000000..8cf0018 --- /dev/null +++ b/docs/SchemaMigration.md @@ -0,0 +1,50 @@ +_Overview of Schema and Data Migration._ + +## Schema Migration + +Depot makes simple schema migrations extremely simple and complex schema migrations pretty easy. + + * Automatic schema migration: adding a new column to a persistent record is as simple as adding + the new field to the POJO and incrementing the `SCHEMA_VERSION_NUMBER` constant. + * Assisted schema migration: dropping, renaming and retyping columns is very easy, and more + sophisticated custom migrations can also be easily incorporated into Depot's schema migration + system. + * Data migration: migrations that do not change record schemas but manipulate their data can also + be registered and Depot will ensure that they run successfully and only once. + * Distributed migration coordination: Depot is designed so that you can bring up a dozen + application servers on a dozen machines and during their initialization they will coordinate + (through the database) which server will handle each migration and the other servers will block + any database access until those migrations have successfully completed. + +The addition of columns is automatic. Dropping, renaming and retyping are very simple: + +```java +public DecorRepository (PersistenceContext ctx) +{ + super(ctx); + + ctx.registerMigration(DecorRecord.class, + new SchemaMigration.Rename(17004, "scale", DecorRecord.ACTOR_SCALE)); + ctx.registerMigration(DecorRecord.class, new SchemaMigration.Drop(17004, "offsetX")); + ctx.registerMigration(DecorRecord.class, new SchemaMigration.Drop(17004, "offsetY")); + ctx.registerMigration(DecorRecord.class, + new SchemaMigration.Retype(17004, DecorRecord.FURNI_SCALE)); +} +``` + +More complex migrations are also possible, one has to take care if they wish to preserve database +agnosticism: + +```java +ctx.registerMigration(FooRecord.class, new SchemaMigration(42) { + @Override + public Integer invoke (Connection conn, DatabaseLiaiason liaison) throws SQLException { + // go crazy with your raw JDBC connection or use the DatabaseLiaison to + // help you do things in a database agnostic way + } +}); +``` + +## Data Migration + +TBD diff --git a/docs/SimpleCodeExample.md b/docs/SimpleCodeExample.md new file mode 100644 index 0000000..ca3d8e3 --- /dev/null +++ b/docs/SimpleCodeExample.md @@ -0,0 +1,153 @@ +_A simple Depot code example._ + +Here's a simple example to give you a quick overview of what code using Depot looks like. + +Start by defining a persistent record, this maps to a database table: + +```java +@Entity +public class PersonRecord extends PersistentRecord +{ + /** Increment this value if you change this record's schema. */ + public static final int SCHEMA_VERSION = 1; + + /** A unique identifier for this record. Automatically filled in at row insertion time. */ + @Id @GeneratedValue(strategy=GenerationType.IDENTITY) + public int personId; + + /** This person's name. Note: one difference between EJB3 and Depot is that columns are + * non-nullable by default. */ + @Column(length=100) + public String name; + + /** This person's age. */ + public int age; +} +``` + +Then you run a simple Ant task or Maven plugin that adds some unfortunately non-POJO boilerplate +code to your record class, but this code allows you to talk about your record in queries in a way +that the compiler can check which is a huge win. + +If and when Java adds field literals, Depot will absolutely take advantage of them and eliminate +this undesirable boilerplate. + +```java +@Entity +public class PersonRecord extends PersistentRecord +{ + // AUTO-GENERATED: FIELDS START + public static final Class _R = PersonRecord.class; + public static final ColumnExp PERSON_ID = colexp(_R, "personId"); + public static final ColumnExp NAME = colexp(_R, "name"); + public static final ColumnExp AGE = colexp(_R, "age"); + // AUTO-GENERATED: FIELDS END + + /** Increment this value if you change this record's schema. */ + public static final int SCHEMA_VERSION = 1; + + /** A unique identifier for this record. Automatically filled in at row insertion time. */ + @Id @GeneratedValue(strategy=GenerationType.IDENTITY) + public int personId; + + /** This person's name. Note: one difference between EJB3 and Depot is that columns are + * non-nullable by default. */ + @Column(length=100) + public String name; + + /** This person's age. */ + public int age; + + // AUTO-GENERATED: METHODS START + /** + * Create and return a primary {@link Key} to identify a {@link PersonRecord} + * with the supplied key values. + */ + public static Key getKey (int personId) + { + return newKey(_R, personId); + } + // AUTO-GENERATED: METHODS END +} +``` + +Next you define a repository class which will provide an application-specific persistence API. We +highly recommend preserving this boundary and having all Depot code inside repository classes and +only pass persistent record classes outside to your application. + +```java +public class PersonRepository extends DepotRepository +{ + /** + * Creates this repository and provides it with a context via which it will obtain JDBC + * connections. + */ + public PersonRepository (PersistenceContext ctx) + { + super(ctx); + } + + /** + * Loads and returns the person with the specified id, or null if no person exists with that + * id. + */ + public PersonRecord loadPerson (int personId) + { + return load(PersonRecord.getKey(personId)); + } + + /** + * Loads records for all people with an age less than or equal to the specified maximum. + */ + public List loadYoungPeople (int maxAge) + { + return from(PersonRecord.class).where(PersonRecord.AGE.lessEq(maxAge)).select(); + } + + /** + * Loads the names of all people in the repository. + */ + public Set loadNames () + { + Set names = new HashSet(); + names.addAll(from(PersonRecord.class).select(PersonRecord.NAME)); + return names; + } + + /** + * Inserts a newly created person record into the database. If record.personId is non-zero (or + * non-null in the case of a non-primitive integer field) an exception will be thrown. + */ + public void insertPerson (PersonRecord record) + { + insert(record); + } + + /** + * Updates a person record. If record.personId is zero (or null in the case of a non-primitive + * integer field) an exception will be thrown. + */ + public void updatePerson (PersonRecord record) + { + update(record); + } + + /** + * Updates a person record. If record.personId is zero (or null in the case of a non-primitive + * integer field) a new row will be created for this record, if not the matching row will be + * updated. + */ + public void storePerson (PersonRecord record) + { + store(record); + } + + @Override // from DepotRepository + protected void getManagedRecords (Set> classes) + { + classes.add(PersonRecord.class); + } +} +``` + +See the [example queries](ExampleQueries) page for examples of other kinds of queries.