Ruby on Rails: In Json-API passing parameters to inner serializer of compound document - ruby-on-rails

For a ruby/rails JSON-API application using jsonapi-serializer let us say we want to support a query like:
https://example.com/movies/100?include=actors
Suppose that we have a custom attribute for the ActorSerializer that requires some data that is not available in the model. Say for example the current location of actors:
class ActorSerializer
include JSONAPI::Serializer
set_type :actor
attributes :first_name, :last_name
attribute current_location do |object, params|
params[:actor_locations][object.id]
end
end
Further suppose we are getting the actor location data from an external website that gives the locations of all the actors in one query, and for performance issues we do not wish to make more than one call to this other website per request. Thus we do not want our current_location custom method to have to call the external website as a movie can have_many actors. Instead we have the query done in the controller method before it creates the json serializer object to be rendered.
Although I can see how to handle passing the location data as a parameter to make a ActorSerializer in the Actor controller, I am not sure how to get the data passed in there from the Movie controller that would presumably use a serializer something like:
class MovieSerializer
include JSONAPI::Serializer
set_type :movie
attributes :title, :year, :rating
has_many :actors
end
Now I am not particular about the data getting there as a serializer parameter. As far as I know there may be some other way for a custom method in an inner serializer in a compound document to get at data that the controller has access to. Some kind of globally available data across the whole request or the like? I am new to ruby and rails. Just looking for a good solution (my actual problem is not about movies and actors but more complicated).

Seems my problem resulted from presuming that ActiveRecords could only have instance data that was in the corresponding db record.
So all I had to do was make an attribute for the extra data like:
class Actor < ActiveRecord::Base
...
attr_accessor :current_location
...
end
With this done the controller can make the query to the external service and update all the actor records for the movie with the results. The serializer then can include the locations from this attribute with no need for any extra parameters passed to the serializer method.

Related

How to know if an "includes" call was made while working with a single record?

Motivation
The motivation was that I want to embed the serialization of any model that have been included in a Relation chain. What I've done works at the relation level but if I get one record, the serialization can't take advantage of what I've done.
What I've achieved so far
Basically what I'm doing is using the method includes_values of the class ActiveRecord::Relation, which simply tells me what things have been included so far. I.e
> Appointment.includes(:patient).includes(:slot).includes_values
=> [:patient, :slot]
To take advantage of this, I'm overwriting the as_json method at the ActiveRecord::Relation level, with this initializer:
# config/initializers/active_record_patches.rb
module ActiveRecord
class Relation
def as_json(**options)
super(options.merge(include: includes_values)) # I could precondition this behaviour with a config
end
end
end
What it does is to add for me the option include in the as_json method of the relation.
So, the old chain:
Appointment.includes(:patient).includes(:slot).as_json(include: [:patient, :slot])
can be wrote now without the last include:
Appointment.includes(:patient).includes(:slot).as_json
obtaining the same results (the Patient and Slot models are embedded in the generated hash).
THE PROBLEM
The problem is that because the method includes_values is of the class ActiveRecord::Relation, I can't use it at the record level to know if a call to includes have been done.
So currently, when I get a record from such queries, and call as_json on it, I don't get the embedded models.
And the actual problem is to answer:
how to know the included models in the query chain that retrieved the
current record, given that it happened?
If I could answer this question, then I could overwrite the as_json method in my own Models with:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend Associations
def as_json(**options)
super(options.merge(include: included_models_in_the_query_that_retrieved_me_as_a_record))
end
end
One Idea
One Idea I have is to overwrite the includes somewhere (could be in my initializer overwriting directly the ActiveRecord::Relation class, or my ApplicationRecord class). But once I'm there, I don't find an easy way to "stamp" arbitrary information in the Records produced by the relation.
This solution feels quite clumsy and there might be better options out there.
class ApplicationRecord < ActiveRecord::Base
def as_json(**options)
loaded_associations = _reflections.each_value
.select { |reflection| association(reflection.name).loaded? }
.map(&:name)
super(options.merge(include: loaded_associations))
end
end
Note that this only loads 1st level associations. If Appointment.includes(patient: :person) then only :patient will be returned since :person is nested. If you plan on making the thing recursive beware of circular loaded associations.
Worth pointing out is that you currently merge include: ... over the provided options. Giving a user no choice to use other include options. I recommend using reverse_merge instead. Or swap the placements around {includes: ...}.merge(options).

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!

