RoR - different object_id inside after_initialize callback after obj.reload - mysql

I've written a method for my project which extends ActiveRecord models behaviour, I've stripped out most of it, consider the following code:
class ActiveRecord::Base
def self.has_translations
after_initialize :clear_translations_cache
def clear_translations_cache
binding.pry
#_translations = {}
end
end
end
Basically, I want the #_translations instance variable to get cleared when I .reload the instance from the database, but for some reason, after fetching an existing object from the database, executing a method which populates #_translations, and then executing object.reload, #_translations still contains the same data.
I know for sure that the callback gets executed when first fetching the object from database and when calling .reload. I used binding.pry to halt execution inside the callback method, but for some reason self.object_id inside .reload has a different object_id than my original object, and therefore #_translations in the original object doesn't get cleared.
Attached is the console output:
1.9.3p194 :008 > s = TranslatedItem.first
76: def clear_translations_cache
=> 77: #_translations = {}
78: end
[1] pry(#<TranslatedItem>)> self.class
=> TranslatedItem(id: integer, created_at: datetime, updated_at: datetime)
[2] pry(#<TranslatedItem>)> self.object_id
=> 70254243993580
[3] pry(#<TranslatedItem>)> exit
1.9.3p194 :009 > s.object_id
=> 70254243993580
1.9.3p194 :010 > s.reload
76: def clear_translations_cache
=> 77: #_translations = {}
78: end
[1] pry(#<ServiceLevel>)> self.class
=> TranslatedItem(id: integer, created_at: datetime, updated_at: datetime)
[2] pry(#<TranslatedItem>)> self.object_id
=> 70254259259120

I'm guessing the behavior you're seeing is related to how ActiveRecord reload works:
fresh_object = self.class.unscoped { self.class.find(self.id, options) }
#attributes.update(fresh_object.instance_variable_get('#attributes'))
You'll notice that it is creating a fresh object by finding it from the database, which explains why you are seeing two different object_id values in your callback method. The object that is initialized during the reload is only used for its attributes and then goes out of scope.
It's not clear from your question whether you were just curious why it behaved this way or if you're looking for an alternative way to do it.
Update:
You've got a few options if you're just looking for a way to clear the instance variable when you reload the model.
1) Add your own reload method that you can explicitly call:
class ActiveRecord::Base
def reload_everything
reload
#_translations = {}
end
end
object.reload_everything
2) Change the behavior of reload
module ReloadTranslations
def reload(*args)
super
#_translations = {}
end
end
ActiveRecord::Base.send(:include, ReloadTranslations)

Related

can't create a record in a database

I am using rails version 4.2 and ruby version 2.2.0. I am trying to save a record to lollypops table. No exceptions indicating reasons.
TASK: As soon as a member is created and saved, I want to populate the lollypops table by calling the create_lollypop(#member.id) in members controller's create method like this:
# POST /members
# POST /members.json
def create
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
create_lollypop(#member.id) #From here I want to create lollypop
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
end
def create_lollypop(member_id)
#member = Member.find(member_id)
Lollypop.create(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name)
end
The 'member' is created but the 'lollypops' table is not populated. The associations are:
MEMBER model:
has_one :lollypop, :dependent=>:destroy
LOLLYPOP model
belongs_to :member
If I use generic SQL command then the lollypops table gets populated but I do not want to do that:
def self.create_lollypop(member_id)
member = Member.find(member_id)
ActiveRecord::Base.connection.execute("insert into lollypops (member_id,product_name,product_price,email,house_flat,street,city_town,country,postcode_index,name)
values(#{member.id},'lollypop',#{100},'#{member.email}','#{member.house_flat}','#{member.street}','#{member.city_town}','#{member.country_code}','#{member.postcode_index}','#{member.name}')")
end
Any advice would be welcomed. Thank you.
In your create_lollypop(), You are not defining #member.
def create_lollypop(member_id)
#member = Member.find member_id
Lollypop.create!(
:member_id=>#member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
end
Also use create! so in case any validation failed then it will raise exception. So it will help you sort out issue.
For the moment try to create lollypop using the association method create_lollypop directly in your controller. use this code in you create controller method, note that create_lollypop method will fill (member_id field automatically):
#member = Member.create(members_params)
return unless request.post?
#member.save!
self.current_user = #member
c = Country.find(#member.country_id)
#member.update_attributes(
:country_code=>c.code)
#From here I want to create lollypop
#member.create_lollypop(
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
MemberMailer.signup_notification(#member).deliver_now
redirect_to(:controller => '/admin/members', :action => 'show',
:id=> #member.id)
flash[:notice] = "Thanks for signing up! Check your email now to
confirm that your email is correct!"
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This is not exactly an answer, more like tips and notes, it's a little long and I hope you don't mind.
return unless request.post?
This is more of a php thing not a rails thing, in rails already the routing is checking this, so you don't need to do this check inside the controller, if it isn't a post it will be routed elsewhere.
#member = Member.create(members_params)
return unless request.post?
#member.save!
Saving after creating is meaningless, because create already saves the data, if you are doing it for the sake of the bang save!, then you could use the create with bang create!, not to mention that you do the redirection check after the member's create, so if this did work, it would leave you with stray members.
c = Country.find(#member.country_id)
#member.update_attributes(:country_code=>c.code)
If you have your assocciations correctly, you don't need to save the code like this, because the member knows that this country_id belongs to a country.
So add this to the member model
class Member < ActiveRecord::Base
has_one :lollypop, dependent: :destroy
belongs_to :country
end
This way you could always call #member.country to return the country object, then the code could come from there, like #member.country.code, or you could just write a method to shorten that up
def country_code
country.code
end
this way will get the code through an extra query, but it has an advantage, if you for any reason change a country's code, you don't need to loop on all members who have that country and update their codes too, you could also shorten this up even more using #delegate
#member.save!
#member.update_attributes(:country_code=>c.code)
Here you are updating the attributes of member after saving the member, which is kinda a waste, because you are doing 2 queries for what could be done with 1 query, programmatically it is correct and it will work, but it's bad for scaling, when more users start using your app the database will be more busy and the responses will be slower.
Instead i would recommend to postpone the creation of member till you have all the data you want
#member = Member.new(members_params) # this won't save to the database yet
#memeber.code = Country.find(#member.country_id).code
#member.save
This will only do 1 query at the end when all data is ready to be saved.
redirect_to(:controller => '/admin/members', :action => 'show', :id=> #member.id)
This is ok, but you probably have a better shorter path name in your routes, something like members_admin_path, check your routes name by doing a bin/rake routes in your terminal.
redirect_to members_admin_path(id: #member)
redirect_to ...
flash[:notice] = "message"
I'm not sure this will work, because the redirection needs to be returned, but when you added the flash after it, either the redirection will happen without the flash, or the flash will be set and returned as it's the last statement, but the redirection won't happen, not sure which will happen, to fix it you can simply swap the two statements, create the flash first and then redirect, or use the more convenient way of setting the flash while redirecting, cause that's supported
redirect_to ....., notice: 'my message'
rescue ActiveRecord::RecordInvalid
load_data
render :action => 'new'
This will do the job, but it isn't conventional, people tend to use the soft save and then do an if condition on the return value, either true or false, here's a short layout
# prepare #member's data
if #member.save
# set flash and redirect
else
load_data
render :new
end
The lollypop creation
Now there's a few things about this, first you have the method in the controller, which is bad cause it shouldn't be the controller's concern, the second method the self.create_lollypop is better cause it's created on the model level, but it's a class method, then the better way is creating it as a member method, this way the member who creates the lollypop already knows the data because it's his own self, notice i don't need to call #member because i am already inside member, so simple calls like id, email will return the member's data
# inside member.rb
def create_lollypop
Lollypop.create!(
member_id: id,
product_name: 'lollypop',
product_price: 100,
email: email,
house_flat: house_flat,
street: street,
city_town: city_town,
country: country,
postcode_index: postcode_index,
name: name
)
end
if you want you can also add this as an after create callback
after_create :create_lollypop
ps: This method name will probably conflict with the ActiveRecords create_lollypop method, so maybe you should pick a different name for this method.
As Mohammad had suggested to me, I changed Lollypop.create to Lollypop.create! and
while running my code, one validation error popped up. After correcting it and
altering my code to:
Lollypop.create!(
:member_id=> #member.id,
:product_name=>'lollypop',
:product_price=>100,
:email=>#member.email,
:house_flat => #member.house_flat,
:street=>#member.street,
:city_town=>#member.city_town,
:country =>#member.country_code,
:postcode_index=>#member.postcode_index,
:name=>#member.name
)
The 'lollypops' table got populated.

Active Record 4.x and MySQL table column type without using migrations

I'm doing some test with Sinatra v1.4.4 and Active Record v4.0.2. I've created a DBase and a table named Company with Mysql Workbench. In table Company there are two fields lat & long of DECIMAL(10,8) and DECIMAL(11,8) type respectively. Without using migrations I defined the Company model as follow:
class Company < ActiveRecord::Base
end
Everything works except the fact that lat and lng are served as string and not as float/decimal. Is there any way to define the type in the above Class Company definition. Here you can find the Sinatra route serving the JSON response:
get '/companies/:companyId' do |companyId|
begin
gotCompany = Company.find(companyId)
[200, {'Content-Type' => 'application/json'}, [{code:200, company: gotCompany.attributes, message: t.company.found}.to_json]]
rescue
[404, {'Content-Type' => 'application/json'}, [{code:404, message:t.company.not_found}.to_json]]
end
end
Active Record correctly recognize them as decimal. For example, executing this code:
Company.columns.each {|c| puts c.type}
Maybe its the Active Record object attributes method typecast?
Thanks,
Luca
You can wrap the getter methods for those attributes and cast them:
class Company < ActiveRecord::Base
def lat
read_attribute(:lat).to_f
end
def lng
read_attribute(:lng).to_f
end
end
That will convert them to floats, e.g:
"1.61803399".to_f
=> 1.61803399
Edit:
Want a more declarative way? Just extend ActiveRecord::Base:
# config/initializers/ar_type_casting.rb
class ActiveRecord::Base
def self.cast_attribute(attribute, type_cast)
define_method attribute do
val = read_attribute(attribute)
val.respond_to?(type_cast) ? val.send(type_cast) : val
end
end
end
Then use it like this:
class Company < ActiveRecord::Base
cast_attribute :lat, :to_f
cast_attribute :lng, :to_f
end
Now when you call those methods on an instance they will be type casted to_f.
Following diego.greyrobot reply I modified my Company class with an additional method. It overrides the attributes method and afterwards typecast the needed fields. Yet something more declarative would be desirable imho.
class Company < ActiveRecord::Base
def attributes
retHash = super
retHash['lat'] = self.lat.to_f
retHash['lng'] = self.lng.to_f
retHash
end
end

select within uncached block is returning a stale object

I am doing something like the following.
class SomeModel
def crazy
sm = SomeModel.where(:id => 1).first
sleep(30)
# after 15 seconds we update the record manually
sm = SomeModel.uncached {SomeModel.where(:id => 1).first}
begin
sm.field = 'value'
sm.save
rescue ActiveRecord::StaleObjectError => e
puts 'happening'
end
end
end
When this model method is called from a controller, as the uncached select is performed after the manual update, I should not be getting a stale object but testing it says otherwise. Am I misunderstanding something? I am using Rails 3.0.5 with Mysql.

why pageObject based on Cheezy does not work?

I'm new to ruby (1.9.3)
I have intermediate experience with Selenium WebDriver plus C#. I want to move to Watir-Webdriver.
I'd be grateful to find out why the first block of IRB code works, but the second block simply loads the correct page, then does nothing. The page is active and responds to manual input.
The second block of code is based on the PageObject example here:
https://github.com/cheezy/page-object/wiki/Get-me-started-right-now%21
require 'watir-webdriver'
browser = Watir::Browser.start 'http://x.com/'
browser.select_list(:id, "ddlInterestType").select("Deferred")
browser.select_list(:id, "ddlCompanyName").select("XYZ")
browser.button(:value,"Enter Transactions").click
Second block
require 'watir-webdriver'
browser = Watir::Browser.new :firefox
browser.goto "http://x.com/"
deferredPage = DeferredPage.new(browser)
deferredPage.interestType.select = 'Deferred'
deferredPage.company.select = 'XYZ'
deferredPage.enterTransactions
class DeferredPage
include PageObject
select_list(:interestType, :id => 'ddlInterestType')
select_list(:company, :id => 'ddlCompanyName')
button(:enterTransactions, :id => 'btnEnterTransactions')
end
In your page-object code example, after loading the page, an exception is likely being thrown (which makes it seem like nothing happens). That code should throw an no method exception:
undefined method `select=' for "stuff":String
When you declare a select list there are three methods created:
your_select= - this is for setting the select list
your_select - this is for getting the select list value
your_select_element - this is for getting the page-object gem element
When you do deferredPage.interestType, it returns a string that is the value of the select list. Strings do not have a select= method, which is why you get the exception (and does nothing).
The two selections should be done without the .select:
deferredPage.interestType = 'Deferred'
deferredPage.company = 'XYZ'
As you can see the page-object API is slightly different than the watir API.
While googling for info on page objects, I found this page by Alister Scott. :
http://watirmelon.com/2012/06/04/roll-your-own-page-objects/
For an idiot++ such as me, I think I'll use his method until I know more about Watir-Webdriver. Based on #justinko's comment, I'll stick to one API for the present. I tried rolling my own, and it works fine:
require 'watir-webdriver'
browser = Watir::Browser.new :ie
class DeferredPage
def initialize( browser )
#browser = browser
end
def enterIntType(intType)
#browser.select_list(:id, "ddlInterestType").select(intType)
end
def clickEnter()
#browser.button(:value,"Enter Transactions").click
end
end
dp = DeferredPage.new(browser)
browser.goto "http://x.com"
dp.enterIntType( "Deferred" )
dp.clickEnter
Could you please let us know what error you are getting? I suspect the problem you are seeing is related to the way the Ruby interpreter reads the code. It reads the file from top to bottom and you are using the DeferredPage class before it is defined. What would happen if you changed your code to this:
require 'watir-webdriver'
require 'page-object'
browser = Watir::Browser.new :firefox
class DeferredPage
include PageObject
select_list(:interestType, :id => 'ddlInterestType')
select_list(:company, :id => 'ddlCompanyName')
button(:enterTransactions, :id => 'btnEnterTransactions')
end
deferredPage = DeferredPage.new(browser)
deferredPage.navigate_to "http://x.com/"
deferredPage.interestType = 'Deferred'
deferredPage.company = 'XYZ'
deferredPage.enterTransactions
In this case I am declaring the class prior to using it.
Another thing I might suggest is creating a higher level method to perform the data entry. For example, you could change your code to this:
require 'watir-webdriver'
require 'page-object'
browser = Watir::Browser.new :firefox
class DeferredPage
include PageObject
select_list(:interestType, :id => 'ddlInterestType')
select_list(:company, :id => 'ddlCompanyName')
button(:enterTransactions, :id => 'btnEnterTransactions')
def do_something(interest, company)
self.interestType = interest
self.company = company
enterTransactions
end
end
deferredPage = DeferredPage.new(browser)
deferredPage.navigate_to "http://x.com/"
deferredPage.do_someting('Deferred', 'XYZ')
This is cleaner - the access to the page is abstracted behind a method that should add some business value.
-Cheezy

Neo4j::Rails::Model to_json - node id is missing

I have a Jruby on Rails application with Neo4j.rb and a model, let's say Auth, defined like this:
class Auth < Neo4j::Rails::Model
property :uid, :type => String, :index => :exact
property :provider, :type => String, :index => :exact
property :email, :type => String, :index => :exact
end
And this code:
a = Auth.find :uid => 324, :provider => 'twitter'
# a now represents a node
a.to_json
# outputs: {"auth":{"uid": "324", "provider": "twitter", "email": "email#example.com"}}
Notice that the ID of the node is missing from the JSON representation. I have a RESTful API within my application and I need the id to perform DELETE and UPDATE actions.
I tried this to see if it works:
a.to_json :only => [:id]
But it returns an empty JSON {}.
Is there any way I can get the ID of the node in the JSON representation without rewriting the whole to_json method?
Update The same problems applies also to the to_xml method.
Thank you!
I am answering my own question. I still think that there is a better way to do this, but, for now, I am using the following hack:
In /config/initializers/neo4j_json_hack.rb I put the following code:
class Neo4j::Rails::Model
def as_json(options={})
repr = super options
repr.merge! '_nodeId' => self.id if self.persisted?
end
end
And now every JSON representations of my persisted Neo4j::Rails::Model objects have a _nodeId parameter.
The ID is typically not included because it shouldn't be exposed outside the Neo4j database. Neo4j doesn't guarantee that the ID will be identical from instance to instance, and it wouldn't surprise me if the ID changed in a distributed, enterprise installation of Neo4j.
You should create your own ID (GUID?), save it as a property on the node, index it, and use that to reference your nodes. Don't expose the Neo4j ID to your users or application, and definitely don't rely on it beyond a single request (e.g. don't save a reference to it in another database or use it to test for equality).