Skip to content

Conversation

@cmelchior
Copy link
Contributor

@cmelchior cmelchior commented Sep 19, 2017

Fixes #2476
Fixes #1616

This PR adds support for creating a mapping between internal names used by Realm and those used in the model classes which e.g. makes it easier to share schemas between languages without compromising the standard naming conventions in Java

it is also possible to apply a RealmNamePolicy for a class or module instead of being forced to manually override each and every field.

The basic implementation is a simple mapping scheme in Java before any relevant field or class name is sent to Object Store. As long as Object Store does not support a true dynamic mode, we cannot have this functionality living there.

// Set defaults on the module affecting all classes
@RealmModule(classNamingPolicy = RealmNamePolicy.CAMEL_CASE, fieldNamingPolicy = RealmNamePolicy.CAMEL_CASE, allClasses = true)
public MyModule {
}

// Set class specific overrides
@RealmClass(name = "!Person", fieldNamingPolicy = RealmNamePolicy.PASCAL_CASE)
public class Person extends RealmObject {

    // Field specific overrrides
    @RealmField(name = "internal_name")
    public string name;

}

Especially, the following semantics are in place:

  • Queries on Realm (typed Java model classes) still uses the names defined in Java
  • Queries on DynamicRealm uses the internal names.
  • Migrations and RealmObjectSchema uses the internal names. This might seem really annoying, but the primary use case is sync, where migrations are automatic.
  • This does not have any impact on importing from JSON which must still use the Java names. As discussed in @SerializedName annotation for defining Class/Field names #1470 this is two different concerns. It would be relatively straightforward to add a useInternalNameForJson to these annotations though Schema errors will use the internal names instead of the external ones.
  • RealmProxyObject toString() still uses Java names

Review Guide:

  • Biggest refactor is in the annotation processor where modules are now processed in two steps (pre- and post-). See ModuleMetaData. This covers detecting proper use of the introduced naming policies
  • ColumnInfo was refactored to store both external and internal names. Most refactors here are adding Javadoc + renaming variables to express intent better.
  • Quite a lot of unit tests added. See RealmNameTests for annotation processor tests that test converter logic + proper use of annotations
  • See CustomRealmNameTests for library unit tests that mostly test the functionality of queries and schema API's.
  • Original implementation used Guava for mapping names, but it was not powerful enough, so a custom built solution was added. I used a slightly changed variant of the one used in https://github.com/cmelchior/realmfieldnameshelper, so it should be fairly battle-tested.

TODO:

  • Figure out naming for the annotation and related classes
  • Determine the number of policies supported and any restrictions.
  • Implement query support
  • Implement schema support
  • Implement DynamicRealm support
  • Implement detection of conflicting policies for a class part of different modules.
  • Check that library and app modules do not conflict

Conflicts:
	realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java
	realm/realm-library/src/main/java/io/realm/internal/ColumnIndices.java
@Zhuinden
Copy link
Contributor

Zhuinden commented Sep 19, 2017

it is also possible to apply a RealmNamePolicy for a class or module instead of being forced to manually override each and every field.

I don't really get the "policy" thing (I know it's like from GSON, but...) I'd just expect something like a @RealmField annotation and if I want to set a different name, then set it.

Maybe that's just me though. I just think the policy is not as essential as the ability of setting a schema field name.

@cmelchior
Copy link
Contributor Author

cmelchior commented Sep 20, 2017

@Zhuinden The problem with @FieldName is that it doesn't work for classes or modules.

Originally I was playing around with overloading @RealmClass(name = "custom") and @RealmField(name = "custom), but it feels awkward to use two different annotations for the same thing. Also, it left the question of what to do on modules.

Modules are a bit of a special case though and allowing @RealmName on there complicates things quite a lot, so we could also consider not allowing @RealmName there. But that means there is no place to define a naming policy for an entire Realm (as the RealmConfiguration is not available to the annotation processor).

@Zhuinden
Copy link
Contributor

@RealmClass(name = "custom") and @RealmField(name = "custom), but it feels awkward to use two different annotations for the same thing.

I think it is the distinction between @Table/@Column or @JsonObject/@JsonField.

@cmelchior
Copy link
Contributor Author

I guess you are right. So there is precedence for that 👍
Having @RealmField would also be nice for other possible future enhancements. I guess we could overload modules the same way:

@RealmModule(policy = RealmNamePolicy.IDENTITY, allClasses = true)
public void MyModule {
}

@RealmClass(name = "custom)
public class Persons extends RealmObject {

  @RealmField(name = "bar)
  public String foo;
}

I kinda like that. It also means we can remove the recursive flag on e.g @RealmModule and @RealmField

@Zhuinden
Copy link
Contributor

@cmelchior The only tricky thing though is that you already have a @RealmClass annotation that you inherit from RealmObject!

@cmelchior
Copy link
Contributor Author

Yes, but that last one would take priority I would assume, but I haven't tested how the annotation processor actually behaves in that scenario

@cmelchior
Copy link
Contributor Author

Just for reference. This is how LoganSquare does it: bluelinelabs/LoganSquare@41572aa

# Conflicts:
#	realm/realm-annotations-processor/src/main/java/io/realm/processor/ClassMetaData.java
#	realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProcessor.java
#	realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java
…mModule, RealmClass and a new RealmField annotation.
# Conflicts:
#	realm-annotations/src/main/java/io/realm/annotations/RealmClass.java
#	realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java
# Conflicts:
#	realm/realm-annotations-processor/build.gradle
#	realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProcessor.java
import some.test.AllTypes;

public class NamingPolicyConflictingModules {
public class NamePoliceConflictingModuleDefinitionsForAllClasses {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Police!

# Conflicts:
#	CHANGELOG.md
#	realm/realm-annotations-processor/src/main/java/io/realm/processor/RealmProxyClassGenerator.java
@cmelchior
Copy link
Contributor Author

Ready for review @nhachicha
It still isn't parsing CI, but any changes to fix that should not effect the semantics.

@cmelchior cmelchior requested a review from nhachicha January 22, 2018 11:43
Copy link
Contributor

@kneth kneth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing doc; disclaimer: not a native English speaker.


/**
* The naming policy applied to all classes part of this module. The default policy is {@link RealmNamingPolicy#NO_POLICY}.
* To define a naming policy for all fields in those classes, use {@link #fieldNamingPolicy()}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"those" -> "the"

* This enum defines the possible ways class and field names can be mapped from what is used in Java
* to the name used internally in the Realm file.
* <p>
* Examples where this can be useful is e.g:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove "e.g"
"is" -> "are"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be right!! 😄

* </li>
* </ol>
* <p>
* Note, that changing the internal name does <i>NOT</i> effect importing data from JSON. The JSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No comma

* if set in {@link RealmClass#fieldNamingPolicy}, the module policy will still apply to field
* names.
* <p>
* If two modules disagree on the policy and one of them is {@code NO_POLICY}, the other one
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"other one" -> "other"

* Example is "_FooBar" or "_Foo$Bar" which both becomes "Foo" and "Bar".
* </li>
* <li>
* Anytime your switch from a lower case character to a upper case character as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"your" -> "you"

* </li>
* <li>
* Anytime your switch from a lower case character to a upper case character as
* identified by a Character.isUpperCase(codepoint)` and `Character.isLowerCase(codepoint)`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing backquote (after "by a")

* Examples are "my😁" and "MY😁" which are both treated as one word.
* </li>
* <li>
* Hungarian notation, i.e. Strings starting with lowercase "m" followed by uppercase letter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Strings" -> "strings"

* to the name used internally in the Realm file.
* <p>
* Examples where this can be useful is e.g:
* Examples where this are useful:
Copy link
Contributor

@Zhuinden Zhuinden Jan 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this are? That can't be right, the previous was better 😛

It's probably is.

* </ol>
* <p>
* Note, that changing the internal name does <i>NOT</i> effect importing data from JSON. The JSON
* Note that changing the internal name does <i>NOT</i> effect importing data from JSON. The JSON
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I wonder if this is affect or effect.

I never know that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* <p>
* If two modules disagree on the policy and one of them is {@code NO_POLICY}, the other one
* will be chosen without an error being thrown.
* If two modules disagree on the policy and one of them is {@code NO_POLICY}, the other will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe the other module's policy

But what happens if there are 3 @RealmModules and they all have conflicting policy? Which one will override NO_POLICY?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each class you find all modules it is part of, then you compare the policy between all these 3, so it will throw. The implementation is slightly different, but the effect is the same.

Copy link
Collaborator

@nhachicha nhachicha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice PR, some minor comments, also I didn't see tests with models defined using RealmModel interface

@@ -0,0 +1,124 @@
/*
* Copyright 2017 Realm Inc.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2018

* <ol>
* <li>
* Pre-processing. Done by calling {@link #preProcess(Set)}, which will do an initial parse
* of the modules and build up all information it can before before processing any model
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove before

* Validates that the class/field naming policy for this module is correct.
*
* @param globalModuleInfo list of all modules with `allClasses` set
* @param classSpecificModuleInfo
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing Javadoc

}

// Check for conflicts with specifically named classes. This can happen if another
// module are listing specific classes with another policy.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is

// with NO_POLICY, meaning it will not trigger any errors.
if (moduleAnnotation.allClasses()) {
// Check for conflicts with other modules with `allClasses` set. Only need to
// check the first since other conflicts would have been detected in an earlier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this depend on the order on which they were added?
ex: we added the following modules :

  • module 1allClass
  • module 2allClass
  • module 3 allClass
    now we add module4 which is incompatible with module 2, how can you detect this if you're checking only the first one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm thinking about this, you might be right. E.g. the following sequence of illegal modules would not be detected.

module1: NO_POLICY
module2: PASCAL_CASE
module3: NO_POLICY
module4: CAMEL_CASE

I'll compare against all previous modules.

import io.realm.annotations.RealmNamingPolicy;

/**
* Class with only a custom name
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where's the custom name definition? or do you mean the module that uses this class use a custom name?

public void dynamicQueryWithInternalNames() {
// Backlink queries not supported on dynamic queries
RealmResults<DynamicRealmObject> results = dynamicRealm.where(ClassWithPolicy.CLASS_NAME)
.equalTo(ClassWithPolicy.FIELD_CAMEL_CASE, "foo") // Java name in model class
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java name? shouldn't be internal name for Dynamic Realm. same for below

Copy link
Contributor

@Zhuinden Zhuinden Jan 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the class no longer exists, then you can't obtain any field-specific / class-specific policy configuration for it.

So I wonder if DynamicRealm should be able to see Java name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DynamicRealms has to work with the assumption that no java model classes exists. That this test case actually references ClassWithPolicy is just so it is easier to maintain the test case.

But you are correct, DynamicRealms do not understand the concept of name policies as that concept isn't relevant to them.

@@ -0,0 +1,16 @@
package io.realm.entities.realmname;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

licence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -0,0 +1,7 @@
package io.realm.entities.realmname;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

licence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

buf.append(", InternalFieldNames=[");
boolean commaNeeded = false;
for (Map.Entry<String, ColumnDetails> entry : indicesFromColumnNames.entrySet()) {
if (commaNeeded) { buf.append(","); }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commaNeeded = true; should be inside the if?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, in that case, it would never be set to the value true.

*
* @param table the start table.
* @param fieldDescription the field description.
* @param fieldDescription the field description using internal column columns.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

column columns?

@Override
public void emitStatement(int i, JavaWriter writer) throws IOException {
writer.emitStatement("return %s.getSimpleClassName()", qualifiedProxyClasses.get(i));
writer.emitStatement("return \"%s\"", internalClassNames.get(i));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does seem more Proguard-friendly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to mention correct 😉 The previous implementation did not work if you renamed the class name using @RealmClass(name = "othername").

@cmelchior
Copy link
Contributor Author

All comments addressed @nhachicha
I added a runtime check that throws if conflicts between internal names are detected among different modules (not possible to check at compile time). It isn't really possible to create a unit test for this, but I verified that we correctly throw an exception using our moduleExample.

for (Class<? extends RealmModel> realmClass : mediator.getModelClasses()) {
// Verify that the module doesn't contain conflicting definitions for the same
// underlying internal name. Can only happen if we add a module from a library and
// and a module from the app at the same time.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove and

@cmelchior cmelchior merged commit f089a87 into master Jan 29, 2018
@cmelchior cmelchior deleted the cm/realmname-annotation branch January 29, 2018 07:47
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants