Neo4j rb inheritance - ruby-on-rails

I am using Neo4j.rb in my rails app.
I am having trouble with polymorphism.
I have something like this I have a Users class which looks like this:
class User
include Neo4j::ActiveNode
#other properties
has_many: out, :social_media_content, model_class: :SocialMediaContent
end
I have the SocialMediaContent class
class SocialMediaContent
include Neo4j::ActiveNode # Is it necessary?
property :first_name, type: Integer
property :first_name, type: Integer
end
I want to have an image class and a video class which inherit from Social media content
Does this mean that while creating a relationship between SocialMediaContent and a User i can provide an image or a video object instead of SocialMediaContent, How does it happen in the database, do i need to have a node for the image as well or can it be an ordinary object.
i.e
class Image < SocialMediaContent
include Neo4j::ActiveNode #Do i have to do this?
I want a behaviour like this: every User has many SocialMediaContent which can be either an image or a video (for now) i do not want to specify now, which one of them is an image and which is a video.
Can anyone suggest how i might acheieve this?
And finally can i store an array of SocialMediaContent instead of has_many associations (which is better?)

Yes, you can definitely do this. In Neo4j.rb when you inherit a class from an ActiveNode class then the child class represents nodes with both labels. For example:
class SocialMediaContent
include Neo4j::ActiveNode # This is necessary for the parent
end
class Image < SocialMediaContent
# No include needed
end
class Video < SocialMediaContent
# No include needed
end
Now if you do Image.create it will create a node which has both the Image and SocialMediaContent labels. If you search for an image with Image.find, Image.where, etc... it will limit to only nodes with both labels.
As for the association, you should be able to specify it as you have it in the question (though it's going to complain if you don't have a type, rel_class, or origin option).

Related

Defining attributes at runtime based on data from related object

I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.
Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.
So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.
I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
organisation.list_attributes.each do |a, t|
attribute a, t
end
end
Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:
{
name: String,
age: Integer
}
As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.
I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.
Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.
So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?
1 To clarify, if I were to use the hash directly like so:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
{
name: String,
age: Integer
}.each do |a, t|
attribute a, t
end
end
it would work, my issue is solely with getting a record's relation at this earlier point in time.
2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.
at the moment of instantiating (initializing or loading) a record, is
there a way I can retrieve a hash stored in a database column of one
of its relations?
Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:
class ListItem < ApplicationRecord
belongs_to :list
end
class List < ApplicationRecord
belongs_to :organisation
has_many :list_items
end
class Organisation < ApplicationRecord
has_many :lists
end
We can instantiate a ListItem and then retrieve data from anyone of its parents.
#list_item = ListItem.find(5) # assume that the proper inherited
foreign_keys exist for this and
its parent
#list = #list_item.list
#hash = #list.organisation.special_hash_of_org
And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:
class ListItem < ApplicationRecord
belongs_to :list
# this is called on ListItem.new and whenever we pull from our DB
after_initialize do |list_item|
puts "You have initialized a ListItem!"
list = list_item.list
hash = list.organisation.special_hash_of_org
end
end
But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!

In a STI table/modeling schema, how can one have type specific attributes that are shared by all types?

I have a set up where I have multiple models inheriting from a Base model - standard Single Table Inheritance:
class Parent < ActiveRecord::Base
end
class A < Parent
end
class B < Parent
end
My STI setup is correct and works great! However, I want to add :type specific attributes such as description.
For example, I want all A types of Parent to have the description, "I am the A type of Parent. My function is for..."
I want to avoid replicating data over and over (having each instance of A store the same description for example).
The first thing that came to mind for this was to have a model specific method on the Subclass. So something like:
class A < Parent
def self.description
"I am the A type of Parent. My function is for..."
end
end
I don't like this solution because this really is data on the specific type of subclass (rather than on the subclass instance itself) and you get all the problems that come with making this behavior (deployments to change data, etc.)
Is this the only way to do it or are there alternatives I just am not seeing?
What about creating a model for the description?
class Description < ActiveRecord::Base
has_many :as
end
class A < Parent
belongs_to :description
before_save { description_id = 1 }
end
This way, you manage the content of description in the database and can modify it through either a web interface or migrations. Furthermore, you can easily add different descriptions for the different subclasses, or even change them per instance if that is ever required.
One downside of this approach is that you need to create the model with the correct description. One potential solution could be the before_save or before_create hook, but I'm sure those are not the only way to do it.
for your case I prefer to use ruby Duck typing as follow
class ParentAll
def talk(object1)
object1.talk
end
end
class A < ParentAll
def talk
puts 'I am the A type of Parent. My function is for...'
end
end
class B < ParentAll
def talk
puts 'I am the B type of Parent. My function is for...'
end
end
#parent = ParentAll.new
puts 'Using the A'
#parent.talk(A.new)
# this will output talk from A
puts 'Using the B'
#parent.talk(B.new)
# this will output talk from B