Advanced ruby on rails models and validation

In general, I have a website which needs to have complex registration process. And in that registration process I need to include 4 tables from database.
Now... I cannot validate one by one model and to enter 4 of them in database. Is there a way to make common points of all those tables in one model?
Let's say:
User model has columns: username, name, etc.
Package model has: type, account_number
etc
And in registration process I need to include username, name, account_number and to validate them. How to do that?
Without seeing your model structure, this is just speculation, but here goes:
--
Virtual Attributes
In your User model, you can use attr_accessor to create a set of virtual attributes - which basically mean you can create a series of setter / getter methods in your User model.
Although I don't think this will help you directly, it should give you an idea as to how you can create single-model validation:
#app/models/user.rb
Class User < ActiveRecord::Base
attr_accessor :new, :values, :to, :validate
validates, :new, :values, :to, :validate, presence: true
end
This will allow you to create the attributes in the User model - and although they won't be saved, you can then use them to validate against
attr_accessor
To give you a quick overview of this method, you first need to remember that Rails is just a collection of modules and classes.
This means that every model you load is just a class which has been populated with a series of getter / setter methods. These methods are defined by ActiveRecord from your data table columns, and are why you can call #user.attribute
The magic is that if you use attr_accessor, you'll basically create your own attributes in your User model - which won't be saved in the database, but will be treated like the other attributes your objects have, allowing you to validate them
Because your registration process seems to be complex, I would go even futher as virtual attributes and use Form Objects
7 Patterns to Refactor Fat ActiveRecord Models
LA Ruby Conference 2013 Refactoring Fat Models
ActiveModel Form Objects
I understand that you multistep registration. You shouldn't create 4 models only because your view pages needs it. You should:
remove validation from User model and add validation on each form
create 4 different forms (for example extends by ActiveModel or user gem reform)
add validation to each form
after form.valid? save part of user info to #user object
Thats all.

mongodb _id reuse after deletion

Suppose I deleted a document or subdocument in mongodb. Can I create document / subdocument with the same _id as the deleted one? In this case, we assume, we cannot do update operation, just delete and create.
For example using Mongoid (Rails gem for mongodb) :
We have Person Model
class Person
include Mongoid::Document
field :a, :type => String
embeds_many :personattributes
end
class Personattribute
include Mongoid::Document
field :myattribute, :type => String
embedded_in :person
end
And in my Rails controller
class MyController < ApplicationController
...
#the_attributes=#person.personattributes.entries
...
#controller will render page, an instance variable #the_attributes will be available as JSON in clientside
end
Then user does some client side data modifications. They can add 1 or more personattributes to that person data. They can do some changes on its attributes. They can delete some also.
All in client side.
Then by AJAX call, user sends the modified data back in JSON format like
[{_id:"5253fd494db79bb271000009",myattribute:"test"},{...},...]
The retriever in controller retrieves the data
Then totally replace the attribute list inside person with the new one. Total deletion and insertion, no update.
class MyController < ApplicationController
...
#person.personattributes.delete_all #delete all attributes a #person has
attributes=params[:attributes]
attributes.map {|attr|
Personattribute.new(:_id => Moped::BSON::ObjectId.from_string(attr["_id"].to_s), :myattribute => attr["myattribute"])
}
#person.personattributes=attributes
#person.save
...
end
Can I do this? It simply means, delete all, and insert all and reuse the _ids.
If not, I will be happy to get some advice on a better approach on this.
I can't do upsert since the deleted documents will need another loop to handle.
Thank you
Yes, you can do it but I would recommend you not to do that. It seems to have lots of security issues if someone modifies the array manually
I could send:
[{_id:"5253fd494db79bb271000009",myattribute:"test_modified"},{...},...]
or even:
[{_id:"my_new_id_1",myattribute:"test_modified"},{...},...]
which would raise an exception
Moped::BSON::ObjectId.from_string "my_new_id_1" #=> raises an Exception
Try something like:
attributes=params[:attributes]
attributes.each do |attr|
#person.personattributes.find(attr["_id"]).myattribute = attr["myattribute"]
#or #person.personattributes.find(attr["_id"]).try(:myattribute=,attr["myattribute"])
end
Probably in a future you want to change the action and send just the modified personattributes in the array instead of all the personattributes. What would you do then if you delete_all and rebuild personattributes with just the sent personattributes?
EDIT
This handles personattributes updates. Create or delete personattributes should go in different actions:
Create action
#person.personattributes.push Personattribute.new(my_attribute: params[:my_attribute])
Delete action
#person.personattributes.delete(params[:personattribute_id])
Yes, you can keep using the same _id. They just need to be unique within a collection -- and that's only true for the document's _id.
Any ObjectId you might use elsewhere in an another field in a document (or in a subdocument) doesn't need to be unique, unless you've created an index where it must be unique.

