realm / realm-java

Realm is a mobile database: a replacement for SQLite & ORMs
http://realm.io
Apache License 2.0
11.45k stars 1.75k forks source link

Provide type safe queries #1744

Open Wattos opened 8 years ago

Wattos commented 8 years ago

With the current API we need to provide a string for the field we want to query. This is suboptimal as refactoring tools might miss the string and create run time exceptions. In addition, the queries are not type safe (e.g. I could query a string field to look for an integer).

E.g.

realm.where(Foo.class).equalTo("name", 2);

Instead, it would be nice if realm created constant fields which could be used directly instead of string literals,

e.g.

realm.where(Foo.class).equalTo(FooFields.NAME, 2);

In addition to this, the constant fields could be made type safe, by using a type parameter:

public class RealmQuery {

    private static Field<String> FIELD_ID;

    public <T> RealmQuery equalTo(Field<T> field, T value) {
        return this;
    }

    interface Field<T> {

    }
}
cmelchior commented 8 years ago

Yes, we have been considering something like that ourselves. However adding a type safe query API would require the annotation processor to run first, which creates some annoying constraints for the order you write your code + run the compiler in.

We haven't ruled it out yet, and It could be added as an advanced or opt-in feature, but it has low priority at the moment.

Right now the closest you can come is probably by manually adding static String field in your RealmObject classes. At least the refactor function in Android Studio will also suggest renaming those if you rename you fields.

Wattos commented 8 years ago

To me it send a much bigger annoyance to create the field per hand.

In addition, the problem with the compile, then code is really minor, especially that it can easily be fixed with an Android studio plugin.

It is a shame eclipse got deprecated, since that would have worked out of the box

Wattos commented 8 years ago

@cmelchior would you guys accept a pull request for this?

cmelchior commented 8 years ago

Hi @Wattos This is a rather big feature, and at this point we are still considering different possibilities for how to improve our query support. So while we do appreciate the offer, at this point we probably don't have the time to design/review such a PR.

As you point out another interesting angle could be trying to add some sort of incremental compile support through an Android Studio plugin.

Something that could work today, without involving the current Realm API's would be a extra add-on library that looked at the RealmClasses and created an additional <Class>Field.java class. This would give you type-safe field names and could just be an extra dependency with an annotation processor. Cloning our current annotation processor library would give you all the infrastructure for introspecting the RealmClasses.

mshusek commented 8 years ago

In my opinion this is very important feature to don't hard coding string inside query. This looks ulgly and developers waste a lot of hours to fix typo inside projects.

For example inside DBFlow ORM we can always get field name by CustomObject$Table.MYFIELD source : https://github.com/Raizlabs/DBFlow Of course the best way is to allow developer to rename column by injection like @Column(name="column_name") but other issue related it is not possible at this moment becuase of core engine.

kneth commented 8 years ago

For the time being, I recommend all app developers to declare string constants of field names in the model class and use these constants in queries. It is not type safe but it minimises the risk to typing errors.

pboos commented 7 years ago

I agree with the others. Hard coding strings is bad and error prone. Of course workaround from @kneth is what I will do, but this could be generated easily at compile time with the other classes. Even okay if it is put into one of the other classes XyzRealmProxy.

Zhuinden commented 7 years ago

You can use this project in the meantime

https://github.com/cmelchior/realmfieldnameshelper

cmelchior commented 7 years ago

We could use the same approach as in my project above to create type-safe query classes, but so far this is not an approach we have explored in-depth, because it does contain a number of pitfalls:

buchandersenn commented 7 years ago

@cmelchior, I cloned https://github.com/cmelchior/realmfieldnameshelper and took a stab at auto-generating query-builders using your approach. Usage:

Dog dogNamedKiller = new DogQueryBuilder(realm)
    .name().equalTo("Killer")
    .findFirst();

If name is a string field the type will be checked at compile-time, so comparing with an integer by mistake won't be possible:

Dog dogNamedKiller = new DogQueryBuilder(realm)
    .name().equalTo(2) // Won't compile - name is a String and can't be compared to an integer
    .findFirst();

I think the method count is manageable. I only generate a method per field. Each method returns a XXXConditionBuilder, where XXX is String, Integer and so on, depending on the type of the field. The condition builders are shared across query builders for all realm model classes.

I also generated builder classes for creating realm objects, but honestly they aren't that useful (yet):

final Person john = new PersonBuilder()
        .name("John")
        .addDog(spot)
        .addDog(fluffy)
        .build();

Link:

https://github.com/buchandersenn/android-realm-builders

Zhuinden commented 7 years ago

@cmelchior I'm actually somewhat surprised, apparently there used to be an api like that before 2014.09.28. (a day before the first public beta, 0.70.0)

    PeopleTable peopleTable = new PeopleTable();

     // @@Example: advanced_search @@
    // Define the query
    PeopleQuery query = peopleTable
                           .age.between(20, 35)    // Implicit AND with below
                           .name.contains("a")     // Implicit AND with below
                           .group()                // "("
                               .hired.equal(true)
                               .or()               // or
                               .name.endsWith("y")
                           .endGroup();            // ")"
    // Count matches
    PeopleView match = query.findAll();

