Realm-java: Default values for fields / assignment in default constructor

Created on 22 Jan 2015  路  23Comments  路  Source: realm/realm-java

Question/request:
Will the java version have default values for fields similar to the iOS one?

i.e.
+defaultPropertyValues can be overriden to provide default values every time an object is created.

This might also helps to alleviate the lack of null support in the interim (and with the standalone object support now, helps with gson deserialization a bunch)

Thanks for all the awesome work on this!

T-Feature

Most helpful comment

Proposed API:

@DefaultValue(42)
private int foo

@DefaultValue("foo")
private String foo

Would not work for RealmObject and RealmList references. Should probably be disallowed for fields annotated with @PrimaryKey.

Cocoa doc is here: https://realm.io/docs/objc/latest/#default-property-values

We need to align on the behaviour across bindings. Especially if the default values should be part of the Realm file schema information

All 23 comments

Hi @erichkleung
We actually havn't considered it yet. It might be slightly more tricky to implement in Java especially for stand alone objects as we don't do any magic on those. I will add it to our TODO to look into, but can't make any promises.

+1

I got a question. If my JSON does not contain a boolean field, what is the value in the database? Null? Current verison is 0.84

@Ralphilius It depends on what methods you are using

1) If you are creating a new object, it will get the default value for that datatype, e.g. false for boolean and null for Boolean.

2) If you are updating an existing object, it will just ignore that field, so it will keep whatever value it already had.

Thanks @cmelchior for clarifying

My case turns into number 1.

Proposed API:

@DefaultValue(42)
private int foo

@DefaultValue("foo")
private String foo

Would not work for RealmObject and RealmList references. Should probably be disallowed for fields annotated with @PrimaryKey.

Cocoa doc is here: https://realm.io/docs/objc/latest/#default-property-values

We need to align on the behaviour across bindings. Especially if the default values should be part of the Realm file schema information

Does annotation work with Date as well?

Ideally yes, but it looks like there is a major limitation for annotations. It looks like we cannot do:

public @interface DefaultValue {
    Object value();
}

List of possible annotation values here: http://stackoverflow.com/questions/1458535/which-types-can-be-used-for-java-annotation-members

