Postgresql JSONB nested form ruby on rails - ruby-on-rails

I have product as active record table and option_type as activemodel model. option types is an array of objects as follows,
[
{name: 'color', values: ['red', 'blue']},
{name: 'size', values: ['small', 'medium']}
]
class OptionType
include ActiveModel::Model
attr_accessor :name, :values, :default_value
def initialize(**attrs)
attrs.each do |attr, value|
send("#{attr}=", value)
end
end
def attributes
[:name, :values, :default_value].inject({}) do |hash, attr|
hash[attr] = send(attr)
hash
end
end
class ArraySerializer
class << self
def load(arr)
arr.map do |item|
OptionType.new(item)
end
end
def dump(arr)
arr.map(&:attributes)
end
end
end
end
I want to desing a form_for with nested form for option_types so that user can add various option names and it's values. How to do it?
reference links are as follow,
Validation of objects inside array of jsonb objects with RubyOnRails

I know this isn't the answer you're hoping for but instead of just tossing the whole lot into a JSONB column and hoping for the best you should model it as far as possible in a relational way:
class Product < ApplicationRecord
has_many :options
has_many :product_options, through: :options
end
# rails g model option name:string product:belongs_to
class Option < ApplicationRecord
belongs_to :product
has_many :product_options
end
# rails g model product_option option:belongs_to name:string ean:string
class ProductOption < ApplicationRecord
belongs_to :option
has_one :product, through: :options
end
If your data is actually structured enough that you can write code that references its attributes then a JSON column isn't the right answer. JSON/arrays aren't the right answer for setting up assocations either.
This lets you use foreign keys to maintain referential integrity and has a somewhat sane schema and queries instead of just dealing with a totally unstructed mess. If you then have to deal with an attribute that can have varying types like for example an option that can be string, boolean or numerical you can use a JSON column to store the values to somewhat mitigate the disadvantages of the old EAV pattern.
Creating variants of a product could then either be done via a seperate form, nested attributes or AJAX depending on your requirements.

Related

Is there a better way to find children and parents of an ActiveRecord object?

