Saving a nested hash in Ruby on Rails - ruby-on-rails

I'm trying to save a nested Hash to my database and retrieve it, but nested values are lost upon retrieval.
My model looks like this:
class User
serialize :metadata, MetaData
end
The class MetaData looks like this:
class MetaData < Hash
attr_accessor :availability, :validated
end
The code I'm using to store data looks something like this (the real data is coming from a HTML form, though):
user = User.find(id)
user.metadata.validated = true
user.metadata.availability = {'Sunday' => 'Yes', 'Monday' => 'No', 'Tuesday' => 'Yes'}
user.save
When I look at the data in the database, I see the following:
--- !map:MetaData
availability: !map:ActiveSupport::HashWithIndifferentAccess
Sunday: "Yes"
Monday: "No"
Tuesday: "Yes"
validated: true
The problem occurs when I try to get the object again:
user = User.find(id)
user.metadata.validated # <- this is true
user.metadata.availability # <- this is nil
Any ideas? I'm using Rails 3.1 with Postgresql as my datastore.

If you look in the database you see "map:ActiveSupport::HashWithIndifferentAccess" for availability?
My approach would be to separate out the single instance of availablity from the hash collection structure of days available.

you mean user.metadata.validated # <- this is true ?
What DB columns are metadata and availability stored as? They need to be TEXT

Related

Save a hash to a parameter?

I would like to save the params from the form submitted into a hash called: hash_options which corresponds to a field in my table.
How do I store the hash of: hash_options as the value for: hash_fields ?
hash_fields is a text field and I am trying to store hash_options in this row as a plain hash.
def person_params
hash_options = {age: params['person']['age'], location: params['person']['location'], gender: params['person']['gender']}
params.require(:person).permit(:first_name, :last_name, :owner, hash_fields: [hash_options])
end
Side question: How does the model access and store the params?
In order to be able to save the hash as is to the database you have a couple of options:
If you are using MongoDB(mongoid) you can just define it as a hash type field in your model with:
field options_hash, type: Hash
You can use postgresql HStore to save schemaless data into that column which enables you to save the hash into the column and work with it as a Hash.
As for the params part there is no need to include it in the permit call, you can just do:
model = Model.new(person_params) do |m|
m.options_hash = hash_options
end
model.save

Rails: use existing model validation rules against a collection instead of the database table

Rails 4, Mongoid instead of ActiveRecord (but this should change anything for the sake of the question).
Let's say I have a MyModel domain class with some validation rules:
class MyModel
include Mongoid::Document
field :text, type: String
field :type, type: String
belongs_to :parent
validates :text, presence: true
validates :type, inclusion: %w(A B C)
validates_uniqueness_of :text, scope: :parent # important validation rule for the purpose of the question
end
where Parent is another domain class:
class Parent
include Mongoid::Document
field :name, type: String
has_many my_models
end
Also I have the related tables in the database populated with some valid data.
Now, I want to import some data from an CSV file, which can conflict with the existing data in the database. The easy thing to do is to create an instance of MyModel for every row in the CSV and verify if it's valid, then save it to the database (or discard it).
Something like this:
csv_rows.each |data| # simplified
my_model = MyModel.new(data) # data is the hash with the values taken from the CSV row
if my_model.valid?
my_model.save validate: false
else
# do something useful, but not interesting for the question's purpose
# just know that I need to separate validation from saving
end
end
Now, this works pretty smoothly for a limited amount of data. But when the CSV contains hundreds of thousands of rows, this gets quite slow, because (worst case) there's a write operation for every row.
What I'd like to do, is to store the list of valid items and save them all at the end of the file parsing process. So, nothing complicated:
valids = []
csv_rows.each |data|
my_model = MyModel.new(data)
if my_model.valid? # THE INTERESTING LINE this "if" checks only against the database, what happens if it conflicts with some other my_models not saved yet?
valids << my_model
else
# ...
end
end
if valids.size > 0
# bulk insert of all data
end
That would be perfect, if I could be sure that the data in the CSV does not contain duplicated rows or data that goes against the validation rules of MyModel.
My question is: how can I check each row against the database AND the valids array, without having to repeat the validation rules defined into MyModel (avoiding to have them duplicated)?
Is there a different (more efficient) approach I'm not considering?
What you can do is validate as model, save the attributes in a hash, pushed to the valids array, then do a bulk insert of the values usint mongodb's insert:
valids = []
csv_rows.each |data|
my_model = MyModel.new(data)
if my_model.valid?
valids << my_model.attributes
end
end
MyModel.collection.insert(valids, continue_on_error: true)
This won't however prevent NEW duplicates... for that you could do something like the following, using a hash and compound key:
valids = {}
csv_rows.each |data|
my_model = MyModel.new(data)
if my_model.valid?
valids["#{my_model.text}_#{my_model.parent}"] = my_model.as_document
end
end
Then either of the following will work, DB Agnostic:
MyModel.create(valids.values)
Or MongoDB'ish:
MyModel.collection.insert(valids.values, continue_on_error: true)
OR EVEN BETTER
Ensure you have a uniq index on the collection:
class MyModel
...
index({ text: 1, parent: 1 }, { unique: true, dropDups: true })
...
end
Then Just do the following:
MyModel.collection.insert(csv_rows, continue_on_error: true)
http://api.mongodb.org/ruby/current/Mongo/Collection.html#insert-instance_method
http://mongoid.org/en/mongoid/docs/indexing.html
TIP: I recommend if you anticipate thousands of rows to do this in batches of 500 or so.

