From 819edb20e8e74a56c474977b106cd601289aff55 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 1 May 2015 21:03:38 +1200 Subject: [PATCH] #282 - API: Add Model.Find as alternative to Model.Finder ... which uses reflection to determine entity bean type literal --- src/main/java/com/avaje/ebean/Model.java | 238 ++++++++++++++---- .../java/com/avaje/ebean/util/ClassUtil.java | 42 +++- .../avaje/tests/model/info/InfoCompany.java | 2 +- .../avaje/tests/model/info/InfoCustomer.java | 2 +- .../avaje/tests/model/onetoone/Account.java | 6 +- .../com/avaje/tests/model/onetoone/User.java | 2 +- 6 files changed, 238 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/avaje/ebean/Model.java b/src/main/java/com/avaje/ebean/Model.java index a09a98cf1..6aaa19766 100644 --- a/src/main/java/com/avaje/ebean/Model.java +++ b/src/main/java/com/avaje/ebean/Model.java @@ -1,11 +1,13 @@ package com.avaje.ebean; import com.avaje.ebean.text.PathProperties; +import com.avaje.ebean.util.ClassUtil; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; import javax.persistence.MappedSuperclass; @@ -34,7 +36,7 @@ import javax.persistence.MappedSuperclass; * *

* If you choose to use the Model mapped superclass you will probably also chose to additionally add - * a {@link Finder} as a public static field to complete the active record pattern and provide a + * a {@link Find} as a public static field to complete the active record pattern and provide a * relatively nice clean way to write queries. * *

Typical common @MappedSuperclass

@@ -64,19 +66,15 @@ import javax.persistence.MappedSuperclass; * // Extend the mappedSuperclass * * @Entity @Table(name="oto_account") - * public class Account extends BaseModel { + * public class Customer extends BaseModel { * - * // add a static Finder - * // ... with Long being the type of our ID property ... + * // Add a static Find + * // ... with Long being the type of our @Id property. + * // ... Note the {} at the end as Find is an abstract class. * - * public static final Finder find = - * new Finder(Long.class, Account.class); + * public static final Find find = new Find(){}; * * String name; - * - * @OneToOne(mappedBy = "account",optional = true) - * User user; - * * ... * } * @@ -86,28 +84,28 @@ import javax.persistence.MappedSuperclass; *
{@code
  *
  *     // Active record style ... save(), delete() etc
- *     Account account = new Account();
- *     account.setName("AC234");
+ *     Customer customer = new Customer();
+ *     customer.setName("AC234");
  *
  *     // save() method inherited from Model
- *     account.save();
+ *     customer.save();
  *
  * }
* - *

Finder: find byId

+ *

Find byId

*
{@code
  *
  *     // find byId
- *     Account account = Account.find.byId(42);
+ *     Customer customer = Customer.find.byId(42);
  *
  * }
* - *

Finder: find where

+ *

Find where

