Skip to content

JAVA-541 + JAVA-636 + JAVA-984: Polymorphism support to object mapper#686

Merged
olim7t merged 17 commits into
3.0from
java541
Jul 22, 2016
Merged

JAVA-541 + JAVA-636 + JAVA-984: Polymorphism support to object mapper#686
olim7t merged 17 commits into
3.0from
java541

Conversation

@adutra
Copy link
Copy Markdown
Contributor

@adutra adutra commented May 30, 2016

This addresses both JAVA-541, JAVA-636, and JAVA-984.

The second commit and following introduce a refactoring of the mapper internals:

  1. ColumnMapper renamed to PropertyMapper;
  2. No more factory patterns to create mappers;
  3. ReflectionX classes removed and merged into their parent classes;
  4. QueryType class cleanup.
  5. Replaced IllegalArgumentException with IllegalStateException where more appropriate.

If you want to read the docs first, jump to the new "polymorphism support" section.

@adutra adutra added this to the 3.1.0 milestone May 30, 2016
@adutra adutra force-pushed the java541 branch 8 times, most recently from e5678bc to 4672ffd Compare June 1, 2016 16:36
@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Jun 20, 2016

Rebased on top of current 3.0.

@adutra adutra changed the title JAVA-541 + JAVA-636: Polymorphism support to object mapper JAVA-541 + JAVA-636 + JAVA-984: Polymorphism support to object mapper Jun 21, 2016
}
```

One powerful advantage of annotating getter methods is that
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Awesome example, like how it shows polymorphism for both tables and UDTs.

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Jul 8, 2016

Guidelines for reviewers:

  1. The QueryType class is now an enum. There were some other instances that were planned for slice queries, but these were never implemented, so it's actually dead code.
  2. There were factories to obtain instances of EntityMapper and ColumnMapper. The factories though would always return the same instances. My guess, reading the commit history, is that the instances that we currently have use reflection to read and write values, so Sylvain or Michael probably left the door open for other solutions (bytecode generation, etc.). But as far as I know nobody ever asked for this. So the current state of code is very over-engineered. I took the opportunity to remove the factory classes.
  3. The ColumnMapper class has been renamed to PropertyMapper. That wasn't required, but I guess the name ColumnMapper was chosen before UDTs arrived, and now this class applies to fields as well. So I thought that it would better stress the Java side of the mapping, because both columns and fields map to java properties, hence PropertyMapper.

If anyone disagrees with these decisions, please chime in.

@olim7t
Copy link
Copy Markdown
Contributor

olim7t commented Jul 8, 2016

Sorry but I feel that the JAVA-541 commit is missing the point: it merely adds the ability to inherit field definitions from a parent class. That's useful in itself, but what the ticket asks for is a way to encode inheritance relationships in the model. Borrowing from the Caffinitas terminology, and considering an inheritance hierarchy Shape > Circle, Shape > Rectangle > Square, that would mean:

  • table-per-class inheritance: there are 4 tables shapes, circles, rectangles and squares sharing the same primary key. A Square instance is persisted as a row in each of shapes, rectangles and squares.
  • single-table inheritance: there is a single table shapes with a discriminator column, and a column for any possible field in the hierarchy. A Square instance is persisted as a single row in this table, any non-applicable column (e.g. radius) is null.

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Jul 8, 2016

Sorry but I feel that the JAVA-541 commit is missing the point. it merely adds the ability to inherit field definitions from a parent class.

This is exactly what one of the commenters in the ticket asked for: fields in superclasses. And I think this guy has it right: keep it simple, but extend to class hierarchies.

So yes, I went for only that strategy, and as stated in the docs: "Each concrete class must correspond to a table or a UDT, and should be annotated accordingly with @table or @UDT. This is analogous to the so-called "table per concrete class" mapping strategy."

Frankly, proposing other strategies is the red line I did not want to cross. Especially the single-table strategy; i wonder if this model is efficient at all for cassandra, given the high number of nulls. There is too much of Hibernate in it IMHO.

And while at it, why not propose strategies to map the same entity to many tables, each one "denormalized" differently (JAVA-549 and JAVA-524)?

I think we need to draw a clear line here and define the scope of "polymorphism support" and how far we want to go.

@al3xandru Could you please give us your insight here?

@olim7t
Copy link
Copy Markdown
Contributor

olim7t commented Jul 8, 2016

I agree with your arguments, and polymorphic mapping would certainly cross a line in term of implementation complexity. But at least we should not introduce property inheritance as a fix for JAVA-541, as I feel users watching that ticket are expecting much more.

@olim7t
Copy link
Copy Markdown
Contributor

olim7t commented Jul 8, 2016

I find 169e7e0 extremely hard to review. There are at least 3 different changes in there:

  • remove factories
  • rename MappedProperty to PropertyMapper
  • allow annotations on getters and setters (JAVA-636)

What's heartbreaking is that the first two would be trivial to review separately, and they probably represent a large portion of the diff. With everything mixed together, you have to pay attention to each line to determine which change (or combination of changes) impacted it.

In the future it would really simply reviews if 1 commit = 1 change.

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Jul 11, 2016

I find 169e7e0 extremely hard to review.

Yes, my bad, please accept my apologies. I found myself in that situation and couldn't separate the changeset in two or three distinct commits, and so had to commit everything in one.


private static final Comparator<Field> fieldComparator = new Comparator<Field>() {
@VisibleForTesting
static final Set<Class<? extends Annotation>> VALID_PROPERTY_ANNOTATIONS = ImmutableSet.of(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wasn't clear to me initially that VALID_FIELD_ANNOTATIONS apply to UDTs while PROPERTY apply to tables, would be good to clarify that here (although it may be somewhere else but haven't gotten to it yet).

Alexandre Dutra and others added 16 commits July 22, 2016 09:45
This commit also includes a refactoring of the mapper internals:
- ColumnMapper renamed to PropertyMapper;
- No more factory patterns to create mappers;
- ReflectionX classes removed and merged into their parent classes;
- QueryType class cleanup.
This commit changes all places where an IllegalStateException was
thrown to throw an IllegalArgumentException instead.
The test "should_save_and_retrieve_sphere" was failing because
it relied on a setter declared in Sphere that overloads a setter
declared in Circle. JDK 7 fails to detect the overloading setter.

This fix simply modifies the test to avoid this edge case.
Also reorganized renamed some tests in MapperInvalidAnnotationTests for
consistency.
@olim7t olim7t merged commit 4a0bc16 into 3.0 Jul 22, 2016
@olim7t olim7t deleted the java541 branch July 22, 2016 16:52
@olim7t olim7t restored the java541 branch July 22, 2016 16:53
olim7t added a commit that referenced this pull request Jul 22, 2016
…t mapper (#686)"

This reverts commit 4a0bc16.

The branch was inadvertently merged with the "squash and merge"
strategy, the next commit re-merges it with a merge commit to preserve
history.
olim7t added a commit that referenced this pull request Jul 22, 2016
@olim7t olim7t deleted the java541 branch July 22, 2016 16:57
@kford
Copy link
Copy Markdown

kford commented Aug 18, 2016

I'm using the 3.1.0 driver here (https://mvnrepository.com/artifact/com.datastax.cassandra/cassandra-driver-mapping/3.1.0) and not sure how to take advantage of the polymorphic support from this PR. Is there an updated example that can demonstrate the capabilities?

Thanks

@newkek
Copy link
Copy Markdown
Contributor

newkek commented Aug 18, 2016

@kford
Copy link
Copy Markdown

kford commented Aug 18, 2016

@newkek and @adutra, It says superclasses are scanned, but it seems not subclasses (by omission in the docs).

*edit: should I move this elsewhere since this PR is already merged?

I have a use case like the following

class Properties{
// no members
}

class UserTaskProperties extends Properties {
// some members
}
/* other types of Properties, like UserApprovalProperties etc., all of which extend Properties */

I created an example application that you can easily run with maven and attached the zip.. It just needs host/user/pass edited in pom.xml.. It demonstrates that saving these mapped subtypes is lossy. Am I doing something wrong? If I'm not, the only alternative I can think of is introducing custom codecs for the sub-types and manage the mapping manually there.. That would create significant maintenance overhead.

Thanks in advance

cassandra-mapping.zip

update: forgot to attach cql/ddl
cassandra-poc-cql-ddl.txt

@tolbertam
Copy link
Copy Markdown
Contributor

Hi @kford,

This kind of polymorphism is not currently supported by the Mapper even with this change. From the jira ticket (JAVA-541) we added a note that this doesn't cover all cases:

We're solving this in 3.1.0 with the ability to inherit properties from parent classes. This allows you to use a "table-per-class" polymorphic mapping, where the table of each concrete class contains all the columns (its own + the ones inherited from the parents). Note that polymorphic queries are not supported (e.g. if Dog extends Animal and both are concrete, queries on the animals table will not return rows from the dogs table).

We're aware that polymorphic mapping can have many different meanings; table-per-class is a good compromise in terms of complexity and compatibility with the existing API. If this is not what you were waiting for, please open a separate ticket (and link back to this one) so that we can track the other alternatives.

Unfortunately this case is not covered, but Cassandra doesn't support this either as far as I know. Using the provided schema:

CREATE TYPE cass_poc.vehicle (id uuid);
create type cass_poc.vehicle_type_truck (towing_capacity int);
create type cass_poc.vehicle_type_car (miles_per_gallon int, passenger_seats int);
CREATE TABLE cass_poc.employees (employee_id uuid PRIMARY KEY, employee_type text, name text, vehicles list<frozen<vehicle>>);

I see in your code that you want to be able to have Truck and Car types in a List<Vehicle> which maps to the vehicles column (list<frozen<vehicle>>) on the employees table. While that is perfectly valid java, C* won't allow it since the vehicles column requires vehicle UDTs, and there is no inheritance for UDTs in Cassandra currently.

As far as solving this particular issue, a couple ways it could be handled.

  1. Make the vehicle UDT a union of car and truck, i.e.: create type cass_poc.vehicle (id uuid, vehicle_type text, towing_capacity int, miles_per_gallon int, passenger_seats int);. The vehicle_type column can hold what type of vehicle it is. This doesn't really map well to the java side, but you could check the vehicle_type to know what fields are relevant...that's not very clean.
  2. As I think you were alluding to, Store the vehicle in C* as some other type (blob, map, etc.), and then register a Codec that maps Vehicle <-> ByteBuffer (or some other type). This should allow you to represent your java model the way you like as long as the codec is registered to convert between Vehicle and the type cassandra needs. I agree it is a bit of work, but it would work and I think is the best option. Internally in C* UDTs are effectively serialized blobs anyways (1 frozen cell that you cannot manipulate a field at a time), but C* nicely represents them in a unified format that the drivers understand, where with your own custom blobs that would not be the case.

@kford
Copy link
Copy Markdown

kford commented Aug 19, 2016

@tolbertam Thank you for the thorough response/explanation. I guess this also means that if I wanted to query by one of the properties that is part of my logical Vehicle class, I'd have to extract that out so it's either not part of the blob, or redundantly stored as (text for example), outside the blob...

like... type vehicle (vehicleType text, otherQueryableField text, vehicleBlob blob) where the vehicleBlob may or may not redundantly contain vehicleType and otherQueryableField, depending on how I want to write my custom codec.

@tolbertam
Copy link
Copy Markdown
Contributor

@tolbertam Thank you for the thorough response/explanation. I guess this also means that if I wanted to query by one of the properties that is part of my logical Vehicle class, I'd have to extract that out so it's either not part of the blob, or redundantly stored as (text for example), outside the blob...

That is correct, you can't query by individual UDT fields (yet: see CASSANDRA-6832). You would have to have an extra column that is part of the primary key or separately indexed to do that.

@newkek
Copy link
Copy Markdown
Contributor

newkek commented Aug 19, 2016

*edit: should I move this elsewhere since this PR is already merged?

Just a quick note since you asked @kford , our mailing is the most appropriate place to ask those kinds of questions: https://groups.google.com/a/lists.datastax.com/forum/#!forum/java-driver-user
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants