What methods are used to set attributes when data is pulled from a table in Ruby ActiveRecord? - ruby

I'm using ActiveRecord 4.1.8 in a Ruby (not Rails) application. I have a table and a corresponding model that looks like the following:
create_table 'people', :force => true do |t|
t.string 'name'
end
class Person < ActiveRecord::Base
def name=(name)
puts "Attribute setter for name called with #{name}"
write_attribute(:name, name)
end
end
When I create a new instance of Person, I see the Attribute setter for name called with... written to STDOUT. However, when I reload the model instance, I do not see the message written to STDOUT.
p = Person.create(name: 'foobar')
--> Attribute setter for name called with foobar
p.reload
--> <nothing>
The model is getting persisted to the database, so this makes me think name= isn't used when data is loaded into a model from the database. I need to modify certain data attributes when they're read in from the database, so does anyone know what other method I need to override?

From the active_record/persistence.rb source:
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
fresh_object =
if options && options[:lock]
self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
#attributes = fresh_object.instance_variable_get('#attributes')
#new_record = false
self
end
It just replaces the attributes hash directly. Seems like the easiest way to handle this is to override reload and patch things up after its called.

Related

ruby - custom validation is not called

I have two classes; customer and reservation. And my project is consist of only ruby code, not rails project.
Class reservation reads bulk json file line by line which includes customer hash.
From that hash, I create customer object.
Here's the code within the reservation class.
def parse_json
File.open(#filename, "r" ).each do |line|
#customers << Customer.new(JSON.parse(line))
end
return #customers
end
And in customer.rb I have the following;
validates_presence_of :hash
validate :hash_should_include_all_fields
attr_reader :name, :userid, :latitude, :longitude, :distance
def hash_should_include_all_fields
puts "I'm here #{hash}"
if hash.assert_valid_keys('user_id', 'name', 'latitude', 'longitude')
puts "Valid"
else
puts "Not valid"
end
end
However as I create customer objects, hash_should_include_all_fields method is not called.
What do I miss in here, that would be great if you can help.
Thanks
You're calling new which will build a Customer object but do nothing more.
Calling create! or create will also run validations and write the Object to the database if it's an active_record model.
If you don't want to write it to the databas you can call valid? on the instance created with new

Serialize a class into an activerecord field

I've tried a few different searches, but I'm not really sure how to word the question correctly. Imagine I have a db table create migration that looks like this:
def self.up
create_table :users do |t|
t.string :name, :null => false,
t.text :statistics
end
end
Example users class
class User
serialize :statistics, JSON
def statistics
self.statistics
end
end
But I want self.statistics to be an instance of class Statistics
class Statistics
#stats = {}
def set(statistic, value)
#stats[statistic] = value
end
def to_json(options)
self.to_json(:only => #stats)
end
end
(or something like that)
Ultimately, what I want to happen is that I want to be able to add Statistics-specific methods to manage the data in that field, but when I save the User object, I want it to convert that instance of the Statistics class into a JSON string, which gets saved in the DB.
I'm coming from a PHP background, where this kind of thing is pretty easy, but I'm having a hard time figuring out how to make this work with ActiveRecord (something I don't have much experience with).
Ideas?
Note: This is NOT Rails... just straight-up Ruby
Do you need it to be JSON? If not, serializing it to YAML will allow you to easily marshal the object back to its original class.
ActiveRecord::Base#serialize takes two arguments: attribute and class. If class is defined, the data must deserialize into an object of the type specified. Here's a cool example of it in action.
By default, your attribute will be serialized as YAML. Let’s say you
define MyField like this:
class MyField
end
It will be serialized like this:
--- !ruby/object:MyField {}
It will deserialize back to a MyField instance.

Freeze a property in DataMapper Model

Consider I have this following model definition, I want a particular property which should be constant from the moment it has been created
class A
property :a1, String, :freeze => true
end
Is there something like this? or may be using callbacks ?
Try the following:
class YourModel
property :a1, String
def a1=(other)
if a1
raise "A1 is allready bound to a value"
end
attribute_set(:a1, other.dup.freeze)
end
end
The initializer internally delegates to normal attribute writers, so when you initialize the attribute via YourModel.new(:a1 => "Your value") you cannot change it with your_instance.a1 = "your value".. But when you create a fresh instance. instance = YourModel.new you can assign once instance.a1 = "Your Value".
If you don't need to assign the constant, then
property :a1, String, :writer => :private
before :create do
attribute_set :a1, 'some value available at creation time'
end
may suffice

Generating JSON for Sinatra

I'm having an issue with passing the generated JSON notation of my object to my Sinatra application. The problem I have is twofold:
I have 2 classes that are mapped to a database using the Sequel gem. When they generate JSON it is ok and properly implemented.
I have a custom class called registration that maps one of the classes with an additional field. The goal is to generate JSON out of this and pass that JSON to the application using cucumber (test purpose)
The application code responsible for handling the request has the following function defined:
post '/users' do
begin
hash = JSON.parse(self.request.body.read)
registration = Registration.new.from_json(#request.body.read)
registration.user.country = Database::Alaplaya.get_country_by_iso_code(registration.user.country.iso_code)
return 400 unless(registration.is_valid?)
id = Database::Alaplaya.create_user(registration.user)
# If the registration failed in our system, return a page 400.
return 400 if id < 1
end
problem 1: I cannot use the params hash. It exists but is just an empty hash. Why?
problem 2: I cannot deserialize the JSON generated by the class itself. Why?
The registration class looks like this:
require 'json'
class Registration
attr_accessor :user, :project_id
def to_json(*a)
{
'json_class' => self.class.name,
'data' => [#user.to_json(*a), #project_id]
}.to_json(*a)
end
def self.json_create(o)
new(*o['data'])
end
# Creates a new instance of the class using the information provided in the
# hash. If a field is missing in the hash, nil will be assigned to that field
# instead.
def initialize(params = {})
#user = params[:user]
#project_id = params[:project_id]
end
# Returns a string representing the entire Registration.
def inspect
"#{#user.inspect} - #{#user.country.inspect} - #{#project_id}"
end
# Returns a boolean valid representing whether the Registration instance is
# considered valid for the API or not. True if the instance is considered
# valid; otherwise false.
def is_valid?
return false if #user.nil? || #project_id.nil?
return false if !#user.is_a?(User) || !#project_id.is_a?(Fixnum)
return false if !#user.is_valid?
true
end
end
I had to implement the methods to generate the JSON output correctly. When I run this in console I get the following output generated:
irb(main):004:0> r = Registration.new(:user => u, :project_id => 1)
=> new_login - nil - 1
irb(main):005:0> r.to_json
=> "{\"json_class\":\"Registration\",\"data\":[\"{\\\"json_class\\\":\\\"User\\\
",\\\"login\\\":\\\"new_login\\\"}\",1]}"
Which looks like valid JSON to me. However when I POST this to the application server and try to parse this, JSON complains that at least 2 octets are needed and refuses to deserialize the object.
If you're using Sequel as your ORM, try something like this:
In your model:
class Registration < Sequel::Model
many_to_one :user
many_to_one :project
plugin :json_serializer
end
The server:
before do
#data = JSON.parse(request.body.read) rescue {}
end
post '/users' do
#registration = Registration.new #data
if #registration.valid?
#registration.save
#registration.to_json #return a JSON representation of the resource
else
status 422 #proper status code for invalid input
#registration.errors.to_json
end
end
I think you may be overcomplicating your registration process. If the HTTP action is POST /users then why not create a user? Seems like creating a registration is overly complex. Unless your user already exists, in which case POST /users would be incorrect. If what you're really intending to do is add a user to to a project, then you should PUT /projects/:project_id/users/:user_id and the action would look something like this:
class User < Sequel::Model
many_to_many :projects
end
class Project < Sequel::Model
many_to_many :users
end
#make sure your db schema has a table called users_projects or projects_users
put '/projects/:project_id/users/:user_id' do
#find the project
#project = Project.find params[:project_id]
raise Sinatra::NotFound unless #project
#find the user
#user = Project.find params[:project_id]
raise Sinatra::NotFound unless #user
#add user to project's users collection
#project.add_user #user
#send a new representation of the parent resource back to the client
#i like to include the child resources as well
#json might look something like this
#{ 'name' : 'a project name', 'users' : ['/users/:user_id', '/users/:another_user_id'] }
#project.to_json
end

Active Record to_json\as_json on Array of Models

First off, I am not using Rails. I am using Sinatra for this project with Active Record.
I want to be able to override either to_json or as_json on my Model class and have it define some 'default' options. For example I have the following:
class Vendor < ActiveRecord::Base
def to_json(options = {})
if options.empty?
super :only => [:id, :name]
else
super options
end
end
end
where Vendor has more attributes than just id and name. In my route I have something like the following:
#vendors = Vendor.where({})
#vendors.to_json
Here #vendors is an Array vendor objects (obviously). The returned json is, however, not invoking my to_json method and is returning all of the models attributes.
I don't really have the option of modifying the route because I am actually using a modified sinatra-rest gem (http://github.com/mikeycgto/sinatra-rest).
Any ideas on how to achieve this functionality? I could do something like the following in my sinatra-rest gem but this seems silly:
#PLURAL.collect! { |obj| obj.to_json }
Try overriding serializable_hash intead:
def serializable_hash(options = nil)
{ :id => id, :name => name }
end
More information here.
If you override as_json instead of to_json, each element in the array will format with as_json before the array is converted to JSON
I'm using the following to only expose only accessible attributes:
def as_json(options = {})
options[:only] ||= self.class.accessible_attributes.to_a
super(options)
end

Resources