realm / realm-java

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

Support MutableRealmInteger #4266

Closed cmelchior closed 7 years ago

cmelchior commented 7 years ago

(The original initial comment for this issue is here. This heading reflects the current understanding of the project)

The basic semantics for this feature, now called MutableRealmInteger have finally been sorted out here.

This issue now tracks implementation.

TODO:

cmelchior commented 7 years ago

Cocoa implementation is here: https://github.com/realm/realm-cocoa/pull/4744

Zhuinden commented 7 years ago

I actually am not sure what this is for exactly.

cmelchior commented 7 years ago

It is only useful in a synchronised setting, but there it is very useful any time you want to count anything, since a normal integer field would get overridden with last-write-wins which means you could "lose" something you counted. A Counter CRDT is guaranteed to always converge with the semantics that increments from all devices are included.

Some use cases:

bmeike commented 7 years ago

Are RealmCounter and RealmInteger different things? I like the name RealmCounter better... It seems to me that the feature of this thing is that it is a counter, not that it is an integer,

cmelchior commented 7 years ago

@bmeike See https://github.com/realm/realm-object-store/issues/357 for the discussion with the rationale for the name.

cmelchior commented 7 years ago

Some description of use cases for the difference between managed/unmanaged states

// Unmanaged variants
RealmInteger ri = new RealmInteger()
ri.isManaged() == false
ri.isValid() == false

// Managed variant
realm.beginTransaction();
AllTypes obj = realm.createObject(AllTypes.class);
realm.commitTransaction();
RealmInteger managedRi = obj.getRealmInteger();
managedRi.isValid() == true
managedRi.isManaged() == true;

// Deleting object will invalidate it
realm.beginTransaction()
obj.deleteFromRealm();
realm.commitTransaction();
managedRi.isValid() == false
managedRi.isManaged() == true
bmeike commented 7 years ago

A further clarification of semantics:

party1 = new Party();
party2 = new Party();
party1.increment(1);
party2.guests = party1.guests;
party2.guests.longValue() == 1; // true // value is copied
party1.guests.equals(part2.guests) // true // objects are considered equal
party1.guests == party2.guests // false -> different objects

// Mixed behaviour, ignore missing transactions
Party party1 = realm.createObject(Party.class);
Party party2 = new Party();

party1.guests.increment(1);
party2.guests = party1.guests;
party2.guests.longValue() == 1; // true
party1.guests.equals(party2.guests) // false -> One is managed, other is not
party1.guests == party2.guests // false -> different objects

// Managed behaviour
Party party1 = realm.createObject(Party.class);
Party party2 = realm.createObject(Party.class);

party1.guests.increment(1);
party2.guests = party1.guests;
party2.guests.longValue() == 1; // true
party1.guests.equals(party2.guests) // false -> Both are managed, and have the same value, but point to two different rows.
party1.guests == party2.guests // false -> different objects
bmeike commented 7 years ago

... and more clarification. Assuming:

public class Party extends RealmObject {
   // ...
   public RealmInteger guests;
}

First of all, guests is null. It could be initialized on creation and, possibly, final (... requiring this might be interesting). If it is not, though, we need at least this:

party1 = new Party();
party1.guests = new RealmInteger(0);
party2 = new Party();
party2.guests = new RealmInteger(30);
party1.guests.increment(1);
party2.guests = party1.guests;
party2.guests.longValue() == 1; // true: value is copied
party1.guests.equals(party2.guests); // true: objects are considered equal
party1.guests == party2.guests; // false: different objects

This is definitely do-able. It is interesting, though, to compare this to what happens if the references in the scenario are NOT members of a RealmObject:

party1Guests = new RealmInteger(0);
party2Guests = new RealmInteger(30);
party1Guests.increment(1);
party2Guests = party1Guests;
party2Guests.longValue() == 1; // true: value is copied
party1Guests.equals(party2Guests); // true: objects are considered equal
party1Guests == party2Guests; // true: duh! you just assigned it 3 lines ago!

