why don't rails tableless models include associations? - ruby-on-rails

After looking at a few tableless solutions in Rails (virtus, active_attr, activemodel) it's clear that Rails associations are not supported. My question is why not? Is there some obvious reason for this that I'm missing? Seems like associations would be extremely useful but in all the examples I've seen they're left out.

I'm not sure how to answer your question of why it is not supported by here is one way you could support it with Rails 4+. This would not require you to have a database table and would also give you access to things like validations, associations, and some callbacks like after_initialize.
class Tableless < ActiveRecord::Base
def self.columns() #columns ||= []; end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
attr_accessor :id, :name, :value
has_many :stuff_things
has_many :things, :through => :stuff_things
end

Related

Postgresql JSONB nested form 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.

What is the best method to create a copy of an item in Ruby? [duplicate]

I want to make a copy of an ActiveRecord object, changing a single field in the process (in addition to the id). What is the simplest way to accomplish this?
I realize I could create a new record, and then iterate over each of the fields copying the data field-by-field - but I figured there must be an easier way to do this.
Perhaps something like this:
new_record = Record.copy(:id)
To get a copy, use the dup (or clone for < rails 3.1+) method:
#rails >= 3.1
new_record = old_record.dup
# rails < 3.1
new_record = old_record.clone
Then you can change whichever fields you want.
ActiveRecord overrides the built-in Object#clone to give you a new (not saved to the DB) record with an unassigned ID.
Note that it does not copy associations, so you'll have to do this manually if you need to.
Rails 3.1 clone is a shallow copy, use dup instead...
Depending on your needs and programming style, you can also use a combination of the new method of the class and merge. For lack of a better simple example, suppose you have a task scheduled for a certain date and you want to duplicate it to another date. The actual attributes of the task aren't important, so:
old_task = Task.find(task_id)
new_task = Task.new(old_task.attributes.merge({:scheduled_on => some_new_date}))
will create a new task with :id => nil, :scheduled_on => some_new_date, and all other attributes the same as the original task. Using Task.new, you will have to explicitly call save, so if you want it saved automatically, change Task.new to Task.create.
Peace.
You may also like the Amoeba gem for ActiveRecord 3.2.
In your case, you probably want to make use of the nullify, regex or prefix options available in the configuration DSL.
It supports easy and automatic recursive duplication of has_one, has_many and has_and_belongs_to_many associations, field preprocessing and a highly flexible and powerful configuration DSL that can be applied both to the model and on the fly.
be sure to check out the Amoeba Documentation but usage is pretty easy...
just
gem install amoeba
or add
gem 'amoeba'
to your Gemfile
then add the amoeba block to your model and run the dup method as usual
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :tags
amoeba do
enable
end
end
class Comment < ActiveRecord::Base
belongs_to :post
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class PostsController < ActionController
def some_method
my_post = Post.find(params[:id])
new_post = my_post.dup
new_post.save
end
end
You can also control which fields get copied in numerous ways, but for example, if you wanted to prevent comments from being duplicated but you wanted to maintain the same tags, you could do something like this:
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :tags
amoeba do
exclude_field :comments
end
end
You can also preprocess fields to help indicate uniqueness with both prefixes and suffixes as well as regexes. In addition, there are also numerous options so you can write in the most readable style for your purpose:
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :tags
amoeba do
include_field :tags
prepend :title => "Copy of "
append :contents => " (copied version)"
regex :contents => {:replace => /dog/, :with => "cat"}
end
end
Recursive copying of associations is easy, just enable amoeba on child models as well
class Post < ActiveRecord::Base
has_many :comments
amoeba do
enable
end
end
class Comment < ActiveRecord::Base
belongs_to :post
has_many :ratings
amoeba do
enable
end
end
class Rating < ActiveRecord::Base
belongs_to :comment
end
The configuration DSL has yet more options, so be sure to check out the documentation.
Enjoy! :)
Use ActiveRecord::Base#dup if you don't want to copy the id
I usually just copy the attributes, changing whatever I need changing:
new_user = User.new(old_user.attributes.merge(:login => "newlogin"))
If you need a deep copy with associations, I recommend the deep_cloneable gem.
In Rails 5 you can simply create duplicate object or record like this.
new_user = old_user.dup
Here is a sample of overriding ActiveRecord #dup method to customize instance duplication and include relation duplication as well:
class Offer < ApplicationRecord
has_many :offer_items
def dup
super.tap do |new_offer|
# change title of the new instance
new_offer.title = "Copy of #{#offer.title}"
# duplicate offer_items as well
self.offer_items.each { |offer_item| new_offer.offer_items << offer_item.dup }
end
end
end
Note: this method doesn't require any external gem but it requires newer ActiveRecord version with #dup method implemented
The easily way is:
#your rails >= 3.1 (i was done it with Rails 5.0.0.1)
o = Model.find(id)
# (Range).each do |item|
(1..109).each do |item|
new_record = o.dup
new_record.save
end
Or
# if your rails < 3.1
o = Model.find(id)
(1..109).each do |item|
new_record = o.clone
new_record.save
end
You can also check the acts_as_inheritable gem.
"Acts As Inheritable is a Ruby Gem specifically written for Rails/ActiveRecord models. It is meant to be used with the Self-Referential Association, or with a model having a parent that share the inheritable attributes. This will let you inherit any attribute or relation from the parent model."
By adding acts_as_inheritable to your models you will have access to these methods:
inherit_attributes
class Person < ActiveRecord::Base
acts_as_inheritable attributes: %w(favorite_color last_name soccer_team)
# Associations
belongs_to :parent, class_name: 'Person'
has_many :children, class_name: 'Person', foreign_key: :parent_id
end
parent = Person.create(last_name: 'Arango', soccer_team: 'Verdolaga', favorite_color:'Green')
son = Person.create(parent: parent)
son.inherit_attributes
son.last_name # => Arango
son.soccer_team # => Verdolaga
son.favorite_color # => Green
inherit_relations
class Person < ActiveRecord::Base
acts_as_inheritable associations: %w(pet)
# Associations
has_one :pet
end
parent = Person.create(last_name: 'Arango')
parent_pet = Pet.create(person: parent, name: 'Mango', breed:'Golden Retriver')
parent_pet.inspect #=> #<Pet id: 1, person_id: 1, name: "Mango", breed: "Golden Retriver">
son = Person.create(parent: parent)
son.inherit_relations
son.pet.inspect # => #<Pet id: 2, person_id: 2, name: "Mango", breed: "Golden Retriver">
Hope this can help you.
Since there could be more logic, when duplicating a model, I would suggest to create a new class, where you handle all the needed logic.
To ease that, there's a gem that can help: clowne
As per their documentation examples, for a User model:
class User < ActiveRecord::Base
# create_table :users do |t|
# t.string :login
# t.string :email
# t.timestamps null: false
# end
has_one :profile
has_many :posts
end
You create your cloner class:
class UserCloner < Clowne::Cloner
adapter :active_record
include_association :profile, clone_with: SpecialProfileCloner
include_association :posts
nullify :login
# params here is an arbitrary Hash passed into cloner
finalize do |_source, record, params|
record.email = params[:email]
end
end
class SpecialProfileCloner < Clowne::Cloner
adapter :active_record
nullify :name
end
and then use it:
user = User.last
#=> <#User(login: 'clown', email: 'clown#circus.example.com')>
cloned = UserCloner.call(user, email: 'fake#example.com')
cloned.persisted?
# => false
cloned.save!
cloned.login
# => nil
cloned.email
# => "fake#example.com"
# associations:
cloned.posts.count == user.posts.count
# => true
cloned.profile.name
# => nil
Example copied from the project, but it will give a clear vision of what you can achieve.
For a quick and simple record I would go with:
Model.new(Model.last.attributes.reject {|k,_v| k.to_s == 'id'}
Try rails's dup method:
new_record = old_record.dup.save

STI with many types

I have a rails 4 app with STI models:
# models/person.rb
def Person < ActiveRecord::Base
end
# models/director.rb
def Director < Person
end
# models/actor.rb
def Director < Person
end
But because one person can be an actor and an director simultaneously, I want STI with many types like:
person = Person.first
person.type = "Director, Actor"
person.save
Actor.first.id => 1
Director.first.id => 1
Is there mechanism in rails or gem for realize this?
Rails does not support this and I'm not aware of any gems that support this as described (i.e. multiple subclass names in the type column).
There is gem at https://github.com/mhuggins/multiple_table_inheritance which uses separate tables for the subclasses and you can always use mixins as an alternative to inheritance.
I believe the more Rails idiomatic way to do something similar would be via scopes, which would allow you to do:
person = Person.first
person.position = 'Director, Actor'
person.save
person.directors.first.id => 1
person.actors.first.id => 1
And you would just have to define a pair of scopes in your Person class:
scope :actors, -> { where('position like ?', '%Actor%') }
scope :directors, -> { where('position like ?', '%Director%') }
You would lose the ability to do person.is_a? with this, but Ruby doesn't really do multiple inheritance in such a way as to allow #is_a? to return true when passed sibling classes anyway. You can also get effectively similar functionality with a simple test method:
def is_actor?
self.position =~ /Actor/
end
def is_director?
self.position =~ /Director/
end
EDIT: I haven't done a lot of Rails 4, so my scope syntax MAY not be right, I just glanced at the docs. The principle should be sound, though.
Thank to all answerers above!
I found solution that most appropriate for me:
I've created hmt association Person-ProfessionsPerson-Profession and leave descendants for Person class (Director and Actor).
# models/profession.rb
Profession < ActiveRecord::Base
has_many :professions_people, dependent: :destroy
has_many :people, through: :professions_people
end
# models/person.rb
def Person < ActiveRecord::Base
has_many :professions_people, dependent: :destroy
has_many :professions, through: :professions_people
end
# models/director.rb
def Director < Person
include PeopleFromProfession
end
# models/actor.rb
def Actor < Person
include PeopleFromProfession
end
I've seed 2 professions with column "class_type" (which should not change in app's work) "Actor" and "Director"
I've also add concern PeopleFromProfession for share some code:
# models/concerns/actor.rb
module PeopleFromProfession
extend ActiveSupport::Concern
included do
default_scope { includes(:professions).where(professions: {class_type: self.name}) }
after_create :create_join_table_record
end
module ClassMethods
def model_name
Person.model_name
end
end
private
def create_join_table_record
self.professions << Profession.where(class_type: self.class.name).first
end
end
default_scope is for scoping only people with specific profession, create_join_table_record callback is monkey-patch for create missed join table record.
Class method model_name was overwriting for purposes, that covered here Best practices to handle routes for STI subclasses in rails
If you will find some problems in that approach, please tell me.

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

