We need to support the new Property API properly, make sure it's easy for modules implementing entity types to integrate with views, make sure translations and revisions work.
This will also decrease the work needed by other entity-supporting backends (Search API, EFQ) since they would get the properties for display with little extra work. (and they can choose what they want to do on the filter / sort side, since Search API for example only filters / sorts fields in the index)
Background: 7.x-3.x
The support for entities in 7.x-3.x is bolted-on and slightly awkward, thanks to the almost-nonexistent Entity API in Drupal core, and the long evolution of the entities since the D7 port of Views started / got its first release.
We have:
- Field API integration, providing fields for each entity type, automatically.
- Each base table of an entity type declares its entity type.
Example: $data['node']['table']['entity type'] = 'node';
This is used by:
- A query object method get_result_entities() that loads all entities from the given relationship, but doesn't support revisions.
The Field API handler doesn't use get_result_entities(), opting for its own code instead, that does support revisions.
- A field handler called views_handler_field_entity used as a base for field handlers that use the entity values, instead of the values fetched from the query.
This is really awkward since you get a new base handler, but don't get entity-generic per-property handlers (string, numeric, prerender_list etc) which decreases its usefulness.
- A way to define "query plugin agnostic" fields by adding the definition to $data["views_entity_{$entity_type}"]. This is used by the node links, for example.
Since they are added to $data['views_entity_node'] instead of $data['node'], they appear on views that display nodes fetched by the Search API or EFQ query plugins as well. These generic fields usually use a handler extending the views_handler_field_entity field handler.
- All field handlers have get_value() / sanitize_value() methods used to fetch / sanitize values used by the render methods. This allows an inheriting handler to change how the value is fetched (taking it from the loaded entity instead of taking it from the row fetched from the database, for example) and allowed efq_views to provide relatively slim handlers that replace the current sql-specific ones shipped with views.
- Entity API provides the Metadata API (property info) and a set of generic field handlers for each property type (so, for example entity_views_handler_field_numeric extends views_handler_field_numeric and overrides get_value() so that the property is fetched from the loaded entity).
Drupal 8 core changes:
#1498634: [meta] Define revision/translation SQL schema for core entities (no patch) resulted in these two changes in Drupal core:
http://drupal.org/node/1722906
http://drupal.org/node/1790138
So, we now have three tables for each entity type:
- {entity} (entity_id | uuid | revision_id | bundle_name | langcode)
- {entity_property_data} ( entity_id | revision_id | langcode | default_langcode | label and all other properties)
- {entity_property_revision} (entity_id | revision_id | langcode | default_langcode | label and all other properties)
So for nodes, that's {node}, {node_property_data} and {node_property_revision}.
We are also getting a Property API that's almost ready:
#1696640: Implement API to unify entity properties and fields
These changes mean that from now on we'll need to use the full entities to display the properties (the accessor methods on the entity select the correct language and possibly do language fallback).
This means that no field handler for any entity type needs anything from the query anymore in order to display the value, we use the entity for both property and field display. This also means that theoretically they could work across query plugins.
Of course, we still need to handle the schema aspect correctly because of filters and sorts.
This also affects Search API, because it basically means that fields in the index can be queried on, but they can't be displayed (which sucks a bit, because I see Search API as a great denormalization tool)
Work done in 8.x-3.x so far:
#1758634: The query plugin should load entities after the query has been executed
Summary:
- Removed get_result_entities() from the query object. Added a load_entities() method (used by the query execute(), not the handlers!) that also supports revisions (like the Field API code, unlike the old get_result_entities()). Entities are stored either in $row->_entity or $row->_relationship_entities[$relationship_id].
- Made the base fields of all entity tables always added to the query (so that we can actually load the entities), and because of that, removed "pure distinct".
- Removed Field API post_execute() and half of the query() code.
- Removed the "Entity" field handler since now all handlers have access to the entities matching the row.
- Added a convenience get_entity() method to the base field handler (FieldHandlerBase) so that each handler can easily fetch its entity.
The new entity loading code in load_entities() supports revisions and that's something that can be backported to the 7.x-3.x get_result_entities method.
Future expectations
- Properties of an entity type are automatically made available (just like Field API fields). This includes both displaying, and filtering / sorting (we have a key in property info telling us if the property exists in the schema).
- Since we're already making properties and fields available, we might as well provide automatic integration for entity types.
This means that if you declared an entity type and its properties to Drupal, there's no need to do explicit Views integration.
Implementation
Drupal 8 no longer contains an "entity" module, so the implementation needs to happen in the "system" module integration.
- We declare each entity type, and all properties (just like we do for fields, allowing the list to be altered in the process) in system_views_data().
The property info is mapped to views data in a helper function that can be used by other query plugin providing modules (like efq_views) to add the properties to their own base tables.
- We provide a hook_views_entity_property_info() (name not even remotely final) that allows you to specify your own query-plugin-agnostic properties, such as "edit link", or whatever gets left out of the core property list.
(Do we want to tell users to just alter the core property list instead?)
Click sorting
We might want to figure out an alternative implementation for click sorting, feels a bit odd to do it through the field handler, which suddenly ties it to the schema and SQL.
Aggregation
If we're using the full entity for properties as well, does aggregation (the SQL-specific way it works right now) even make sense for entities?
Problems:
- The entity determined for a row might be completely wrong
Example: node view, shows number of nodes per uid, so bojanz | 10, dawehner | 30, etc
In this case, there's no valid node entity to load. We take the MIN(nid) and load the first one, which still isn't right.
- We're assembling fake entities, which can cause the system to choke, and which won't work with translations
That's what the Field API handler does right now. So if fetched a COUNT(uid) instead of uid, that's going to be stored as the uid, which is wrong (confuses the system). Also, since we're assembling the entity ourselves, some languages might be missing, leading to translations failing (or fallback) failing.
The solution might be to always use the actual loaded entity instead of assembling the fake entity, but that would kill the usefulness of aggregation to a smaller or larger extent.
- It makes the code look odd.
So, imagine a views_handler_property_string extends views_handler_strings (using the old handler naming convention for clarity). That handler overrides get_value() to get the value from the entity, and overrides the query() method to do nothing.
Now, to support aggregation we need to check for it, and then fallback to the parent methods.
Revisions
In Drupal 8 we finally have the same properties available for the current and past revisions. So declaring the revision table, and the properties there (for the second time) is not actually needed, we could switch between the current and revision table based on an "Age" setting in the Views UI ("Show current revision" VS "Show all revisions").
This is non-critical and can be done in a followup, just wanted to note it down.
Comments
Comment #1
tim.plunkettComment #1.0
tim.plunkettTweak the text visually.
Comment #2
fagooh, that's a good write-up.
Here some random thoughts:
yeah, I've the feeling that display should generally work based upon loaded entity objects.
I think we should do a "entity display component" hook instead of this, i.e. such that you can implement this hook and your "display component" appears in both "panels" and views. Not sure if it gets that far though ;)
Yeah, I think we should clearly differentiate between real entities and plain db-records - even if they are entity-like.
Views integration is probably tight to the storage controller used, unless done based upon efq. Maybe it would be useful to have its own "views integration controller" which might be tight to a specific storage controller or query controller (given efq is implemented as query controller)?
So you could plugin another Views integration controller, e.g. having a simple, efq based one by default and be able to plugin in a more sophisticated one? Or vice versa? Not sure how this would work with views fields or relationship going away, but I'd assume that changing this is something for skilled devs only anyway.
Also, if you consider entities not being stored in SQL and Views being based on SQL directly having the Views controller would be useful to plugin in a Views controller suiting for your storage backend and you are done. In the d7 entity API module the Views controller approach was used to generate views integration while allowing devs to easily adapt it per entity type (before any other alter hook). I guess that woud use-case would apply to d8 as well.
Comment #3
bojanz commentedThere's an open Views issue for the controller idea: #1740492: Implement a default entity views data handler but I think it's a smaller implementation detail at this point.
Formatters still expect a real entity. And now properties do as well. So how do we differentiate?
Comment #4
fagoSure, you cannot pass a fake entity to something that expects an entity - that's good. I'm just saying that the fake entity should be $record or whatever, not $entity.
Comment #5
dawehnerGreat summary!
In ideal world views would know how long it will be able to use EFQ and when it has to switch to it's own mechanism, which is probably SQL specific. It would be though nice if we have a proper interface what's needed so other entity storage engines could write a viewsQuery class to support all what's needed and do something else instead.
Chx is currently working on a new version of efq (v2) with a proper interface etc. which will be really helpful to get the query effort in a clean way
I agree that it does not matter for know whether there is a views data controller or not, but just for sanity of readability of code it might even make sense in the first place.
Regarding click sort: I think the way it should be done is that the field handler creates an internal sort handler
and uses that. Maybe the field_field one has to additional logic on top of that.
Regarding aggregation:
This all is indeed a big problem.
The grouped by field (uid) here is indeed a problem.
In general i think support at least the most use-cases (like not 100% get language fallback right)
is fine, as long things are swappable. If we load wrong entities and change some of the properties and render
after that, wouldn't we somehow get it at least working most of the time?
I agree with your approach to fallback on parent functionality if needed.
We should try to produce classes in a way that the alternative plugins don't have to handle to much code.
The field_field handler is here a bad example as it handles too much at the same time.
The aggregated field(nid) here is a solved problem, as it just requires the numeric field handler.
Comment #6
plachInteresting, can you post the link please?
Comment #7
dawehner@plach
Afaik he did not created an issue yet, but http://sprunge.us/VdRe is some old version.
Comment #8
bojanz commentedHere it is #1801726: EntityFieldQuery v2.
Comment #9
xjmComment #10
yched commentedmore specific title, then :-)
Comment #11
DanZ commentedI'll just chime in here as someone who has been having difficulty with Views and Entities.
From what I read here, aggregation is tied to SQL and the schema. That's indeed a problem.
I have an entity with a simple computed property (total_price = cost * qty) that uses entity_views_handler_field_numeric. In a non-aggregated View, this is fine, and the field is displayed properly for each row. In an aggregated view, though, there's a GROUP BY on the entity ID, which means there's a separate row for every entity, so SUM() aggregation on the total_price doesn't do anything. As far as I can tell, there isn't any way to get this to work.
So, a way to aggregate non-DB fields would be very useful.
Comment #12
dawehnerlink to another issue: #1740492: Implement a default entity views data handler
Comment #13
rcross commentedanyone still working on this?
Comment #13.0
rcross commentedFlesh out the text more
Comment #14
andypostLet's close this one as fixed because there's only left #1740492: Implement a default entity views data handler
that postponed on #2183231: Make ContentEntityDatabaseStorage generate static database schemas for content entities