-
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
ActiveLabel
documentation proposal
#1457
base: 9.1.x
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 |
---|---|---|
@@ -0,0 +1,135 @@ | ||
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 | ||
|
||
.. code-block:: ruby | ||
|
||
class Person | ||
include Neo4j::ActiveNode | ||
|
||
property :name, type: String | ||
|
||
label :HasAddress | ||
label :Destroyable | ||
end | ||
|
||
class Organization | ||
include Neo4j::ActiveNode | ||
|
||
property :title, type: String | ||
|
||
label :HasAddress | ||
label :Destroyable | ||
end | ||
|
||
|
||
.. code-block:: ruby | ||
|
||
class Address | ||
property :line1, type: String | ||
property :line2, type: String | ||
property :country, type: String | ||
property :postal_code, type: String | ||
end | ||
|
||
module HasAddress | ||
include Neo4j::ActiveLabel | ||
|
||
included do | ||
has_one :out, :address, type: :HAS_ADDRESS | ||
end | ||
|
||
module InstanceMethods | ||
def distance_from(has_address_object) | ||
address.distance_from(has_address_object.address) | ||
end | ||
end | ||
end | ||
|
||
|
||
.. code-block:: ruby | ||
|
||
module Destroyable | ||
include Neo4j::ActiveLabel | ||
|
||
follows_label :Destroyed | ||
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. @leehericks Responding to your comment about following the module name I'm definitely a bit on the fence about having a The question of what As I'm thinking about this more and more, this might actually be best implemented as two features. That could actually simplify defining So maybe then it does make sense to think in terms similar to what @thefliik proposed to have something like: optional_label :Destroyed do
property :destroyed_at, type: DateTime
def destroy
destroyed_at = Time.now
super
end
end That could be defined on the model or inside of a 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. Oops, I should have read this first.
Looks like we've arrived at similar conclusions 👍 |
||
|
||
included do | ||
property :destroyed_at, type: DateTime | ||
end | ||
|
||
module InstanceMethods | ||
def destroy | ||
destroyed_at = Time.now | ||
|
||
super | ||
end | ||
end | ||
|
||
module ClassMethods | ||
def destroyed_recently | ||
all.where("#{identity}.destroyed_at > ?", 1.week.ago) | ||
end | ||
end | ||
end | ||
|
||
|
||
Creating | ||
-------- | ||
|
||
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 | ||
|
||
# Node gets both `Person` and `HasAddress` labels | ||
person = Person.create | ||
|
||
# `Destroyed' label is added. `mark_destroyed` method is automatically defined via `follows_label` definition | ||
person.label_as_destroyed | ||
|
||
# `Destroyed' label is removed | ||
person.label_as_not_destroyed | ||
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. @leehericks Responding to your comment: I can see what you're saying about the magic prefixes of I definitely see the appeal of the consistent naming (though again, the Ruby convention that I've seen is to generally go with different phrasings of English even if they aren't exactly the same). Are you saying that we might have 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. @cheerfulstoic The boolean value needs to go somewhere. Adding and removing labels should fit within the persistence workflow just like properties and be able to check them or enforce some logic in callbacks. If something fails, rolling back the save or update should also make sure the labels are not set. If labels are applied as individual actions, how do you rollback or save errors to the model? This is the same problem I have with associations autosaving right now. |
||
|
||
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: | ||
|
||
.. code-block:: ruby | ||
|
||
Destroyable.all | ||
|
||
HasAddress.as(:obj).address.where(postal_code: '12345').pluck('DISTINCT obj') | ||
|
||
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. | ||
|
||
By defining the ``follows_label``, some methods are automatically provided to allow you to filter and interrogate: | ||
|
||
.. code-block:: ruby | ||
|
||
Person.labeled_as_destroyed | ||
|
||
Person.first.labeled_as_destroyed? | ||
|
||
Associations | ||
~~~~~~~~~~~~ | ||
|
||
You can even create associations to traverse to labels: | ||
|
||
.. code-block:: ruby | ||
|
||
class Organization | ||
include Neo4j::ActiveNode | ||
|
||
has_many :out, :addressables, type: :HAS_ADDRESSABLE_OBJECT, label_class: :HasAddress | ||
|
||
# `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 | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ Contents: | |
RakeTasks | ||
|
||
ActiveNode | ||
ActiveLabel | ||
ActiveRel | ||
|
||
Properties | ||
|
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.
I'd replace with:
ActiveNode
supports class inheritance, creating "submodels" which inherit the methods and labels of theirActiveNode
parents while adding their own label & any methods / properties you define. However, inheritance is not always appropriate. Sometimes what you want to do is conditionally add a module of functionality to anActiveNode
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, and sometimes and Actor and Director.ActiveLabel
allows you to accomplish this.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.
I still don't understand the conditional part.
Suppose you have a person model who may be an Actor or a Director or both.
You still have to be careful not to have namespace clash.
Regardless of having the label or not, you still need associations like directed_movies, acted_movies for each relationship type. If each module called the association
movies
...Is there any benefit to hiding these chunks of code when the label is not set??
Class methods for associations are called when loading the models, right? and then instances are made. Why bother conditionally adding in the the methods? Won't you have to pepper your code with checks if the methods exist and if the label is set anyway?
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.
Regarding your first point, you'd only need a
directed_movies
association if someone was a director (i.e. has the:Director
label). Put another way, ifperson.director? == false
thenperson.directed_movies
would return an unknown method error.Regarding your second point, if you don't see a benefit to conditionally adding code based on the presence of a label, what have you been advocating
arguingfor this whole time(and by arguing, I don't mean in a negative sense)? It seems clear that we've been operating with different goals.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.
Perhaps I need to go back and re-read some of your comments, given your second point above...I've missed something.
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.
You're absolutely correct. I never really caught the conditional argument. My bad.
But my concern is, as a developer one has to check
person.director?
before called these methods or one has to handle the unknown method error. What I'm trying to understand then is why conditionally include the code if making the checks anyway?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.
Oh ya, you absolutely need to check for
person.director?
Or in my case, I'd need to check forperson.current?
orperson.destroyed?
and handle these two cases differently. I always need to do this though. A current person node is conceptually different from a destroyed person node and needs to be handled different. Its appropriate that different methods exist on each.It's conceptually similar to checking what type of object a variable contains.
As far as why I'd want to conditionally include the code, well mainly because a
current
node might have a method of the same name as adestroyed
node, but the two methods do completely different things. I'd want to check to see what type of node I was dealing with, so I knew what the method did. Obviously you could achieve similar results with some type of method overloading, but there are always multiple ways to accomplish something inrubyprogramming.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.
Also worth noting: if your query only returns
:Person:Actor
nodes, then you know that the resulting objects have the:Actor
methods.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.
Ok, but in the case of :Actor and :Director, a :Person could be both, and now if each one has a method with the same name, you have a clash.
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.
True, but I think that's a limitation of Ruby modules, rather than a flaw with this implementation strategy. If two modules define a method with the same name, and you include both of them in a class, the last one will overwrite the others.
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.
Put another way, if a method might apply to either an :Actor or :Director, you need to be more thoughtful about the naming than if it just applies to one or the other.