Note that, even if it were possible to change this behaviour, we certainly better not.

bmeike commented 7 years ago

I'd like to suggest the following alternative semantics. They seems pretty consistent, to me. The one thing that I can think of, that this table doesn't completely address, is how we initialize the value of a Realm-created RealmInteger. That is, assuming class Party, from the example above, and the rules below, what is the value of realm.createObject(Party.class).guests? I would suggest it should be a RealmInteger created as part of creating the Party object and reflecting the current value of the underlying core value.

semantics
bmeike commented 7 years ago

Adopting the semantics suggested in the table above, the sample code would work like this:

party1 = new Party();
party1.guests = new RealmInteger(0);
party2 = new Party();
party2.guests = new RealmInteger(30);
party1.guests.increment(1);
party2.guests = party1.guests; // legal: both objects are unmanaged
party2.guests.longValue() == 1; // true: party2's guests were replaced by party1's
party1.guests.equals(part2.guests); // true: objects have the same value
party1.guests == party2.guests; // true: same object

// Mixed behaviour, ignore missing transactions
Party party1 = realm.createObject(Party.class); // assuming party1's guest count, as known to core, is 0
Party party2 = new Party();
party2.guests = new RealmInteger(30);
party1.guests.increment(1); // throws outside a transaction
party2.guests = party1.guests; // throws: can't put a managed RealmInteger into an unmanaged object
party2.guests.set(party1.guests.getLong()); // legal anywhere: party2 is unmanaged
party2.guests.longValue() == 1; // true
party1.guests.equals(party2.guests); // true: both objects have the same value
party1.guests == party2.guests; // false: different objects, one is managed, the other is not
party1.guests = party2.guests; // throws: assignment to a managed RealmInteger ref not allowed
party1.guests.set(party2.guests.getLong()); // works in a transaction, fails outside

// Managed behaviour: ignore missing transactions and initialization.
Party party1 = realm.createObject(Party.class); // assuming party1's guest count, as known to core, is 0
Party party2 = realm.createObject(Party.class);
party1.guests.increment(1);
party2.guests = party1.guests; // throws.  assignment to a managed RealmInteger ref not allowed
party1.guests.set(party2.guests.getLong()); // works in a transaction, fails outside
party2.guests.longValue() == 1; // true
party1.guests.equals(party2.guests); //  true -> both objects have the same value
party1.guests == party2.guests; // false -> different objects
cmelchior commented 7 years ago

Some notes:

I could live with the semantic outlined in https://github.com/realm/realm-java/issues/4266#issuecomment-305333092, I'm still a bit uneasy about party1.guests = party2.guests; // throws: assignment to a managed RealmInteger ref not allowed though, but the opposite would be akin to operator overloading (replacing = with original.set(otherRi.longValue() which could probably also throw some people of. Your approach makes the behavior explicit. The downside is that the exception will not be thrown until runtime since it would legal when writing it in the editor.

In any case, this is probably fine for the first implementation of this. Depending on feedback, it also seems possible to lift the restriction that you cannot assign to managed RealmIntegers later. Going in the other direction would definitely be more painful.

bmeike commented 7 years ago

Got it.

FWIW, note that the RealmInteger actually is a bit like@LinkingObjects. The reference in the containing object is not live and does not get updated. It is only the value to which the RealmInteger is a reference, that is live. This is very different, from, say, a reference to an Integer which is not a reference to an updatable thing but actually the thing that is updated.

Also, I completely agree about party1.guests = party2.guests; // throws: assignment to a managed RealmInteger ref not allowed. If we can think of some consistent meaning for it, I'd be all over it.

Proceeding under the assumption that we have a basic plan, here, and will probably need only to touch it up, later.

zaki50 commented 7 years ago

I don't think we should interfere unmanaged model object. We should always allow party1.guests = party2.guests; if party1 is unmanaged in order to keep Java's semantic in unmanaged object.

And I think we already allow to assign managed RealmList to a field in unmanaged object.