Updating Attribute through reference

I am wondering if it's possible to reference an object's attribute.
The object User have attribute first_name, so normally if we want to update the first name attribute we do:
user0 = User.new()
user0.update_attribute(:first_name, "joe")
now my question is can I update the :first_name attribute through another variable/symbol like so:
user0.update_attribute(:fname_ref, "jack")
user0.first_name #=> has the value jack
I was looking for variable reference like in Perl, but came up nothing in Ruby.
---------- EDIT
I am in the middle of doing the lynda ruby on rails tutorial, and in the middle of creating a module to adjust positions of items in a table.
unfortunately when I first started I named my tables columns differently
pages.page_position, subjects.subject_position, sections.section_position
now the module PositionMover is to be used accross three models, so now
I have a problem since the attributes names are different for each model
so I thought no worry I'll just create a pointer / refference for each model
:position = :pages_position , :position = :subjects_position , :position = :section_position
hence the question , if its even possible to do it.
if its not possible , any suggestion what should I do , so the module can
be used accross three different models , with different attribute names.
sorry I am a newbie.
Symbols are not like variables, they are actually a object type, like String or Fixnum so you can have a variable that is of type symbol.
I think this is what you are looking for:
attribute = :first_name
user0.update_attribute(attribute, "jack")
user0.first_name #=> has the value jack
Update: If you have a String and need to convert to a symbol, (I'm not sure if you need this for update_attribute)
foo = "string"
foo.to_sym #=> :string
Use the alias_attribute . Define into each model like :
Page model
alias_attribute :position , :pages_position
Subject Model
alias_attribute :position , :subjects_position
Section Model
alias_attribute :position , :section_position
And use (Model.position = values) with each model . Hope Its solution of your problem .
You can also use send docs and use symbols or strings to reference the attribute's methods. send can be incredibly useful since it enables you to choose the method that you'll be invoking at runtime.
Eg
user.first_name = "Jack" # set first_name to Jack
# Note: method "first_name=" (a settor) is being used
attribute = "first_name"
user.send attribute + "=", "Jack" # set first_name to Jack
# The string "first_name=" is coerced into
# a symbol
attribute = :first_name
val = user.send attribute # => returns "Jack"
see the definition of update_attribute in the ActiveRecord::Persistence module on github:
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
send("#{name}=", value)
save(:validate => false)
end
this leads me to believe you could add the following to your model to achieve that behavior:
alias_method :fname_ref= :first_name=
I'd be interested to know why you want to do that as #Andrew Marshall asked.

searching a model in rails for 2 values?

I wrote this retrieval statement to check if an appointment being saved or created dosent conflict with one thats already saved. but its not working, can someone please point me to where I'm going wrong?
#new_appointment = :appointment #which is the params of appointment being sent back from submit.
#appointments = Appointment.all(:conditions => { :date_of_appointment => #new_appointment.date_of_appointment, :trainer_id => #new_appointment.trainer_id}
)
the error is from the :date_of_appointment => #new_appointment.date_of_appointment this will always be false as:
thank you
At face value, there doesn't appear to be anything wrong with your syntax. My guess is that #new_appointment isn't containing the values you're expecting, and thus the database query is returning different values than you expect.
Try dumping out #new_appointment.inspect or check the logfiles to see what SQL the finder is producing, or use
Appointment.send(:construct_finder_sql, :conditions => {
:date_of_appointment => #new_appointment.date_of_appointment,
:trainer_id => #new_appointment.trainer_id
})
to see the SQL that will be generated (construct_finder_sql is a protected ActiveRecord::Base method).
Update based on your edit
#new_appointment = :appointment should be something like #new_appointment = Appointment.new(params[:appointment]). :appointment is just a symbol, it is not automatically related to your params unless you tell it to.

Overriding id on create in ActiveRecord