That pretty much makes my proposal impossible :(

More thoughts for the default value

  • Consistency with the creating standalone object by new
  • Do we have to maintain a history of default values for migration? Especially for the primary key fields.

After we merged our RealmTransformer we should be able to just use normal Java constructors + field assignment for default values instead of adding a new annotation:

The below is a request for such a feature:


Expected Results

Adding an object to a realm using Realm.createObject() should respect the field values set in the default constructor or the value the field is initialized with in the field declaration.

Actual Results

Using Realm.createObject() adds an object with all fields set to null, 0or false.

Code Sample

Code Sample

public class Model extends RealmObject {
    public String string = "Success!";

    public Model() {
        this.string = "Success!";
    }
}

realm.beginTransaction();
Model obj = realm.createObject(Model.class);
assertTrue("Success!", obj.string); //This should work
realm.commitTransaction();

Version of Realm and tooling

Realm version(s): 0.88.2

Android Studio version: 2.1 Preview 4

Which Android version and device: 6.0.1 (MHC19J) on Nexus 5X

The @DefaultValue actually looks much better than any other solutions. For the Date issue, how about we solve it through:

public Foo extends RealmObject {
    @DefaultValue(DefaultDateGenerator)
    public Date;
    class DefaultDateGenerator implements DefaultValueGenerator {
        public Object value() {
            return new Date();
        }
    }
}

An other strange idea about this. It would be very convenient that if we just use the default field value. So:

public class Foo extends RealmObject {
    private int a = 6;
}

After annotation processing

public class FooRealmObjectProxy extends Foo {
    public static Foo createDefault() {
        return null;
    }
}

We insert a constructor during byte code transforming and if user doesn't define a default constructor, we insert one as well:

public class Foo extends RealmObject {
    private int a = 6;
    public Foo () {
    } 

    public Foo(RealmDefaultConstructor rdc) {
    }
}

Then modify the byte code of proxy:

public class FooRealmObjectProxy extends Foo {
    public static Foo createDefault() {
        return new Foo(RealmDefaultConstructor.staticInstance);
    }
}

One problem above is this creates overhead if user have some giant @Ignore fields, but maybe it is good enough that we document it?

And above idea should solve #2536 as well since the mediator could call FooRealmObjectProxy.createDefault() instead.

@beeender What is the RealmDefaultConstructor?

The challenge is that when you instantiate the class, the super class is created first, this means that any field you set will call our overridden getter/setter which will throw a NPE because our internal state hasn't been set yet.

public class Foo {
 private String foo = "Boom"; // this is  really realmSet$foo("Boom')

public Foo() {}

 public realmSet$foo(String s) {
      foo = s;
 }
}

public class FooRealmProxy {
  public FooRealmProxy() {
    // Java does not allow you to have anything here, not even through bytecode manipulation
    super(); 
    row = getRow();
  }

 @Override
 public realmSet$foo(String s) {
      row.setString(2, s); // row is null if called from Foo's constructor 
 }
}

At least that was some of the problems I ran into when playing around with it last

If we allow to invoke realmSet$foo() in its model's constructor, we can't avoid to have null check for Row in each realmSet$*() since there is no way to set Row instance before execution of model's constructor.

if having null checks in getters is acceptable, one solution is that

    public void realmSet$fieldString(String value) {
        // added
        if (proxyState.getRealm$realm() == null) {
            // called in model's constructor. store value to thread local instead.
            Map<String, Object> defaultValues = RealmObject.defaultValues.get();
            defaultValues.put("fieldString", value);
            return;
        }

        // original 
        proxyState.getRealm$realm().checkIfValid();
        if (value == null) {
            proxyState.getRow$realm().setNull(columnInfo.fieldStringIndex);
            return;
        }
        proxyState.getRow$realm().setString(columnInfo.fieldStringIndex, value);
    }

and add one more method applyDefaultValues$realm() to each proxy class.

    public void applyDefaultValues$realm(boolean skipPrimaryKey) {
        proxyState.getRealm$realm().checkIfValid();
        final Row row = proxyState.getRow$realm();
        for (Map.Entry<String, Object> entry : RealmObject.defaultValues.get().entrySet()) {
            final Object value = entry.getValue();
            switch (entry.getKey()) {
                case "fieldString":
                    realmSet$fieldString((String) value);
                    break;
                case: "fieldLong":
                    if (skipPrimaryKey) {
                        continue;
                    }
                    realmSet$fieldLong((long) value);
                    break;
                ...
            }
        }
    }

A code to create proxy instance (BaseRealm#get(Class)) becomes:

    <E extends RealmModel> E get(Class<E> clazz, long rowIndex) {
        Table table = schema.getTable(clazz);
        UncheckedRow row = table.getUncheckedRow(rowIndex);

        RealmObject.defaultValues.get().clear(); // added
        E result = configuration.getSchemaMediator().newInstance(clazz, schema.getColumnInfo(clazz));
        RealmObjectProxy proxy = (RealmObjectProxy) result;
        proxy.realmGet$proxyState().setRow$realm(row);
        proxy.realmGet$proxyState().setRealm$realm(this);
        proxy.realmGet$proxyState().setTableVersion$realm();

        ((RealmObjectProxy) result).applyDefaultValues$realm(true) // added
        RealmObject.defaultValues.get().clear(); // added

        return result;
    }

This can be done in annotation processor.
How about this?

As a default adding anything to our accessors should be done with extreme care since that is our hottest code path. That said, our already existing thread check + JNI call will most likely take orders of magnitude more time.

So having a null-check in the accessors and find a way to set the default values afterwards is probably the best we can do.

@cmelchior

public class Foo {
 private String foo = "Boom"; // this is  really realmSet$foo("Boom')
}

I don't think this will call our overwritten setter since the value is called before constructor. And we only insert an empty Constructor. Right?

@zaki50 RealmDefaultConstructor is just a dummy class to enable us to overload a constructor that user will never create. Also with this, it doesn't require user to have a default constructor anymore.

Also, since it is an empty constructor created by us, we are sure no setters/fields and accessing in the constructor anymore, so the mediator will call this constructor instead of the default one which could be created by user, so the overwritten setter problem in #2536 should not exist anymore. Right? Or i missed something?

I still don't have clear picture on how you will actually set the default values?

@cmelchior See comments in below

public class Foo extends RealmObject {
    private int a = 6;
    public Foo () {
    } 

    // This is the constructor we inserted through byte code transforming
    // When create an object with this constructor, the created object's  field a  will have
    // a default value 6
    public Foo(RealmDefaultConstructor rdc) {
    }
}

But you cannot set the realm reference in the proxy class before calling the constructor?
Having the null-check prevents it from throwing, but at some point you need to do a = 6 where it will actually work?

@beeender

public class Foo {
    private String foo = "Boom";
}

is compiled into

public class Foo {
    private String foo;
    public Foo() {
        foo = "Boom";
    }
}

and we can replace this field assignment with this.realmSet$foo("Boom")

I started implementing my concept https://github.com/realm/realm-java/commit/4bf3a852d71c750682c1bfe97c821ff7128ca6ea

this implementation is a bit different what I wrote above.
I passed Realm and Row instead of Map for initial values via thread local (https://github.com/realm/realm-java/commit/4bf3a852d71c750682c1bfe97c821ff7128ca6ea#diff-9bbf8752325de6eed4a407ae41857948R122).

Still a few tests are failing though.

This also solves #2536

Was this page helpful?
0 / 5 - 0 ratings

Related issues

pawlo2102 picture pawlo2102  路  3Comments

cmelchior picture cmelchior  路  3Comments

bryanspano picture bryanspano  路  3Comments

Merlin1993 picture Merlin1993  路  3Comments

AAChartModel picture AAChartModel  路  3Comments