Neo4j.rb How to Subclass ActiveRel relationship?

163 Views Asked by At

Newbie Neo4j.rb (v8.2.1) gem user here trying to figure out how to handle subclassing ActiveRel properly...

I have one relationship type called HasAccount that I then subclass in others, i.e. OwnsAccount < HasAccount. I have relationships defined in my model like this:

has_many :out, :accounts, rel_class: :HasAccount
has_many :out, :owned_accounts, rel_class: :OwnsAccount
has_many :out, :managed_accounts, rel_class: :ManagesAccount

My intent is that whenever I create an OwnsAccount or ManagesAccount relationship for a node, they'd also be accessible via my_node.accounts (i.e. the superclass' has_many).

Am I approaching this wrong? I've tried every which way and that shy of ditching HasAccount entirely and definining an accounts method that merges owned_accounts and managed_accounts...

2

There are 2 best solutions below

2
On BEST ANSWER

I don’t think that subclasses ActiveRel classes would help, unfortunately (probably you’ve found that ;) ).

If you didn’t have an ActiveRel class you can specify multiple types (I don’t remember if we implemented type: [:OWNS_ACCOUNT, :MANAGES_ACCOUNT] but you could certainly do either type: "OWNS_ACCOUNT|MANAGES_ACCOUNT”’ or type: false / type: :any).

If you need to have logic on your relationships then you would need ActiveRel. You can have type :any on the ActiveRel class I think, but I don’t know how you’d have separate owned_accounts / managed_accounts associations in that case (I think type and rel_class are mutually exclusive, but it might be work trying).

Really, probably, we should allow for rel_class: [:OwnsAccount, :ManagesAccount], I think

0
On

Adam Sharp @adamsharp Sep 07 13:32 @cheerfulstoic — No delay at all! <24 hours for a free product is lightning service! Feel better too knowing the answer isn't "here's the stupid mistake you made." Thank you. Yeah, I had hoped the inheritance on ActiveRel would be like that on ActiveNode where subclassed items are part of .all at each level. I have some instances in my project where I can do the direct approach, but one or two spots where I need other properties and logic in the relationship.

I also considered two other approaches:

1) Having just HasAccount but with a role property. But I had trouble figuring out the simplest way to write an owns method for the parent node that would be something like:

def owns
  self.accounts.(SOME LOGIC FOR role == 'owner' IN THE REL)
end

Also was unsure how to change the role after set, and wondered if performance-wise separate rels would be better.

2) Refactoring all the logic into an intermediate ActiveNode type, i.e. AccountRole in this example , which I could subclass such that an OwnerRole < AccountRole node would be included in either scope. Then key bits of my models would be:

class AccountRole
  include Neo4j::ActiveRel

  from_class :User
  to_class   :Account

  def account
    self.to_node
  end
end

class OwnerRole < AccountRole

end

class User

  include Neo4j::ActiveNode
  include Neo4j::Timestamps

  has_many :out, :has_account_roles, rel_class: :AccountRole
  has_many :out, :has_owner_roles, rel_class: :OwnerRole

  def accounts
    self.has_account_roles.map{ |role| role.account }
  end

  def owns
    self. has_owner_role.map{ |role| role.account }
  end

end

Leaning toward #2 right now, but very keen for your take.

Thanks again!

Brian Underwood @cheerfulstoic Sep 07 20:29 @adamsharp Actually, it should be pretty easy to do #1 with something like self.accounts.rel_where(role: ‘owner) The owns method would then still return a QueryProxy representing Account nodes which you could filter further or chain associations off of. You could also have a class method with the same name on User implemented with all (like self.accounts.rel_where(role: ‘owner)) which would allow you to chain like User.all.owns or Something.users.owns, etc...

Adam Sharp @adamsharp Sep 07 21:44 Awesome. Thanks @cheerfulstoic! Will give that a try. What's the easiest way to change a rel property? (Ie from owner to manager)?

Brian Underwood @cheerfulstoic Sep 07 21:57 If you have an ActiveRel object you could just say rel.role = ‘owner’; rel.save / rel.update(role: ‘owner’). Otherwise you’d use Cypher or Ruby APIs like user.owns(:node, :rel).where(id: account.id).query.set(rel: {role: ‘owner’}).exec (obviously that’s more verbose, but that syntax can be useful if you’re updating more than one)

Adam Sharp @adamsharp 13:48 Thank you @cheerfulstoic. Yeah, it was trying to construct the latter and generally figuring out clean ways of accessing the relevant object that tied me in knots on that front. Will go this route. Thanks for the help! Been a SQL guy my whole career. This is fun.