What is the easiest way to duplicate an activerecord record?

I want to make a copy of an ActiveRecord object, changing a single field in the process (in addition to the id). What is the simplest way to accomplish this?
I realize I could create a new record, and then iterate over each of the fields copying the data field-by-field - but I figured there must be an easier way to do this.
Perhaps something like this:
new_record = Record.copy(:id)
To get a copy, use the dup (or clone for < rails 3.1+) method:
#rails >= 3.1
new_record = old_record.dup
# rails < 3.1
new_record = old_record.clone
Then you can change whichever fields you want.
ActiveRecord overrides the built-in Object#clone to give you a new (not saved to the DB) record with an unassigned ID.
Note that it does not copy associations, so you'll have to do this manually if you need to.
Rails 3.1 clone is a shallow copy, use dup instead...
Depending on your needs and programming style, you can also use a combination of the new method of the class and merge. For lack of a better simple example, suppose you have a task scheduled for a certain date and you want to duplicate it to another date. The actual attributes of the task aren't important, so:
old_task = Task.find(task_id)
new_task = Task.new(old_task.attributes.merge({:scheduled_on => some_new_date}))
will create a new task with :id => nil, :scheduled_on => some_new_date, and all other attributes the same as the original task. Using Task.new, you will have to explicitly call save, so if you want it saved automatically, change Task.new to Task.create.
Peace.
You may also like the Amoeba gem for ActiveRecord 3.2.
In your case, you probably want to make use of the nullify, regex or prefix options available in the configuration DSL.
It supports easy and automatic recursive duplication of has_one, has_many and has_and_belongs_to_many associations, field preprocessing and a highly flexible and powerful configuration DSL that can be applied both to the model and on the fly.
be sure to check out the Amoeba Documentation but usage is pretty easy...
just
gem install amoeba
or add
gem 'amoeba'
to your Gemfile
then add the amoeba block to your model and run the dup method as usual
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :tags
amoeba do
enable
end
end
class Comment < ActiveRecord::Base
belongs_to :post
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
end
class PostsController < ActionController
def some_method
my_post = Post.find(params[:id])
new_post = my_post.dup
new_post.save
end
end
You can also control which fields get copied in numerous ways, but for example, if you wanted to prevent comments from being duplicated but you wanted to maintain the same tags, you could do something like this:
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :tags
amoeba do
exclude_field :comments
end
end
You can also preprocess fields to help indicate uniqueness with both prefixes and suffixes as well as regexes. In addition, there are also numerous options so you can write in the most readable style for your purpose:
class Post < ActiveRecord::Base
has_many :comments
has_and_belongs_to_many :tags
amoeba do
include_field :tags
prepend :title => "Copy of "
append :contents => " (copied version)"
regex :contents => {:replace => /dog/, :with => "cat"}
end
end
Recursive copying of associations is easy, just enable amoeba on child models as well
class Post < ActiveRecord::Base
has_many :comments
amoeba do
enable
end
end
class Comment < ActiveRecord::Base
belongs_to :post
has_many :ratings
amoeba do
enable
end
end
class Rating < ActiveRecord::Base
belongs_to :comment
end
The configuration DSL has yet more options, so be sure to check out the documentation.
Enjoy! :)
Use ActiveRecord::Base#dup if you don't want to copy the id
I usually just copy the attributes, changing whatever I need changing:
new_user = User.new(old_user.attributes.merge(:login => "newlogin"))
If you need a deep copy with associations, I recommend the deep_cloneable gem.
In Rails 5 you can simply create duplicate object or record like this.
new_user = old_user.dup
Here is a sample of overriding ActiveRecord #dup method to customize instance duplication and include relation duplication as well:
class Offer < ApplicationRecord
has_many :offer_items
def dup
super.tap do |new_offer|
# change title of the new instance
new_offer.title = "Copy of #{#offer.title}"
# duplicate offer_items as well
self.offer_items.each { |offer_item| new_offer.offer_items << offer_item.dup }
end
end
end
Note: this method doesn't require any external gem but it requires newer ActiveRecord version with #dup method implemented
The easily way is:
#your rails >= 3.1 (i was done it with Rails 5.0.0.1)
o = Model.find(id)
# (Range).each do |item|
(1..109).each do |item|
new_record = o.dup
new_record.save
end
Or
# if your rails < 3.1
o = Model.find(id)
(1..109).each do |item|
new_record = o.clone
new_record.save
end
You can also check the acts_as_inheritable gem.
"Acts As Inheritable is a Ruby Gem specifically written for Rails/ActiveRecord models. It is meant to be used with the Self-Referential Association, or with a model having a parent that share the inheritable attributes. This will let you inherit any attribute or relation from the parent model."
By adding acts_as_inheritable to your models you will have access to these methods:
inherit_attributes
class Person < ActiveRecord::Base
acts_as_inheritable attributes: %w(favorite_color last_name soccer_team)
# Associations
belongs_to :parent, class_name: 'Person'
has_many :children, class_name: 'Person', foreign_key: :parent_id
end
parent = Person.create(last_name: 'Arango', soccer_team: 'Verdolaga', favorite_color:'Green')
son = Person.create(parent: parent)
son.inherit_attributes
son.last_name # => Arango
son.soccer_team # => Verdolaga
son.favorite_color # => Green
inherit_relations
class Person < ActiveRecord::Base
acts_as_inheritable associations: %w(pet)
# Associations
has_one :pet
end
parent = Person.create(last_name: 'Arango')
parent_pet = Pet.create(person: parent, name: 'Mango', breed:'Golden Retriver')
parent_pet.inspect #=> #<Pet id: 1, person_id: 1, name: "Mango", breed: "Golden Retriver">
son = Person.create(parent: parent)
son.inherit_relations
son.pet.inspect # => #<Pet id: 2, person_id: 2, name: "Mango", breed: "Golden Retriver">
Hope this can help you.
Since there could be more logic, when duplicating a model, I would suggest to create a new class, where you handle all the needed logic.
To ease that, there's a gem that can help: clowne
As per their documentation examples, for a User model:
class User < ActiveRecord::Base
# create_table :users do |t|
# t.string :login
# t.string :email
# t.timestamps null: false
# end
has_one :profile
has_many :posts
end
You create your cloner class:
class UserCloner < Clowne::Cloner
adapter :active_record
include_association :profile, clone_with: SpecialProfileCloner
include_association :posts
nullify :login
# params here is an arbitrary Hash passed into cloner
finalize do |_source, record, params|
record.email = params[:email]
end
end
class SpecialProfileCloner < Clowne::Cloner
adapter :active_record
nullify :name
end
and then use it:
user = User.last
#=> <#User(login: 'clown', email: 'clown#circus.example.com')>
cloned = UserCloner.call(user, email: 'fake#example.com')
cloned.persisted?
# => false
cloned.save!
cloned.login
# => nil
cloned.email
# => "fake#example.com"
# associations:
cloned.posts.count == user.posts.count
# => true
cloned.profile.name
# => nil
Example copied from the project, but it will give a clear vision of what you can achieve.
For a quick and simple record I would go with:
Model.new(Model.last.attributes.reject {|k,_v| k.to_s == 'id'}
Try rails's dup method:
new_record = old_record.dup.save

Resources