I'm working on a gem to export a small portion of related ActiveRecord objects.
Here's how I'm currently finding parents & children.
# belongs_to, based on column names with an _id suffix
def belongs_to_relations(ar_instance)
columns = ar_instance.class.column_names
parents = columns.map{ |c| c if c =~ /_id/ }.reject{ |c| c.nil? }
parents.map!{ |parents| parents.gsub('_id', '') }
end
# has_many, based on existence of a xxx_id column in other tables
def has_many_relations(ar_instance)
column_name = "#{ar_instance.class.name.underscore}_id"
descendents = ActiveRecord::Base.connection.tables
descendents.reject!{ |table| false unless table.classify.constantize rescue true }
descendents.reject!{ |table| true unless table.classify.constantize.column_names.include?(column_name) }
end
Is there a better way to find these relationships? This works ok, but distant relationships, like :through, I have to specify manually.
Use class.reflections. It returns information about a model's relationships.
Imagine you have this simple set up:
# user.rb
belongs_to :user_type
has_many :user_logs
If you call User.reflections you will get a hash similar to the following:
{
:user_type => <Reflection #macro=:belongs_to, etc. ...>,
:user_logs => <Reflection #macro=:has_many, etc. ...>
}
The reflection is an instance of ActiveRecord::Reflection::AssociationReflection or ActiveRecord::Reflection::ThroughReflection. It contains information about which model it references, what the options are (like dependent => :destroy), what type of association it is (the #macro in my example), etc.
I'm not exactly sure if that's what you're looking for but ActiveRecord has helpers to do this.
In your models:
#school.rb
has_many :classrooms
#classroom.rb
belongs_to :school
You can now use pretty much anywhere:
school = random_classroom.school
classrooms = school.classrooms
For a has_many :through relationship:
# school.rb
has_many :students,
:through => :classrooms

Rails select value into attribute

Say I have a simple model like this with a field called "name" and an attribute called "aliased_name":
class User < ActiveRecord::Base
attr_accessor :aliased_name
end
User.create(name: "Faye Kname")
I can do:
user=User.select(:id, :name)
user.name # Faye Kname
But how can I use select to populate the aliased_name attribute.
user=User.select(:id, "name AS aliased_name")
user.aliased_name # nil
user[:aliased_name] # Faye Kname
I can access on the :aliased_name symbol, but the attribute is not assigned. I'd like to not have to do
user.aliased_name = user[:aliased_name]
I'm actually doing a more complex join on another table and I'm trying to select a field from the join table into the alias, but figured this would be a simpler example.
Typically I do these kinds of aliases with methods instead of attr_accessors. Something like
def aliased_name
has_attribute?(:aliased_name) ? read_attribute(:aliased_name) : self.name
end
The has_attribute? is there in case you didn't load the attribute with your query, so you can have a default value.
So the attr_accessor is looking for the instance variable #aliased_name which I don't think is being set in your code. You can set it with #aliased_name = "some value" or using the attr_accessor aliased_name = "some value", but it's not going to be set with the initial query that returns the object, or in the second SELECT query, at least as it's written now.
One route that might make sense would be to use both a separate method and attr_writer. Something like this
attr_writer :aliased_name
def aliased_name
#aliased_name ||= self.name
end
This sets the instance variable the first time it's called and leaves you free to change it with the attr_writer. I'm not sure how this fits in with the more complex join, but this is a fairly simple way to solve the problem you describe initially.
You may be better using alias_attribute:
#app/models/user.rb
class User < ActiveRecord::Base
alias_attribute :aliased_name, :name
end
Although it will only take user.name data & put it into user.alias_attribute
I'm trying to select a field from the join table into the alias
Done this before:
Rails Scoping For has_many :through To Access Extra Data
Accessing additional values on has_many through Rails
You have two options. Either use an SQL ALIAS column, or access the proxy_association method in your model. I have worked extensively with both:
--
SQL Alias
#app/models/parent.rb
class Parent < ActiveRecord::Base
has_many :joins
has_many :children, -> { select("#{Parent.table_name}.*, #{Join.table_name}.attr AS alias_name") }, through: :joins, dependent: :destroy
end
This will give you...
#parent.children.each do |child|
child.alias_name
end
--
Association Extensions
The next method is a lot more complicated; more efficient:
#app/models/parent.rb
class Parent < ActiveRecord::Base
has_many :joins
has_many :children, through: :joins, -> { extending AliasAttribute }
end
#app/models/concerns/alias_attribute.rb
module PlayerPermission
#Load
def load
alias_names.each do |permission|
proxy_association.target << permission
end
end
#Private
private
#Names
def names
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({name: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:name)
end
#Target
def target_collection
#load_target
proxy_association.target
end
end
Each time you call an association, you have access to the .association object for it. Within the association itself, you have access to proxy_association objects; all of which can be manipulated to insert the aliased data into your parent data.
The above will allow you to use:
#parent = Parent.find x
#parent.children.each do |child|
child.alias_name
end
I can provide support if required.

How to remove Rails nested association before running validations?

I'm using Mongoid and embedded documents. I am using the standard approach to using a hidden form field of _destroy with a value of 1. This works fine, except for when a validator is run which includes the association that I'm trying to delete. For example:
class Thing
include Mongoid::Document
embeds_many :actions
validate :uniqueness_of_actions
def uniqueness_of_actions
subjects = actions.map(&:subject)
subjects_are_unique = subjects == subjects.uniq
errors.add(:actions, 'must have unique subjects') unless subjects_are_unique
subjects_are_unique
end
end
What is the proper way to remove/exclude the associations marked for destruction before validations are run which might include them?
Try something along these lines.
class Artist < AR::Base
has_many :songs
validate :custom_thing
def custom_thing
songs.reject{ |x| x._destroy}.each do |a|
# magic here
end
end
end

How to migrate from belongs_to, to embedded_in in Mongoid?

If one first build their models with a belong_to and has_many association and then realized they need to move to a embedded_in and embeds_many association, how would one do this without invalidating thousands of records? Need to migrate them somehow.
I am not so sure my solution is right or not. This is something you might try to accomplish it.
Suppose You have models - like this
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
At first step I will create another model that is similar to the Book model above but it's embedded instead of referenced.
#EmbedBook Model
class EmbedBook
include Mongoid::Document
field :title
embedded_in :user
end
#User Model (Update with EmbedBook Model)
class User
include Mongoid::Document
embeds_many :embed_books
has_many :books
end
Then create a Mongoid Migration with something like this for the above example
class ReferenceToEmbed < Mongoid::Migration
def up
User.all.each do |user|
user.books.each do |book|
embed_book = user.embed_books.new
embed_book.title = book.title
embed_book.save
book.destroy
end
end
end
def down
# I am not so sure How to reverse this migration so I am skipping it here
end
end
After running the migration. From here you can see that reference books are embedded, but the name for the embedded model is EmbedBook and model Book is still there
So the next step would be to make model book as embed instead.
class Book
include Mongoid::Document
embedded_in :user
field :title
end
class User
include Mongoid::Document
embeds_many :books
embeds_many :embed_books
end
So the next would be to migrate embedbook type to book type
class EmbedBookToBook < Mongoid::Migration
def up
User.all.each do |user|
user.embed_books.each do |embed_book|
book = user.books.new
book.title = embed_book.title
book.save
embed_book.destroy
end
end
def down
# I am skipping this portion. Since I am not so sure how to migrate back.
end
end
Now If you see Book is changed from referenced to embedded.
You can remove EmbedBook model to make the changing complete.
This is just the suggestion. Try this on your development before trying on production. Since, I think there might be something wrong in my suggestion.
10gen has a couple of articles on data modeling which could be useful:
Data Modeling Considerations for MongoDB Applications
Embedded One-to-Many Relationships
Referenced One-to-Many Relationships
MongoDB Data Modeling and Rails
Remember that there are two limitations in MongoDB when it comes to embedding:
the document size-limit is 16MB - this implies a max number of embedded documents, even if you just embed their object-id
if you ever want to search across all embedded documents from the top-level, then don't embed, but use referenced documents instead!
Try these steps:
In User model leave the has_many :books relation, and add the
embedded relation with a different name to not override the books
method.
class User
include Mongoid::Document
has_many :books
embeds_many :embedded_books, :class_name => "Book"
end
Now if you call the embedded_books method from a User instance
mongoid should return an empty array.
Without adding any embedded relation to Book model, write your own
migration script:
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
belongs_to :user
def self.migrate
attributes_to_migrate = ["title","price"] # Use strings not symbols,
# we keep only what we need.
# We skip :user_id field because
# is a field related to belongs_to association.
Book.all.each do |book|
attrs = book.attributes.slice(*attributes_to_migrate)
user = book.user // through belong_to association
user.embedded_book.create!(attrs)
end
end
end
Calling Book.migrate you should have all the Books copied inside each user who was
associated with belongs_to relation.
Now you can remove the has_many and belongs_to relations, and
finally switch to clean embedded solution.
class User
include Mongoid::Document
embeds_many :books
end
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
embedded_in :user
end
I have not tested this solution, but theoretically should work, let me know.
I have a much shorter concise answer:
Let's assume that you have the same models:
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
So change it to embeds:
#User Model
class User
include Mongoid::Document
embeds_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
embedded_in :user
end
And generate a mongoid migration like this:
class EmbedBooks < Mongoid::Migration
##attributes_to_migrate = [:title]
def self.up
Book.unscoped.where(:user_id.ne => nil).all.each do |book|
user = User.find book[:user_id]
if user
attrs = book.attributes.slice(*##attributes_to_migrate)
user.books.create! attrs
end
end
end
def self.down
User.unscoped.all.each do |user|
user.books.each do |book|
attrs = ##attributes_to_migrate.reduce({}) do |sym,attr|
sym[attr] = book[attr]
sym
end
attrs[:user] = user
Book.find_or_create_by(**attrs)
end
end
end
end
This works because when you query from class level, it is looking for the top level collection (which still exists even if you change your relations), and the book[:user_id] is a trick to access the document attribute instead of autogenerated methods which also exists as you have not done anything to delete them.
So there you have it, a simple migration from relational to embedded

Rails: How do fetch, then iterate over all child (+child, etc) models of a parent model?

Is there a way to easily and efficiently fetch the corresponding (child) models of a parent model and then render it in a template? I would like to know how to do it with and without joins
For Example, Consider these 3 tables:
# ProductGroup is the highest parent
class ProductGroup < ActiveRecord::Base
attr_accessible :name, :merchant_id
has_many :product_items
has_many :product_group_selections
end
# ProductItem is a child of ProductGroup
class ProductItem < ActiveRecord::Base
attr_accessible :base_price, :name, :product_group_id
belongs_to :product_group
end
# ProductGroupSelection is a child of ProductGroup
class ProductGroupSelection < ActiveRecord::Base
attr_accessible :name, :price_extra, :product_attr_group_id, :product_item_id
belongs_to :product_group
has_many :product_group_selection_attrs
end
# ProductGroupSelectionAttr is a child of ProductGroupSelection
class ProductGroupSelectionAttr < ActiveRecord::Base
attr_accessible :name, :product_group_id
belongs_to :product_group_selection
end
What I want is a data-structure that looks like this (when searching product_groups for merchant_id = 1)
merchant_id 1 => {
ProductGroup.name, ProductGroup.merchant_id,
ProductItems => [...],
ProductGroupSelections => {ProductGroupSelections.name, ProductGroupSelectionAttrs => [...]}
}
This way I can loop through, in-turn, all groups and their sub models to generate a form using ERB.
Thank you
When iterating over a collection of records that in turn have collections you'll run into the infamous N+1 query. Essentially for every ProductGroup you would run a query to pull back all of it's ProductItem records. And worse if your working with 2 levels of relations.
To make this work more efficiently you want to make use of includes which ActiveRecord defines as a means of eager loading associations in as few queries as possible.
ProductGroup.includes(:product_items).includes(:product_group_selections => :product_group_selection_attrs)
From there you simply add on any conditions you need and whatever gets loaded for ProductGroup will ensure that all of the associated models also get loaded.
Now you just iterate normally over your associations. Assuming #product_groups is has a collection of ProductGroup
#product_groups.each do |product_group|
# do stuff with product_group
product_group.product_items.each do |product_item|
# do stuff with product_item
end
product_group.product_group_selections do |product_group_selection|
# do stuff with product_group_selection
product_group_selection.product_group_selection_attrs do |product_group_selection_attr|
# do stuff with product_group_selection_attr
end
end
end
The default way that rails sets up associations should fulfill the data structure you asked for, just with actual records instead of a hash of hashes, which you would need to load anyway to create the hash of hashes.
Maybe something like this:
class ProductGroup < ActiveRecord::Base
# I have no idea what to call this method
def merchant_data
{:name => self.name, :merchant_id => self.merchant_id, :items => self.product_items, :selections => self.product_group_selections}
end
end
Inside of your controller you would have something like:
def merchant_search
#product_group = ProductGroup.find_by_merchant_id(params[:merchant_id})
#merchant_data = #product_group.merchant_data
##merchant_data => {:name=>"...", :merchant_id=> 1, :items=>[....], :selections=>[..]}
end
Simply make use of the hash inside your view similar to how you would work with any other instance variable, only this time with a Hash. For instance if you wanted to loop through all the items inside of the returned data structure simply:
#merchant_data[:items].each {|item| ... }

Resources