Is there any way of overriding a model's id value on create? Something like:
Post.create(:id => 10, :title => 'Test')
would be ideal, but obviously won't work.
id is just attr_protected, which is why you can't use mass-assignment to set it. However, when setting it manually, it just works:
o = SomeObject.new
o.id = 8888
o.save!
o.reload.id # => 8888
I'm not sure what the original motivation was, but I do this when converting ActiveHash models to ActiveRecord. ActiveHash allows you to use the same belongs_to semantics in ActiveRecord, but instead of having a migration and creating a table, and incurring the overhead of the database on every call, you just store your data in yml files. The foreign keys in the database reference the in-memory ids in the yml.
ActiveHash is great for picklists and small tables that change infrequently and only change by developers. So when going from ActiveHash to ActiveRecord, it's easiest to just keep all of the foreign key references the same.
You could also use something like this:
Post.create({:id => 10, :title => 'Test'}, :without_protection => true)
Although as stated in the docs, this will bypass mass-assignment security.
Try
a_post = Post.new do |p|
p.id = 10
p.title = 'Test'
p.save
end
that should give you what you're looking for.
For Rails 4:
Post.create(:title => 'Test').update_column(:id, 10)
Other Rails 4 answers did not work for me. Many of them appeared to change when checking using the Rails Console, but when I checked the values in MySQL database, they remained unchanged. Other answers only worked sometimes.
For MySQL at least, assigning an id below the auto increment id number does not work unless you use update_column. For example,
p = Post.create(:title => 'Test')
p.id
=> 20 # 20 was the id the auto increment gave it
p2 = Post.create(:id => 40, :title => 'Test')
p2.id
=> 40 # 40 > the next auto increment id (21) so allow it
p3 = Post.create(:id => 10, :title => 'Test')
p3.id
=> 10 # Go check your database, it may say 41.
# Assigning an id to a number below the next auto generated id will not update the db
If you change create to use new + save you will still have this problem. Manually changing the id like p.id = 10 also produces this problem.
In general, I would use update_column to change the id even though it costs an extra database query because it will work all the time. This is an error that might not show up in your development environment, but can quietly corrupt your production database all the while saying it is working.
we can override attributes_protected_by_default
class Example < ActiveRecord::Base
def self.attributes_protected_by_default
# default is ["id", "type"]
["type"]
end
end
e = Example.new(:id => 10000)
Actually, it turns out that doing the following works:
p = Post.new(:id => 10, :title => 'Test')
p.save(false)
As Jeff points out, id behaves as if is attr_protected. To prevent that, you need to override the list of default protected attributes. Be careful doing this anywhere that attribute information can come from the outside. The id field is default protected for a reason.
class Post < ActiveRecord::Base
private
def attributes_protected_by_default
[]
end
end
(Tested with ActiveRecord 2.3.5)
Post.create!(:title => "Test") { |t| t.id = 10 }
This doesn't strike me as the sort of thing that you would normally want to do, but it works quite well if you need to populate a table with a fixed set of ids (for example when creating defaults using a rake task) and you want to override auto-incrementing (so that each time you run the task the table is populate with the same ids):
post_types.each_with_index do |post_type|
PostType.create!(:name => post_type) { |t| t.id = i + 1 }
end
Put this create_with_id function at the top of your seeds.rb and then use it to do your object creation where explicit ids are desired.
def create_with_id(clazz, params)
obj = clazz.send(:new, params)
obj.id = params[:id]
obj.save!
obj
end
and use it like this
create_with_id( Foo, {id:1,name:"My Foo",prop:"My other property"})
instead of using
Foo.create({id:1,name:"My Foo",prop:"My other property"})
This case is a similar issue that was necessary overwrite the id with a kind of custom date :
# in app/models/calendar_block_group.rb
class CalendarBlockGroup < ActiveRecord::Base
...
before_validation :parse_id
def parse_id
self.id = self.date.strftime('%d%m%Y')
end
...
end
And then :
CalendarBlockGroup.create!(:date => Date.today)
# => #<CalendarBlockGroup id: 27072014, date: "2014-07-27", created_at: "2014-07-27 20:41:49", updated_at: "2014-07-27 20:41:49">
Callbacks works fine.
Good Luck!.
For Rails 3, the simplest way to do this is to use new with the without_protection refinement, and then save:
Post.new({:id => 10, :title => 'Test'}, :without_protection => true).save
For seed data, it may make sense to bypass validation which you can do like this:
Post.new({:id => 10, :title => 'Test'}, :without_protection => true).save(validate: false)
We've actually added a helper method to ActiveRecord::Base that is declared immediately prior to executing seed files:
class ActiveRecord::Base
def self.seed_create(attributes)
new(attributes, without_protection: true).save(validate: false)
end
end
And now:
Post.seed_create(:id => 10, :title => 'Test')
For Rails 4, you should be using StrongParams instead of protected attributes. If this is the case, you'll simply be able to assign and save without passing any flags to new:
Post.new(id: 10, title: 'Test').save # optionally pass `{validate: false}`
In Rails 4.2.1 with Postgresql 9.5.3, Post.create(:id => 10, :title => 'Test') works as long as there isn't a row with id = 10 already.
you can insert id by sql:
arr = record_line.strip.split(",")
sql = "insert into records(id, created_at, updated_at, count, type_id, cycle, date) values(#{arr[0]},#{arr[1]},#{arr[2]},#{arr[3]},#{arr[4]},#{arr[5]},#{arr[6]})"
ActiveRecord::Base.connection.execute sql

Resources