Mongoid "reverse polymorphism"? How to embed multiple class types under one field that doesn't match class name

I have one parent object, Order, that I need to embed one channel object. The thing is, the object that is the channel can be one of N classes within a module called Channels.
I keep getting the following error, because Mongoid can't look up the right class for the relation. How can I essentially tell Mongoid that the class of the :class field can be one of several? Essentially, I won't know until I'm creating the order which of the concrete classes in the Channels module it will need.
(This seems a bit like "reverse polymorphism" since it's the parent that has to handle multiple types, as opposed to the child being embeddable in multiple types of parents.)
My classes are like so:
class Order
# ...
with_options cascade_callbacks: true do |o|
o.embeds_one :channel
# I've tried several combinations of making the child object polymorphic
# using the `as`/`belongs_to` syntax, to no avail
end
# ...
end
module Channels
class Base
embedded_in :order
# lots of code here that all concrete channels share
end
# then there are concrete subclasses of Channels::Base, e.g.
class Channel1 < Channels::Base
# specifics omitted...
end
class Channel2 < Channels::Base
# specifics omitted...
end
end
The error I get when trying to load/save an order in the shell is this: NameError: uninitialized constant Channel
facepalm The solution is very simple and should have been obvious from the error/stack trace:
Either create a base class (not in the Channels module) called Channel, or
Specify the class_name as the base class, e.g. o.embeds_one :channel, class_name: "Channels::Base"
Either of these will work and allow me to override the field with the appropriate concrete subclass for my channel.
Error results from the fact that Mongoid is calling #constantize (or equivalent) under the hood, and no base Channel class exists.

Restricting Rails engine functionality to a certain class rather than ActiveRecord::Base

I've created a rails engine which contains some common functionality I need when creating new users. For example, there's a before_validation(on: :create) hook that populates a certain field with something to ensure no user can be created without this field having something in it. It looks similar to:
module OfflineUser
extend ActiveSupport::Concern
included do
before_validation(on: :create) do
self.member_number = rand(0..100000)
end
end
end
ActiveRecord::Base.send(:include, OfflineUser)
If I include the engine into another project and do User.create it correctly populates a member_number field for me. However, because it's added the methods to ActiveRecord::Base, it also tries to populate that field in every model I try to run create on! How can I restrict this functionality to only the User model or other model of my choosing rather than globally on every model. Thanks.
By including it in the specific class:
class User < ActiveRecord::Base
include OfflineUser
end
Get rid of the your last line where you include the module in ActiveRecord::Base.

How to dynamically set a belongs_to association in ActiveRecord?

I'm trying to create a mixin that allows an ActiveRecord model to act as a delegate for another model. So, doing it the typical way:
class Apple < ActiveRecord::Base
def foo_species
"Red delicious"
end
end
class AppleWrapper < ActiveRecord::Base
belongs_to :apple
# some meta delegation code here so that AppleWrapper
# has all the same interface as Apple
end
a = Apple.create
w = AppleWrapper.create
w.apple = a
w.foo_species
# => 'Red delicious'
What I want is to abstract this behavior into a Mixin, so that given a bunch of data models, I can create "Wrapper" classes that are also ActiveRecords, but that each wrapper corresponds to a specific class. Why? Each of the data models have calculations, aggregations with other models, and I want the "Wrapper" classes to contain fields (in the schema) that correspond to these calculations...so in effect. the Wrapper acts as a cached version of the original data model, with the same interface.
I will have to write out each Wrapper...so for Apple, Orange, Pear, there is a different Wrapper model for each of them. However, I just want to abstract out the wrapper behavior...so that there's a class level method that sets what the Wrapper points to, a la:
module WrapperMixin
extend ActiveSupport::Concern
module ClassMethods
def set_wrapped_class(klass)
# this sets the relation to another data model and does the meta delegation
end
end
end
class AppleWrapper < ActiveRecord::Base
include WrapperMixin
set_wrapped_class Apple
end
class OrangeWrapper < ActiveRecord::Base
include WrapperMixin
set_wrapped_class Orange
end
How would I set this up? And would this have to be a STI type relation? That is, does the Wrapper class have to have a wrapped_record_id and a wrapped_record_type?
You can use belongs_to in your set_wrapped_class method.
def set_wrapped_class(klass)
belongs_to klass.to_s.downcase.to_sym
end

Resources