bmeike commented 7 years ago

There is discussion about the setter for a RealmInteger. I submit the following as my train of thought concerning the issue. I am, of course, open to suggestion.

The user creates a field:

private RealmInteger myCounter;

The signatures getter/setter that every Java dev expects (and that are auto-generated by IDEs) for this field, are:

public void setMyCounter(RealmInteger myCounter)
public RealmInteger getMyCounter()

These methods do not take/return Objects. They do not take/return Long or Integer. They do not take/return Number. They take/return RealmIntegers. The other possibilities require runtime checking that should have been compile time checking.

While I hold the view with equal strength, my argument for disallowing the assignment of a managed RealmInteger, to an unmanaged RealmObject is not as clear. It is, essentially, that I cannot think of any circumstances that it is not a mistake. All of a sudden, your unmanaged object cannot be passed between threads! All of a sudden some parts of your unmanaged object can be changed only in a transaction. To me, it doesn't make sense.

Some of the confusion may be due to a misunderstanding about what is a reference to what. A java Integer and a java int are the same in the following respect: you cannot assign them. Forgive me if this is all obvious. I'm not certain we are on the same page.

You can do this:

 class C {
  private int x = 3;
  private Integer y = Integer.valueOf(4);

  {
    x = 5;
    y = Integer.valueOf(6)
  }

... but you cannot do this:

  {
    x.set(7);
    y.set(Integer.valueOf(8));
  }

The location called x holds a value. The location y hold a reference to an immutable value. Because it is immutable, it is almost exactly as if y held the value itself.

RealmIntegers are not immutable. You can do this:

 class C {
  private RealmInteger z = RealmInteger.valueOf(11);

  {
    z.set(42);
  }

A RealmInteger does not function as a value. Instead, it functions as a reference to a reference to a native value. Two references. Like this:

C.z -> RealmInteger1 -> native value

so, re-assigning C.z does this:

             RealmInteger1  ----- \
  C.z   ->   RealmInteger2  -> native value

Exactly nothing has happened.

We absolutely could change these semantics. We could pretend that the middle reference didn't exist. We can only do it for RealmObjects, though. If we do it, as I pointed out previously, it makes the behavior of perfectly vanilla assignment different for fields of RealmObject than anywhere else.

I argue against doing that.

beeender commented 7 years ago

If umanagedRealmInt = managedRealmInt is not allowed, then managedRealmInt1 = managedRealmInt2 should not be allowed as well if they point to different rows. But those behaviours are quite different from how we support boxed types.

zaki50 commented 7 years ago

We are adding hidden setters/getters (such as realmSet$foo()) only for our proxy's intercepting field values. We should not do more than that even if users are doing wrong thing.

And we already have RealmList which is similar to RealmInteger. Those are references to internal native data. We should not introduce different rules to them.

If we change the rule, it should be happen in the next major version as a breaking change.

cmelchior commented 7 years ago

Sorry for wall of text. Also feel free to edit the comment if you think I didn't represent your oppinion correctly. TLDR: What a mess :/

If umanagedRealmInt = managedRealmInt is not allowed, then managedRealmInt1 = managedRealmInt2 should not be allowed as well if they point to different rows. But those behaviours are quite different from how we support boxed types.

I think that is @bmeike point. RealmInteger isn't a boxed type, it is a weird mix between a true object and a datatype wrapper. That it isn't immutable is what is causing a big headache, but I agree that if we disallow one, we should probably disallow the other as well.

And we already have RealmList which is similar to RealmInteger. Those are references to internal native data. We should not introduce different rules to them.

RealmList is something slightly different, but that said, you are right. We already have precedence for referencing managed data from un-managed datatypes. This is e.g useful when preparing objects for copyToRealm(). The consistency argument is a pretty strong one IMO, especially in the light of clear "correct" alternatives.

Let me try to summarize the advantages / disadvanteges so far. Perhaps that will make agreeing on something easier:

Option 1:

Allow unmanagedObj.realmInteger = managedObj.realmInteger and managedObj1.realmInteger = managedObj2.realmInteger Basically treat RealmInteger as a datatype instead of an object reference. We will copy it when needed.

Advantages

Disadvantages


Option 2

Disallow unmanagedObj.realmInteger = managedObj.realmInteger and managedObj1.realmInteger =managedObj2.realmInteger``

We elevate RealmInteger to a "special" type that cannot be assigned once managed, but need to be manipulated using methods. This is so the underlying implementation is more visible to users.

RealmList does something similar as if you do managedObj1.list = managedObj2.list it will actually copy all values from obj2 to obj1: https://github.com/realm/realm-java/blob/master/realm/realm-annotations-processor/src/test/resources/io/realm/AllTypesRealmProxy.java#L351

Advantage

Disadvantage


Secret option 3

Treat RealmInteger as a first class object reference instead of a row value. This allow multiple objects to reference the same "Counter" and will make it behave like a RealmObject reference.

Advantages

Disadvantages


Secret option 4

Treat RealmInteger as an immutable datatype. Don't allow modifications in-place. set()/increment()/decrement() all return the modified value, instead of modifying in place

Advantage

Disadvantages

nirinchev commented 7 years ago

For .NET, I'm fairly certain RealmInteger will be a mutable reference type. We'll probably introduce implicit conversion operators so you can use it without casting in methods that expect integers.

It will likely have to keep a reference to its owning object and property index like:

public class RealmInteger
{
    internal RealmObject Parent { get; }
    internal int PropertyIndex { get; }
}

Additionally, we'll generate a setter in the parent object that will call Reset rather than replace the object reference. So in the case of firstObj.RealmInteger = secondObj.RealmInteger, under the hood, the setter will get the value stored in RealmInteger and call firstObj.RealmInteger.Reset(secondObj.RealmInteger.Value). With this approach, we don't care whether the parent objects are managed or not as no transfer of ownership occurs. Obviously, this will be subject to the threading confines that all RealmObjects are, but I guess our users would expect that.

bmeike commented 7 years ago

@zaki50 has convinced me that it is more consistent, albeit abhorrent, to allow the assignment of a managed RealmInteger to an unmanaged subclass of RealmObject. Changing the implementation.

beeender commented 7 years ago

actually @cmelchior 's option 4 looks very charming to me :P but it will totally create a mess if user decides to have a public RealmInteger field.

bmeike commented 7 years ago

Yeah, I agree, #4 is nice. Making it immutable would be a good thing. Note that it has an affect on the getter too:

RealmInteger obj = managedObject.getRealmInteger();

obj is not a live object. It's value does not change. In order to find the current value of the counter, you have to ask again.

The alternative would be a bit weird: you cannot change the value of the RealmInteger, but it might change value behind your back. Certainly possible to implement, but, I think, way weird.

bmeike commented 7 years ago

After an extended and enlightening discussion with @nirinchev , I think that Java is sufficiently different (cannot change the meaning of ==) so that we cannot use .NET as a model.

Zhuinden commented 7 years ago

Option 2 makes most sense to me as you can always just do unmanagedObj.realmInt = new RealmInt(managedRealmInt)

bmeike commented 7 years ago

I don't understand Option #3. I am currently implementing Option #1, based, as I said, on @zaki50's completely convincing argument.

bmeike commented 7 years ago

@Zhuinden : @zaki50 convinced me with the following argument. We support:

class C extends RealmObject {
   public C myC;
}
// ...
C aC = new C();
aC.myC = realm.where(C.class).findFirst().myC;

if we support that, we kinda have to support assignment of a managed RealmInteger to an unmanaged instance of a subclass of RealmObject.

Not that I don't like your solution better. It isn't consistent, though.

zaki50 commented 7 years ago

If we prohibit to call setters of RealmInteger field, we need to provide a way to nullify and un-nullify (is this correct wording?) the field.

zaki50 commented 7 years ago

I think we have two points not discussed yet.

One is RealmInteger field can be nullable or not. I think it can be nullable, but no deep discussion yet. Another is how we define the semantics of Realm.*OrUpdate() methods. In my understanding, current semantic of update() is that passing each field value to another's field respectively. If we prohibit to assign RealmInteger to a field in managed Realm Object, Realm.*OrUpdate() does not work against objects that have RealmInteger field anymore. If we allow to work Realm.*OrUpdate() against those Realm objects, I think that we should allow to assign RealmInteger to managed Realm object as well.

What do you think? @realm/java

cmelchior commented 7 years ago

Field can definitely be nullable, and prohibiting copyOrUpdate would also be really strange.

bmeike commented 7 years ago

I can't imagine plausible implementation in which *OrUpdate didn't work. No idea where that came from. Is it a requirement that it works by calling the synthetic setter? Totally didn't know that!

Also, please clarify what it means to set a counter to null? To me that seems to be the same thing as setting an int to null. If you can define semantics, though, I can write the code!

nirinchev commented 7 years ago

We have to keep in mind that under the hood, the counter is just an integer, so making it required would be artificial limitation. Not sure what the increment and decrement instructions will result in when applied on null though. Probably someone from the core team can share? cc @finnschiermer

bmeike commented 7 years ago

Yeah. That discussion is complicated, though, by the fact that Java has two kinds of integer, int and Integer. Former is not nullable; latter is. I'm more interested in what it means. The operations on a RealmInteger are set, increment, and decrement. There are no corresponding operations on int or Integer.

A better comparison might be RealmList. What happens if, in a shared Realm, device X sets a RealmList to null, at the same time that device Y adds two items to the exact same RealmList?

bmeike commented 7 years ago

Just talking to the guys here in the SF office. Is it the case that we do not allow nulling a RealmList? If so, I'm back in the "we shouldn't allow nulling a RealmInteger" camp

nirinchev commented 7 years ago

RealmList is not nullable because it doesn't make sense to be, but I'm not sure if it's relevant to the integer discussion.

@simonask can you shed some light on how a set(null) is merged with increment(1) on an integer column?

I feel we should not place arbitrary restrictions unless those are imposed by core or sync. As a very artificial example, I can have a Timer object with a RealmInteger Seconds property and I may want to differentiate between a timer that hasn't been started (Seconds == null) and a timer that has just started (Seconds == 0). Obviously, this can be modeled differently, but that's generally the case for most scenarios involving nullable types, yet we allow them :)

bmeike commented 7 years ago

@nirinchev Just to be sure we are on the same page, here: There are two objects in Java (one more way to look at it: there is no such thing as a type alias, in Java) . In Java, there will be an artificial object, a mutable RealmInteger that is a reference to the core value. Note that that is much more similar to a RealmList, a container for values, than it is to an integer valued field.
In Java, we will be able to control what happens when you use the assignment operator (=) when it is used in the context of a subclass of RealmObject. We will not be able to control what it does outside that context. Here is what an Integer valued field looks like in Java: x[ ] --> i That is, there is an object with a field that contains a value "i". If you want to add one to i, you get j: p = *x x[ ] --> j *x != p // true: x points at a new thing. i and j are different things. This is true for both int and Integer That is not the way counters work. Counters work like this: x[ ] --> m[ ] --> i That is, there is an object with a field that contains a reference to a container that contains a value. In particular, adding one to the value of i does not change x's value: p = *x x[ ] --> m[ ] --> j *x == p // true. x is still pointing at the same thing. Under these circumstances, the effect of nulling out x's reference seems unclear to me: x[ ]         i The value i is still there. It can no longer be changed from x, though. I have argued in the past, btw, that assigning x's reference at all seems of dubious value. It does this: x[ ] --> n[ ] --> i             m[ ] It has not changed the value i. It has change x's reference. The current implementation will eliminate that problem by translating all assignments to x's reference into assignments to m's value. That will work only when assigning to a field on a RealmObject, though. As I pointed out above, though, assignment will have its normal meaning everywhere else. I admit to finding the inconsistency quite bothersome.

beeender commented 7 years ago

Just talking to the guys here in the SF office. Is it the case that we do not allow nulling a RealmList? If so, I'm back in the "we shouldn't allow nulling a RealmInteger" camp

We don't allow null for RealmList is simply because of core doesn't support it which it should since an empty list and a null list are totally two different things.

simonask commented 7 years ago

@simonask can you shed some light on how a set(null) is merged with increment(1) on an integer column?

Yes, the increment is discarded.

Note that Core considers an increment on a null value as a logic error. This means that as opposed to Set with a value, which only discard AddInteger instructions with a lower timestamp (resetting the counter to a particular value), Set(null) discards even AddInteger instructions with a higher timestamp (since there is no way to apply the addition to a null value).

cmelchior commented 7 years ago

@bmeike We have the same behaviour elsewhere, because the Java objects are just pointers into the underlaying data, which means that things can change under the hood, so things like "null" somethings get represented slightly weird:

Person p =  realm.where(Person.class).findFirstAsync(); // Works like an optional/future kind of thing
p.isLoaded(); // = false, object represents the intent to load things
p.load();
p.getName(); // Now points to data
p.deleteFromRealm(); // Object effectively become "null"
p.isValid(); // = false, indicating that the wrapper is now "empty"

// RealmInteger is also a "wrapper" type like the `Person` above
Party p = realm.where(Party.class).findFirst();
RealmInteger guestsRef = p.getGuests();
guests.longValue(); // 0
p.guests = null; 
guestsRef.longValue(); // Crash, since it is now null?

// We could allow RealmInteger to contain the value "null" while still not allowing the
// reference to the RealmInteger to be null in Java. Having such requirements 
// are really awkward although we already have it on RealmList. 
p.guests = null; // Never allowed.
p.guests.setNull(); // is Allowed
p.guests.set(null); // or this. 
p.guests.isNull(); // null check 

To be honest, I stand with @bmeike on this. All the different versions we are discussing have flaws that are extremely weird in different cases and this whole debate really shows just how wrong the conclusion we came to in https://github.com/realm/realm-object-store/issues/357 is.

While I'm afraid to blow this whole discussion wide open again, I feel we need to take a step back.

Trying to create a datatype that functions as both an Integer and a Counter is just fundamentally not going to work well due to restrictions in the languages we want to support.

IMO the absolute simplest solution would be:

// New datatype shipped with Realm that works like an object
// Object Store is responsible for adding the class to the Realm files.
public RealmCounter extends RealmObject {
  private long value;  
  public void increment(long val) { ... }
  public void decrement(long val) { ... }
  // ...
}

// My app model class
public class Party extends RealmObject {
  // Normal object reference like any other
  public RealmCounter guests = new RealmCounter(42);
}

// Default values implementation should automatically create it.
Party p = realm.createObject(Party.class); 
p.guests.longValue(); // 42

// This is just a normal mutable object reference like any other
// It can be added to both managed and unmanaged objects just like other Realm objects
RealmCounter guestsRef = p.guests; 
new Party(guestsRef)
Party p2 = realm.createObject(Party.class).setGuests(guestsRef)
// p2.guests point to the same object as p.guests 

// Only problem is what happens if you delete object
// Without support for cascading deletes, removing the counters
// again will be slightly cumbersome. 
// I'm actually fine with this as we will have cascading deletes down the line
p.deleteFromRealm(); //

Consequences:

Thoughts @bmeike @nirinchev @austinzheng ?

beeender commented 7 years ago

@cmelchior I don't quite agree with that. Yeah, the proposal makes the semantics closer to RealmObject, but further from RealmList.

RealmList list1;
obj1.setList(list1);
obj2.setList(obj1.getList());
obj2.getList().add(1);

print(obj1.getList().size()) // 0
print(obj2.getList().size()) // 1

I think making the Counter with that semantics make our situation unnecessary complex. The semantics to RealmInteger should be similar to the RealmListif it is good enough to solve the original problem for us.

simonask commented 7 years ago

This seems wrong:

public RealmCounter extends RealmObject {

... unless RealmObject means something totally different in Java than what I thought?

cmelchior commented 7 years ago

No, this is exactly what I mean. RealmCounter is a separate class with one INTEGER column named value.

zaki50 commented 7 years ago

@cmelchior I agree with mulong and I think your new proposal is losing a way for counters to be @Required.

cmelchior commented 7 years ago

@zaki50 Not sure what you mean by " losing a way to counters to @Required."

But @zaki50 / @beeender what do you suggest we do then?

zaki50 commented 7 years ago

@cmelchior In your proposal, I don't think users can mark guests field as @Reqired since it is a relationship to another object.

public class Party extends RealmObject {
  @Required // Users can't do like this
  public RealmCounter guests = new RealmCounter(42);
}

At least, current @bmeike 's implementation is allowing that.

cmelchior commented 7 years ago

Yes, that would be a drawback. That is correct. If it is a problem or not is up for debate though.

nirinchev commented 7 years ago

I feel strongly that counters should allow for being required. That way, I can use the type safely without worrying about null checks and stuff :)

beeender commented 7 years ago

hmmm, now I got the point, nulling is a problem:

public class Something extends RealmObject {
    RealmInteger counter;
}

Something obj = realm.where(Something.class).findFirst();
Counter counter = obj.getCounter();
obj.setCounter(new RealmInteger(1));
counter.longValue(); // 1
obj.setCounter(new RealmInteger(2));
counter.longValue(); // 2

obj.setCounter(null);
counter.longValue(); // NOTHING would be correct to return here.

I am actually thinking another approach to make the situation simpler, since what we need is the operation for increment/decrement, so something like a static method somewhere would be good enough without introducing a new type?

public class RealmObject {
    public static incrementIntegerField(String fieldName, long value);
    public static decrementIntegerField(String fieldName, long value);
}

with some annotation tricks, it might be still easy to use.

zaki50 commented 7 years ago

@beeender I guess you mean counter.set(null); counter.longValue(), but RealmInteger.set() takes long as an argument.

beeender commented 7 years ago

@zaki50 Sorry, put a wrong example. It was just updated.

zaki50 commented 7 years ago

@beeender got it. I was thinking the same thing and writing my proposal.

zaki50 commented 7 years ago

About managed RealmObject:

public class Foo extends RealmObject {
    public RealmInteger realmInteger = RealmInteger.valueOf(42);
}

Foo managed0 = realm.createObject(Foo.class);

// comparing the same field
managed0.realmInteger == managed0.realmInteger // true. managed object caches `RealmInteger` instance like `RealmList`.

// comparing RealmIntegers those have the same value but belong to different managed object 
Foo managed1 = realm.createObject(Foo.class);
managed1.realmInteger == managed0.realmInteger; // false.
managed1.realmInteger.equals(managed0.realmInteger); // true. equals only check the current values

// after a field becomes null
RealmInteger oldManaged1Value = managed1.realmInteger;
managed1.realmInteger = null; // internally cached RealmInteger is also disposed.
managed1.realmInteger // returns null
oldManaged1Value.isValid() // false
oldManaged1Value.longValue();// throws IllegalStateException since the oldManaged1Value is invalid. same with deleted RealmObject

// assigning a value to null field
managed1.realmInteger = RealmInteger.valueOf(100)
managed1.realmInteger == oldManaged1Value; //false. Once new value is set to null field, getter returns new RealmInteger (and cache it).
managed1.realmInteger.longValue(); // of course 100

// assigning managed RealmInteger
managed1.realmInteger = managed0.realmInteger; // identical to managed1.realmInteger.set(managed0.realmInteger.longValue);
managed1.realmInteger == managed0.realmInteger; // false. since each getter returns its cached RealmInteger.
managed1.realmInteger.longValue(); // 42