It was removed entirely when Realm became io.realm. I'm guessing there was something clunky about it; most likely the PeopleTable class, and handling subtables.

Apparently history can be super-duper interesting though! :smile:


What's super-interesting to me is that it was dismissed because this was "unusual" at the time (and there were two APIs for querying - the non-typed one is what we see now with cached column indices), but this typed version is very similar to what QueryDSL does.

kneth commented 7 years ago

@Zhuinden The clunky part was that generating classes like PeopleTable happened outside Android Studio, and the user was not able to have completion, etc. Somewhat like GreenDAO 2. We decided to use the annotation processor instead ;-)

cmelchior commented 7 years ago

We could theoretically create user visible classes like the RealmFieldHelper project does using an annotation processor, and @buchandersenn actually already started doing here: https://github.com/buchandersenn/android-realm-builders

I'm not entirely sure this is something we would want as part of the main Realm project though, mostly because the method count would kinda explode, also we would still need the current support for stuff like queries on linked fields, since those could not feasible be auto-generated.

Zhuinden commented 7 years ago

Yeah, method count would explode, also QueryDSL also only supports paths only 4 levels deep by default (kinda like links, really), and there's an additional annotation that you can use to "initialize a path" if you need something deeper.

There is a reason why the "dynamic query api" is what stayed instead of the "typed" one, I guess. In fact, back then I'm not sure there even was multidexing!

I just thought the history of the Realm query API is very interesting.

For my intents and purposes, the realmfieldnameshelper library is sufficient 😄

cmelchior commented 7 years ago

@We all love type safety, and I could see us baking validation into an Android Studio plugin (or Lint), possibly even auto-completion. Now to find those cycles somewhere 😄

buchandersenn commented 7 years ago

Just to chime in here, the snippet provided by @Zhuinden seems to indicate that Realm used to generate an inner class per field, where that inner class then provided all the methods for between(), contains() and other relevant queries for that field. If you do that then yes, the method count would quickly explode. I agree that's bad!

In my library (https://github.com/buchandersenn/android-realm-builders) I've included some "hardcoded" classes, such as StringConditionBuilder, BooleanConditionBuilder and so on. These classes contains the type safe query methods. It's about 170-180 methods all in all. But the trick is that those methods only need to be included once. The 180 methods are a sort of "base cost" for type safety and autocomplete. Once the base cost is paid, I only have to generate one method per field in the Realm entities.

That is, a project containing 3 Realm entity classes, with 4 fields in each, would "cost" about 180 + 3 x 4 = 192 extra methods.

I can live with that cost.

The DIY-approach worked fine for me, so I don't mind that Realm doesn't include this feature in the main project. It was really fun experimenting with annotation processors and java poet to generate this stuff, and I found it quite useful in my project. With the open source policy followed by the Realm team, I could see a growing set of useful add-on projects being developed by third-parties. The only issues are discovery and maintenance of such projects. I must admit that I'm rather bad at keeping library projects up-to-date whenever I'm not actively using the projects myself :)

letronje commented 7 years ago

If you use Kotlin you can use Model::field.name to refer to the field in a type safe way. e.g. Person::profileUrl.name

The .name makes it a bit verbose, but I can live with it for now.

Also works well if you refactor field names

heinrichreimer commented 6 years ago

@letronje This wouldn't be much of a problem. You could easily define the builder like this:

fun <E : RealmModel, T> RealmQuery<E>.equalTo(field: KProperty<T>, T value)
    = equalTo(field.name, value)

Given a class Foo:

open class Foo: RealmObject() {
    @PrimaryKey 
    val id: Int
    val bar: String
}

You could then call it like this:

Realm.where<Foo>()
        .equalTo(Foo::bar, "bar")
// -> Success

Realm.where<Foo>()
        .equalTo(Foo::id, "bar")
// -> Compiler error

Realm could also check if the property is from the same class like this:

class RealmQuery<E: RealmModel> {
    val clazz: KClass<E>
    ...
    fun <T> equalTo(property: KProperty<T>, T value){
        if (property !in clazz.members) {
            throw IllegalArgumentException("Realm class $clazz doesn't contain property $property")
        }
        return equalTo(field.name, value)
    }
}
PawanDalal commented 4 years ago

Is this in WIP? Would love to switch to realm. I use mongodb on backend which provides type safe querying via morphia. I am thinking to get realm on client side as mongodb and realm getting aligned under one umbrella.

bmunkholm commented 4 years ago

No this is not in progress at the moment. And it's unfortunately not on the top of our priorities right now.

heinrichreimer commented 4 years ago

@PawanDalal In fact you could just define Kotlin extension functions:

fun <E : RealmModel, T> RealmQuery<E>.equalTo(
    property: KProperty1<E, T>,
    value: T
) = equalTo(property.name, value)

No need for implementation by Realm. If you'd like type-safe support, just write a library.