Converting Rails attributes to property-value pairs

I have a Media model that has a bunch of standard metadata attributes and is persisted in the database as normal. What I want to do now is to add some configurable metadata attributes to this model on top of the existing attributes. A list of these attributes will be defined in a config file and loaded in at runtime. They'll be stored in the database in a different table as a series of property-value pairs with an association to the main model.
So, my code currently is,
class Media < ActiveRecord::Base
has_many :custom_metadata
attr_accessible :title, :language, :copyright, :description
end
and
class CustomMetadata < ActiveRecord::Base
belongs_to :media
attr_accessible :name, :value
end
What I want to do is to be able to access and update the custom metadata attributes on the Media model in the same way as the standard metadata attributes. For example, if the custom metadata attributes are called publisher and contributor, then I want to access them in the Media model as #media.publisher and #media.contributor even though they will be in the association #media.custom_metadata where its values would be something like [{:name => 'publisher', :value => 'Fred'}, {:name => 'contributor', :value => 'Bill'}]
It seems to be that virtual attributes would be the best way of achieving this but all of the examples I can find of people using virtual attributes is where the names of the attributes are static and known rather than dynamic from a run-time configuration, so they can define methods such as publisher and publisher= which would then contain code to write to the relevant associated property-value record.
I can define attributes on the class with attr_accessor *Settings.custom_metadata_fields (assuming Settings.custom_metadata_fields returns [:publisher, :contributor]) and also allow mass-assignment using a similar technique with attr_accessible.
The part I get stuck on is how to populate the virtual attributes from the association when loading the data from the record and then, in reverse, how to pass the data in the virtual attributes back into the association before the record is saved.
The two ways I currently see this working are either using method_missing or attribute_missing, or perhaps via initialize and a before_save callback? In either case, I'm not sure how I would define it given that my model has a mix of normal attributes and virtual attributes.
Any suggestions?
Using callbacks sounds reasonable.
What database are you using? If PostgreSQL, maybe you should take a look at HStore extension (http://www.postgresql.org/docs/9.2/static/hstore.html)
it will perform better, and there are some gems making it easy to use.
After looking into the callbacks some more I discovered the after_initialize callback and this is much better than using the initialize method as I'd first planned.
In the end, this was the final code for the Media model and I didn't change anything in the CustomMetadata model from what I defined in the question,
class Media < ActiveRecord::Base
has_many :custom_metadata
attr_accessor *Settings.custom_metadata_fields
attr_accessible :title, :language, :copyright, :description
attr_accessible *Settings.custom_metadata_fields
validates_presence_of *Settings.required_custom_fields
before_save :save_custom_metadata
after_initialize :load_custom_metadata
def load_custom_metadata
MediaMetadata.custom_all_fields.each do |field|
custom_record = custom_metadata.where(:name => field.to_s).first_or_initialize()
send("#{field}=", custom_record.value)
end
end
def save_custom_metadata
MediaMetadata.custom_all_fields.each do |field|
custom_record = custom_metadata.where(:name => field.to_s).first_or_initialize()
custom_record.value = send(field)
if custom_record.value.blank?
custom_record.destroy
else
custom_record.save
end
end
end
end
This solution had a couple of nice benefits. Firstly, it doesn't affect any of the normal attributes on the Media model. Secondly, only custom metadata with actual values are stored in the custom metadata table. If the value is blank, the record is removed completely. Finally, I can use standard validations on the model attributes as shown for my required custom metadata attributes.

Resources