*
{@code
  *
  *     // find where ...
- *     List accounts =
- *         Account.find
+ *     List customers =
+ *         Customer.find
  *         .where().gt("startDate", lastMonth)
  *         .findList();
  *
@@ -124,7 +122,8 @@ public abstract class Model {
    * 
    * 

* Example: - *

+   * 
{@code
+   *
    * Transaction transaction = Customer.db().beginTransaction();
    * try {
    * 
@@ -141,7 +140,7 @@ public abstract class Model {
    *   customer.save();
    * 
    *   Customer otherCustomer = new Customer();
-   *   otherCustomer.setName("Franko");
+   *   otherCustomer.setName("Franko");
    *   otherCustomer.save();
    * 
    *   transaction.commit();
@@ -150,7 +149,7 @@ public abstract class Model {
    *   transaction.end();
    * }
    * 
-   * 
+ * }
*/ public static EbeanServer db() { return Ebean.getServer(null); @@ -179,7 +178,7 @@ public abstract class Model { * An unmodified bean that is saved or updated is normally skipped and this marks the bean as * dirty so that it is not skipped. * - *
+   * 
{@code
    * 
    * Customer customer = Customer.find.byId(id);
    * 
@@ -188,7 +187,7 @@ public abstract class Model {
    * customer.markAsDirty();
    * customer.save();
    * 
-   * 
+ * }
* * @see EbeanServer#markAsDirty(Object) */ @@ -266,46 +265,193 @@ public abstract class Model { db().refresh(this); } + /** + * A concrete implementation of Find. + *

+ * It should be preferred to use {@link Find} instead of Finder as that can use reflection to determine the class + * literal type of the entity bean. + *

+ * @param type of the Id property + * @param type of the entity bean + */ + public static class Finder extends Find { + + /** + * Create with the type of the entity bean. + * + *
{@code
+     *
+     * @Entity
+     * public class Customer extends BaseModel {
+     *
+     *   public static final Finder find = new Finder(Customer.class);
+     *   ...
+     *
+     * }
+ * + *

+ * The preferred approach is to instead use Find as below. This approach is more DRY in that it does + * not require the class literal Customer.class to be passed into the constructor. + * + *

{@code
+     *
+     * @Entity
+     * public class Customer extends BaseModel {
+     *
+     *   public static final Find find = new Find(){};
+     *   ...
+     *
+     * }
+ */ + public Finder(Class type) { + super(null, type); + } + + /** + * Create with the type of the entity bean and specific server name. + */ + public Finder(String serverName, Class type) { + super(serverName, type); + } + + /** + * Please migrate to use {@link Find} or constructor Finder(Class) that + * does not have the idType parameter. + *

+ * Create with the type of the ID property and entity bean and specific server name. + * + * @deprecated + */ + public Finder(Class idType, Class type) { + super(null, type); + } + + /** + * Please migrate to use the constructor Finder(String, Class) that + * does not have the idType parameter. + *

+ * Create with the type of the ID property and entity bean and specific server name. + * + * @deprecated + */ + public Finder(String serverName, Class idType, Class type) { + super(serverName, type); + } + } + /** * Helper object for performing queries. * *

- * Typically a Finder is defined as a public static field on an entity bean class to provide a + * Typically a Find instance is defined as a public static field on an entity bean class to provide a * nice way to write queries. - * + * + *

Example use:

+ * + *
{@code
+   *
+   * @Entity
+   * public class Customer extends BaseModel {
+   *
+   *   public static final Find find = new Find(){};
+   *
+   *   ...
+   *
+   * }
+ *

+ * This enables you to write code like: + *

{@code
+   *
+   * Customer customer = Customer.find.byId(42L);
+   *
+   * List customers =
+   *     Customer.find
+   *         .select("name, dateOfBirth")
+   *         .findList();
+   *
+   * }
+ * + *

Kotlin

+ * In Kotlin you would typically create Find as a companion object. + *
{@code
+   *
+   *   // kotlin
+   *   companion object : Model.Find() {}
+   *
+   * }
* @param * The Id type. This is most often a {@link Long} but is also often a {@link UUID} or * {@link String}. * * @param - * The bean type + * The entity bean type */ - public static class Finder { + public static abstract class Find { - private final Class idType; + /** + * The entity bean type. + */ private final Class type; + + /** + * The name of the EbeanServer, null for the default server. + */ private final String serverName; /** * Creates a finder for entity of type T with ID of type I. - * - *

- * Typically you use this constructor to have a static "find" field on each entity bean. + *

+ * Typically you create Find as a public static field on each entity bean as the example below. + * + *

+ * Note that Find is an abstract class and hence {} is required. This is done so + * that the type (class literal) of the entity bean can be derived from the generics parameter. + * + *

{@code
+     *
+     * @Entity
+     * public class Customer extends BaseModel {
+     *
+     *   // Note the trailing {} as Find is an abstract class.
+     *   // We do this so that we can derive the type literal Customer.class
+     *   // via reflection
+     *   public static final Find find = new Find(){};
+     *   ...
+     *
+     * }
+ *

+ * This enables you to write code like: + *

{@code
+     *
+     * Customer customer = Customer.find.byId(42L);
+     *
+     * List customers =
+     *     Customer.find
+     *        .select("name, email, dateOfBirth")
+     *        .findList();
+     *
+     * }
+ * + *

Kotlin

+ * In Kotlin you would typically create it as a companion object. + * + *
{@code
+     *
+     *   // kotlin
+     *   companion object : Model.Find() {}
+     *
+     * }
*/ - public Finder(Class idType, Class type) { - this(null, idType, type); + public Find() { + this.serverName = null; + this.type = (Class)ClassUtil.getSecondArgumentType(getClass()); } /** - * Creates a finder for entity of type T with ID of type I, using a - * specific EbeanServer. - * - *

- * Typically you don't need to use this method. + * Construct passing the class literal type of the entity type. */ - public Finder(String serverName, Class idType, Class type) { + protected Find(String serverName, Class type) { this.type = type; - this.idType = idType; this.serverName = serverName; } @@ -335,12 +481,12 @@ public abstract class Model { /** * Creates a Finder for the named EbeanServer. - * + * *

* Create and return a new Finder for a different server. */ public Finder on(String server) { - return new Finder(server, idType, type); + return new Finder(server, type); } /** diff --git a/src/main/java/com/avaje/ebean/util/ClassUtil.java b/src/main/java/com/avaje/ebean/util/ClassUtil.java index f823a539a..60ce5e08f 100644 --- a/src/main/java/com/avaje/ebean/util/ClassUtil.java +++ b/src/main/java/com/avaje/ebean/util/ClassUtil.java @@ -1,9 +1,11 @@ package com.avaje.ebean.util; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; + /** * Helper to find classes taking into account the context class loader. - * - * @author rbygrave */ public class ClassUtil { @@ -20,4 +22,40 @@ public class ClassUtil { } } + + /** + * Returns the raw type for the 2nd generic parameter for a subclass. + */ + public static Class getSecondArgumentType(Class subclass) { + Type[] typeArguments = getSuperclassTypeParameter(subclass); + if (typeArguments.length != 2) { + throw new IllegalArgumentException("Expected type with 2 generic argument types but got " + + typeArguments.length + " - " + Arrays.toString(typeArguments)); + } + + return getRawType(typeArguments[1]); + } + + static Type[] getSuperclassTypeParameter(Class subclass) { + Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing generics type parameters on subclass " + subclass); + } + return ((ParameterizedType) superclass).getActualTypeArguments(); + } + + private static Class getRawType(Type type) { + + if (type instanceof Class) { + return (Class) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type rawType = parameterizedType.getRawType(); + if (rawType instanceof Class) { + return (Class) rawType; + } + } + throw new RuntimeException("Unable to obtain raw class type from " + type); + } } diff --git a/src/test/java/com/avaje/tests/model/info/InfoCompany.java b/src/test/java/com/avaje/tests/model/info/InfoCompany.java index 202bd5cba..c8fa175e3 100644 --- a/src/test/java/com/avaje/tests/model/info/InfoCompany.java +++ b/src/test/java/com/avaje/tests/model/info/InfoCompany.java @@ -9,7 +9,7 @@ import java.util.List; @Entity public class InfoCompany extends Model { - public static Finder find = new Finder(Long.class, InfoCompany.class); + public static Finder find = new Finder(InfoCompany.class); @Id Long id; diff --git a/src/test/java/com/avaje/tests/model/info/InfoCustomer.java b/src/test/java/com/avaje/tests/model/info/InfoCustomer.java index 252a6283c..033b2785d 100644 --- a/src/test/java/com/avaje/tests/model/info/InfoCustomer.java +++ b/src/test/java/com/avaje/tests/model/info/InfoCustomer.java @@ -7,7 +7,7 @@ import javax.persistence.*; @Entity public class InfoCustomer extends Model { - public static Finder find = new Finder(Long.class, InfoCustomer.class); + public static Finder find = new Finder(InfoCustomer.class); @Id Long id; diff --git a/src/test/java/com/avaje/tests/model/onetoone/Account.java b/src/test/java/com/avaje/tests/model/onetoone/Account.java index 29923e161..ba4ab835f 100644 --- a/src/test/java/com/avaje/tests/model/onetoone/Account.java +++ b/src/test/java/com/avaje/tests/model/onetoone/Account.java @@ -1,16 +1,16 @@ package com.avaje.tests.model.onetoone; +import com.avaje.tests.model.BaseModel; + import javax.persistence.Entity; import javax.persistence.OneToOne; import javax.persistence.Table; -import com.avaje.tests.model.BaseModel; - @Entity @Table(name="oto_account") public class Account extends BaseModel { - public static final Finder find = new Finder(Long.class, Account.class); + public static final Find find = new Find(){}; String name; diff --git a/src/test/java/com/avaje/tests/model/onetoone/User.java b/src/test/java/com/avaje/tests/model/onetoone/User.java index a459d4232..e1cfd8b6c 100644 --- a/src/test/java/com/avaje/tests/model/onetoone/User.java +++ b/src/test/java/com/avaje/tests/model/onetoone/User.java @@ -10,7 +10,7 @@ import com.avaje.tests.model.BaseModel; @Table(name="oto_user") public class User extends BaseModel { - public static final Finder find = new Finder(Long.class, User.class); + public static final Find find = new Find(){}; String name;