-
Notifications
You must be signed in to change notification settings - Fork 276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Active label API Proposals #1458
base: active_label
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,135 +1,320 @@ | ||
ActiveLabel | ||
=========== | ||
|
||
``ActiveNode`` provides an ability to define inheritance of models which also gives subclasess the labels of their parent models. In Ruby, however, inheritence of classes is not sufficient. Sometimes is makes more sense to be able to build a module which defines behavior (or "concerns") which could be applied to any model. This is what ``ActiveLabel`` provides. | ||
|
||
``ActiveLabel`` modules can be defined in two ways: | ||
|
||
* Default: Where the module's behavior is always defined on the ``ActiveNode`` model and the model's nodes always have a corresponding label in Neo4j | ||
* Optional: Where the module's behavior is defined on the class only when the model's nodes have a corresponding label in Neo4j | ||
As you build out your application's models, you likely will want to share code between them. | ||
Neo4jrb's ``ActiveNode`` module supports class inheritance, allowing you to create "submodels" which | ||
inherit the methods and labels of their ``ActiveNode`` parents while also adding their own submodel specific | ||
label & methods. This code sharing strategy should be familiar to anyone coming from the ActiveRecord world. | ||
|
||
Sometime's however, inheritance is not always appropriate. Sometimes what you want to do is conditionally add | ||
a module of functionality to an ActiveNode model, but only if a specific label is present on the node. | ||
For an example of when this is needed, look at the Neo4j's example movie database (https://neo4j.com/developer/movie-database/). | ||
In this example, a Person node is sometimes an Actor, sometimes a Director, sometimes a User, and sometimes a | ||
combination of Actor, Director, and/or User. | ||
|
||
Multiple inheritance such as this is not possible with ``ActiveNode`` (or with ActiveRecord). This | ||
is where ``ActiveLabel`` comes to the rescue! ``ActiveLabel`` allows you to create a Ruby module which is only | ||
applied to an ``ActiveNode`` model when a specific label is attached to an instance of the model. Using our | ||
example movie database from above, you could create an ``ActiveLabel`` module which only adds Actor methods and | ||
properties to an instance of Person if a Person node also has an Actor label. Or only adds InShowbusiness methods | ||
and properties to an instance of Person if a Person node has `either` Actor or Director labels. | ||
|
||
``ActiveLabel`` can fully replace ``ActiveNode`` inheritence, but it involves a different way of thinking | ||
then what many ActiveRecord developers might be used to. If you're just starting out with Neo4jrb, you might find | ||
it easiest to stick with the "ActiveRecord" like workflow provided by ``ActiveNode`` and inheritence. | ||
But as you get more comfortable with Neo4j's flexibility and | ||
polymorphism, you'll might find that ``ActiveLabel`` is the better option for many tasks. | ||
|
||
.. code-block:: ruby | ||
|
||
class Person | ||
include Neo4j::ActiveNode | ||
include Actor | ||
include Director | ||
include User | ||
|
||
property :name, type: String | ||
|
||
label :HasAddress | ||
label :Destroyable | ||
end | ||
|
||
class Organization | ||
class Movie | ||
include Neo4j::ActiveNode | ||
|
||
property :title, type: String | ||
property :name, type: String | ||
end | ||
|
||
label :HasAddress | ||
label :Destroyable | ||
.. code-block:: ruby | ||
|
||
module Actor | ||
include Neo4j::ActiveLabel | ||
include InShowbusiness | ||
|
||
has_many :out, :acts_in, type: :ACTS_IN, model_class: :Movie | ||
end | ||
|
||
module Director | ||
include Neo4j::ActiveLabel | ||
include InShowbusiness | ||
|
||
.. code-block:: ruby | ||
has_many :out, :directed, type: :DIRECTED, model_class: :Movie | ||
end | ||
|
||
module InShowbusiness | ||
include Neo4j::ActiveLabel | ||
|
||
class Address | ||
property :line1, type: String | ||
property :line2, type: String | ||
property :country, type: String | ||
property :postal_code, type: String | ||
self.associated_labels = [:Actor, :Director] | ||
self.associated_labels_matcher = :any | ||
|
||
property :biography | ||
property :lastModified | ||
property :version | ||
end | ||
|
||
module HasAddress | ||
module User | ||
include Neo4j::ActiveLabel | ||
|
||
included do | ||
has_one :out, :address, type: :HAS_ADDRESS | ||
end | ||
property :login | ||
property :password | ||
property :roles | ||
|
||
module InstanceMethods | ||
def distance_from(has_address_object) | ||
address.distance_from(has_address_object.address) | ||
def administrate_stuff | ||
puts "administrate stuff" | ||
end | ||
end | ||
end | ||
|
||
|
||
Creating | ||
-------- | ||
|
||
``ActiveLabel`` modules are defined by creating a standard ruby module with ``include Neo4j::ActiveLabel``. | ||
By convention, the ``ActiveLabel`` module will be associated with a label equal to the module name. For example, | ||
in the example above, the ``Actor`` ``ActiveLabel`` module is associated with the ``:Actor`` label. You can | ||
customize the label(s) which an ``ActiveLabel`` module is associated with using ``self.associated_labels =``. You must also | ||
include an ``ActiveLabel`` module in an ``ActiveNode`` class if you want the class to respond to the ``ActiveLabel``. | ||
|
||
``ActiveLabel`` modules have several parts: | ||
|
||
.. code-block:: ruby | ||
|
||
module Destroyable | ||
include Neo4j::ActiveLabel | ||
module Actor | ||
include Neo4j::ActiveLabel # adds ActiveLabel functionality to the Actor module | ||
|
||
follows_label :Destroyed | ||
# ``ActiveLabel`` modules can have associations and properties just like ``ActiveNode`` classes | ||
property :popularity | ||
has_one :out, :friend, type: :FRIEND, model_class: :Person | ||
|
||
included do | ||
property :destroyed_at, type: DateTime | ||
# When a node is retrieved from the database, it is mapped to an ``ActiveNode`` class and a new | ||
# instance of that class is created. We'll call this created object obj A. | ||
# If obj A's class includes this ``ActiveLabel``, and, additionally, obj A has the label associated | ||
# with this ``ActiveLabel``, then this included block will be evaluated | ||
# within the context of obj A. | ||
end | ||
|
||
module InstanceMethods | ||
def destroy | ||
destroyed_at = Time.now | ||
# After obj A has been found and initialized, before the included block is evaluated, obj A will | ||
# be extended with these InstanceMethods (e.g. obj.extend(InstanceMethods)) | ||
|
||
super | ||
def act | ||
puts "I acted!" | ||
end | ||
end | ||
|
||
module ClassMethods | ||
def destroyed_recently | ||
all.where("#{identity}.destroyed_at > ?", 1.week.ago) | ||
# Similar to ``ActiveSupport::Concern``, when this ``ActiveLabel`` module is included in an | ||
# ``ActiveNode`` class, the class will be extended with these singleton methods (e.g. Person.extend(ClassMethods)) | ||
|
||
def actor_popularity_scale | ||
puts "5 stars = excellent. 1 star = poor." | ||
end | ||
end | ||
end | ||
|
||
``ActiveLabel`` modules only describe functionality that is tied to a label. Actually adding that label to instances of a class | ||
is a seperate step. If you'd like to add a label to specific instances of a class, you can use standard ``neo4j-core`` methods | ||
``add_label()`` or ``remove_label``. You can also use special helper methods that ``ActiveLabel`` adds to a class when it is | ||
included in a class | ||
|
||
Creating | ||
-------- | ||
.. code-block:: ruby | ||
|
||
# Initializes a Person with additional Actor label | ||
Person.actor.new | ||
|
||
# Creates a Person with additional Actor label | ||
Person.actor.create | ||
|
||
# Creates a Person with additional Actor AND Director labels | ||
Person.actor.director.create | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this api to be more expressive (and concise) than
The above is also two DB queries, where ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This being said, I'm wondering what happens given the following
What labels does
Below, I added a comment suggesting that I'm thinking that, if
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thefliik again you are bringing up something I had on my mind for a very long time. My idea was to do all this natively with ruby without any changes to the DSL or the api methods. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @klobuczek Interesting. I think it's important for me to say that this proposal only adds to the existing API, and, even if this were implemented, anyone could continue to use the neo4j gem exactly as they have been without changes. That being said, your suggestion sounds pretty similar to mine, except that you are getting rid of individual ActiveNode classes and simply using one, anonymous class. Are you suggesting this because you think it would be more flexible? One downside of your approach, is that (and correct me if I'm wrong), but in your approach a specific label (e.g. I guess what I'm trying to say, is that both strategies are basically the same: neo4jrb gets a node from the database and then wraps that node in a class (either an ActiveNode class or anonymous ActiveNode class) based on the labels the node has. Then, based on which of those labels are determined to be ActiveLabels, the object gets extended with one or more modules. Why remove standard There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @thefliik Please note that ActiveNode is not a class but a module, so this is already different from active record, but does offer a huge advantage. What I am suggesting is to not be required to define classes including ActiveNode with properties and associations (which would be standard way like today), but allow defining just modules (including ActiveNode or ActiveLabel or similar,) which you can combine to classes, but which also could live alone if an unknown combination of labels is retrieved from the database. This is the only case where I would return anonymous class. This would work nicely for creation and retrieval. I have not thought through the promotion (adding a label to existing node) and demotion (removing a label), which does not seem to have a native counterpart in ruby. |
||
|
||
If you'd like to `always` add one or more additional labels to instances of a class, you can use the ``ActiveNode`` ``label`` method | ||
|
||
If an ``ActiveLabel`` does not declare ``follows_label``, creating a node will attach the corresponding label. Otherwise you must trigger the attachment of the label: | ||
.. code-block:: ruby | ||
|
||
class Person | ||
include Neo4j::ActiveNode | ||
include Actor | ||
include Director | ||
include User | ||
|
||
# ``label :Actor, optional: true`` automatically adds the label ``:Actor`` | ||
# to every instance of the Person class. The :Actor label is technically | ||
# optional, even though it is always added, because a node will still be mapped | ||
# to the Person class even if you manually remove the :Actor label from it. | ||
label :Actor, optional: true | ||
|
||
# If you call the ``label`` method without the ``optional: true`` argument, | ||
# then nodes will only be mapped to the Person class if the label is | ||
# also present on the node. (i.e. removing the :User label from a node will | ||
# mean that that node is no longer considered a Person) | ||
label :User | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note, if the However there WOULD be a difference between This also makes me see the implementation difficulties of a
|
||
end | ||
|
||
To dry up your code, you can include ``ActiveLabel B`` inside ``ActiveLabel A``. This ensures that when you include | ||
``ActiveLabel A`` in a module you also always include ``Activelabel B`` | ||
|
||
.. code-block:: ruby | ||
|
||
# Node gets both `Person` and `HasAddress` labels | ||
person = Person.create | ||
module Hollywood | ||
include Neo4j::ActiveLabel | ||
|
||
self.associated_labels = [:Actor, :Director] | ||
self.associated_labels_matcher = :any | ||
|
||
property :name | ||
end | ||
|
||
module Actor | ||
include Neo4j::ActiveLabel | ||
include Hollywood | ||
end | ||
|
||
module Director | ||
include Neo4j::ActiveLabel | ||
include Hollywood | ||
end | ||
|
||
# `Destroyed' label is added. `mark_destroyed` method is automatically defined via `follows_label` definition | ||
person.label_as_destroyed | ||
Helper methods | ||
~~~~~~~~~~~~~~ | ||
|
||
# `Destroyed' label is removed | ||
person.label_as_not_destroyed | ||
Including an ``ActiveLabel`` module in a class will `automatically` add a few helper methods to the class and class instances. | ||
For example, using the ``Actor`` ``ActiveLabel`` module: | ||
|
||
1. You can call ``person.actor?`` which will return true if the obj has the label associated with the ``Actor`` ``ActiveLabel``. | ||
2. You can call ``Person.actor.new`` or ``Person.actor.create`` to initialize / create a new ``Person`` instance with the additional ``Actor`` label. | ||
3. You can call ``Person.actor.all`` or ``Person.actor.first`` to return all ``Person`` nodes with the ``Actor`` label. In fact, calling ``Person.actor`` simply adds a label scope, which can be combined with any custom scopes you have (e.g. ``Person.most_popular`` -> ``Person.actor.most_popular``) | ||
|
||
Querying | ||
-------- | ||
|
||
``ActiveLabel`` allows your Ruby module to act like a model class. However, since you can add a label to any module, you can query for nodes across modules: | ||
Querying for ``ActiveLabel``s is easy, and can allow you to query across classes. | ||
|
||
.. code-block:: ruby | ||
|
||
Destroyable.all | ||
|
||
HasAddress.as(:obj).address.where(postal_code: '12345').pluck('DISTINCT obj') | ||
# This returns all nodes which have the Actor label | ||
Actor.all | ||
|
||
By default this returns all nodes for all models where the ``ActiveLabel`` module is defined. If ``follows_label`` is declared, this returns just those nodes which have the label. | ||
# This returns all nodes with the Director label which have a directed association to | ||
# a node with the title "Star Wars" | ||
# This works because the ``Director`` ``ActiveLabel`` defines a ``directed`` association | ||
Director.as(:dir).directed.where(title: 'Star Wars').pluck('DISTINCT dir') | ||
|
||
By defining the ``follows_label``, some methods are automatically provided to allow you to filter and interrogate: | ||
Including an ``ActiveLabel`` module in a class will `automatically` add a few helper methods to the class and class instances. | ||
|
||
.. code-block:: ruby | ||
|
||
Person.labeled_as_destroyed | ||
Person.actor.all | ||
|
||
Person.actor.first | ||
|
||
Person.first.labeled_as_destroyed? | ||
Calling ``Person.actor`` simply adds a label scope, which can be combined with any custom scopes you have (e.g. | ||
``Person.most_popular`` -> ``Person.actor.most_popular`` | ||
|
||
Associations | ||
~~~~~~~~~~~~ | ||
|
||
You can even create associations to traverse to labels: | ||
You can create associations with ActiveLabels: | ||
|
||
.. code-block:: ruby | ||
|
||
class Organization | ||
class Movie | ||
include Neo4j::ActiveNode | ||
|
||
has_many :out, :addressables, type: :HAS_ADDRESSABLE_OBJECT, label_class: :HasAddress | ||
has_many :in, :actors, type: :ACTS_IN, label_module: :Actor | ||
|
||
# `model_class` acts as a filter to the `label_class` argument. Both `model_class` and `label_class` can be arrays | ||
has_many :out, :addressable_people, type: :HAS_ADDRESSABLE_OBJECT, label_class: :HasAddress, model_class: :Person | ||
# `label_module` acts as a filter to the `model_class` argument. | ||
# Both `model_class` and `label_module` can be arrays | ||
has_many :in, :human_actors, type: :ACTS_IN, label_module: :Actor, model_class: :Person | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where you had "model_class" acts as a filter... I changed it to "label_module" acts as a filter because the class is what is instantiated and so seems like the more "primary" descriptor (in my mind). Put another way, |
||
end | ||
|
||
If you want more control over your association, you can use the ``node_labels:`` option instead | ||
|
||
.. code-block:: ruby | ||
|
||
class Movie | ||
include Neo4j::ActiveNode | ||
|
||
# The node_labels option accepts a two dimentional array. Each array in the node_labels | ||
# array includes a set of labels that the association will match against. In the example | ||
# below, the ``actors`` association only includes nodes which have either ``:Actor:Person`` | ||
# OR ``:Actor:Animal`` labels and have an ``<-[:ACTS_IN]-`` relation to a ``Movie`` node | ||
has_many :in, :actors, type: :ACTS_IN, node_labels: [[:Actor, :Person], [:Actor, :Animal]] | ||
|
||
# Other valid params for the node_labels option are | ||
has_many :in, :actors, type: :ACTS_IN, node_labels: [[:Actor, :Person], :Actor] | ||
|
||
# or | ||
has_many :in, :actors, type: :ACTS_IN, node_labels: :Actor | ||
end | ||
|
||
Note, while the ``label_module`` option requires its params to resolve to ``ActiveLabel`` modules, the ``node_labels`` | ||
option doesn't. The ``node_labels`` option simply matches against the specified labels. | ||
|
||
Multiple Conditions | ||
------------------- | ||
|
||
Sometimes you may wish for ``ActiveLabel`` code to be associated with an array of labels, rather than a single label. | ||
Perhaps the code triggers if `any` label in the array is present, or perhaps it only triggers if `all` labels in the | ||
array are present. | ||
|
||
.. code-block:: ruby | ||
|
||
module Hollywood | ||
include Neo4j::ActiveLabel | ||
|
||
self.associated_labels = [:Actor, :Director] | ||
self.associated_labels_matcher = :any | ||
|
||
# OR | ||
|
||
self.associated_labels = [:Actor, :Director] | ||
self.associated_labels_matcher = :all | ||
|
||
end | ||
|
||
By default, ``self.associated_labels_matcher == :any`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Including one ActiveLabel module in other ActiveLabel modules would be the primary way of sharing code between ActiveLabel modules rather than using a concern. To make this work, a dev would specify that the ActiveLabel module containing the shared code had multiple associated labels and would trigger if any of those labels were present. I realized that sharing code using a concern wouldn't work, because there isn't a good (any?) way of specifying that the concern's code should only apply to certain instances of a class (you can't do Also, if you had an |
||
|
||
included_if block | ||
~~~~~~~~~~~~~~~~~ | ||
|
||
Sometimes conditional functionality is limited to one class, and is simple enough that a full ``ActiveLabel`` module seems like | ||
overkill. You can make use of ``ActiveNode``'s ``included_if_any`` and ``included_if_all`` methods to specify blocks of code that only | ||
run if `any` or `all` of the specified labels are present on a node (note, these methods resolve their params to labels, | ||
rather than ``ActiveLabel`` modules. This means that you can match against an optional label which does not have an associated ``ActiveLabel`` | ||
module). | ||
|
||
.. code-block:: ruby | ||
|
||
class Person | ||
include Neo4j::ActiveLabel | ||
|
||
# only run if a Person node also has the Actor or Director labels | ||
included_if_any :Actor, :Director do | ||
property :medium_ego | ||
end | ||
|
||
# only run if a Person node also has the Actor AND Director labels | ||
included_if_all :Actor, :Director do | ||
property :large_ego | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking that maybe this should be refactored as just
This format would also be more powerful and allow
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For that matter, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another issue with any potential
In the above, devs could
But I could see a dev also wanting to do something like Maybe this is fine, and would simply be a known limitation of I definitely view anything like Another note: If someone was using an |
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may just come down to personal preference. I originally wrote
But I found the
follows
metaphor to be less intuitive for me than theassociated
metaphor, which resulted in the change toself.associated_labels =
.I don't feel strongly about this though, so if others find
follows
more intuitive I'm all for changing it back.