How to make attribute setter send value through SQL function - sql

I'm trying to make an attribute setter in an ActiveRecord model wrap its value in the text2ltree() postgres function before rails generates its sql query.
For example,
post.path = "1.2.3"
post.save
Should generate something like
UPDATE posts SET PATH=text2ltree('1.2.3') WHERE id = 123 # or whatever
What's the best way of doing this?

EDIT: To achieve exactly what you are looking for above, you'd use this to override the default setter in your model file:
def path=(value)
self[:path] = connection.execute("SELECT text2ltree('#{value}');")[0][0]
end
Then the code you have above works.
I'm interested in learning more about ActiveRecord's internals and its impenetrable metaprogramming underpinnings, so as an exercise I tried to accomplish what you described in your comments below. Here's an example that worked for me (this is all in post.rb):
module DatabaseTransformation
extend ActiveSupport::Concern
module ClassMethods
def transformed_by_database(transformed_attributes = {})
transformed_attributes.each do |attr_name, transformation|
define_method("#{attr_name}=") do |argument|
transformed_value = connection.execute("SELECT #{transformation}('#{argument}');")[0][0]
write_attribute(attr_name, transformed_value)
end
end
end
end
end
class Post < ActiveRecord::Base
attr_accessible :name, :path, :version
include DatabaseTransformation
transformed_by_database :name => "length"
end
Console output:
1.9.3p194 :001 > p = Post.new(:name => "foo")
(0.3ms) SELECT length('foo');
=> #<Post id: nil, name: 3, path: nil, version: nil, created_at: nil, updated_at: nil>
In real life I presume you'd want to include the module in ActiveRecord::Base, in a file somewhere earlier in the load path. You'd also have to properly handle the type of the argument you are passing to the database function. Finally, I learned that connection.execute is implemented by each database adapter, so the way you access the result might be different in Postgres (this example is SQLite3, where the result set is returned as an array of hashes and the key to the first data record is 0].
This blog post was incredibly helpful:
http://www.fakingfantastic.com/2010/09/20/concerning-yourself-with-active-support-concern/
as was the Rails guide for plugin-authoring:
http://guides.rubyonrails.org/plugins.html
Also, for what it's worth, I think in Postgres I'd still do this using a migration to create a query rewrite rule, but this made for a great learning experience. Hopefully it works and I can stop thinking about how to do it now.

Related

Translating SQL Query to ActiveRecord and Rendering as a Table

I am trying to translate a raw SQL query in my model to use ActiveRecord Query Interface. I think that I translated the query correctly, but I cannot transform it to an array in order to render it.
Here is my model:
class Pgdb < ActiveRecord::Base
self.abstract_class = true
self.table_name = 'test'
if Rails.env.development?
establish_connection :pg_development
end
def self.getInfo(name)
get = Pgdb.where(city: "New York")
get_a = get.to_a
get_a
end
end
The raw SQL query, which I was able to render, was:
get = connection.query("SELECT * FROM test WHERE city = "New York")
As the above code indicates, I am accessing an external PostgreSQL database and have tried to convert the ActiveRecord object to an array using #to_a but this isn't working. When I try to render it in my view:
<% #info.each do |row| %>
<tr>
<% row.each do |element| %>
<td><%= element %></td>
<% end %>
</tr>
<% end %>
I get an error: undefined method 'each' for #<Pgdb:0x007fd37af79040>. I have tried using to_a on the object in different places in the code but nothing has worked.
Controller
def index
end
def new
#thing = Thing.new
end
def create
#info = Pgdb.getInfo(#thing.something)
render :index
end
You are receiving the error undefined method 'each' for an instance of Pgdb because your code is trying to iterate over the instance's data attributes in this line:
<% row.each do |element| %>
ActiveRecord instances are not collections of attributes that you can iterate over. Instead, they are objects that respond to messages named for their attributes. In other words, you can do this:
p = Pgdb.first
p.city # because the underlying table has a `city` attribute
but you can't do this:
p.each { |attribute| puts attribute }
However, ActiveRecord provides the attributes accessor for just this very thing. The attributes method returns a hash that you can iterate over with the each method. Therefore, you can do this:
p.attributes.each { |key, value| puts "#{key}: #{value}" }
In your view, you can substitute the inner loop with:
<% row.attributes.each do |key, value| %>
<td><%= "#{key}: #{value}" %></td>
<% end %>
And this should render the attributes for your instance of Pgdb.
By the way, it is not necessary to cast the result of where into an Array in Pgdb::getInfo. The where query returns an ActiveRecord::Relation object that responds to each, as well as other Enumerable messages like map and select, similarly to an array. In your code, you are successfully iterating over the result set in
<% #info.each do |row| %>
This will work whether you use to_a or not in getInfo. There are good reasons not to cast your result set to an array. For one, ActiveRecord::Relation objects have other capabilities, like scoping, which you may often need to use.
Hope that helps. Happy coding!
The correct way to connect rails to an external database is using the config/database.yml file:
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
defaults: &defaults
adapter: postgresql
encoding: utf8
template: template0
# used for test & development
local:
host: localhost
username: j_random_user # change this!
password: p4ssword # change this!
development:
<<: *defaults
<<: *local
database: my_app_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *defaults
<<: *local
database: my_app_test
production:
<<: *defaults
host: "my_app_something.rds.amazonaws.com" # change this!
username: my_amazon_db_user # change this!
password: my_amazon_user # change this!
reconnect: true
port: 3306
You may want to use a local postgres database for development and mirror the production database with pgbackups.
But your main issue is that your are doing pretty much everything wrong when it comes to creating a rails application. That looks like a PHP example where some clueless soul is reinventing an database manager for the 1000th time.
So here is a fast Rails MVC & ActiveRecord crash course:
Models reflect the objects in your domain. Lets say we have a pet store app.
We of course need a Pet model:
$ rails g model Pet name:string
$ rake db:migrate
This creates a pets table in the database and a Pet class. Note that the table name is plural and the model name is singular.
# app/models/pet.rb
class Pet < ActiveRecord::Base
end
We can then access the pets by:
Pet.all
Pet.find(1) # find pet where id is 1
# ... etc
And we can create pets by:
pet = Pet.create(name: "Spot")
All of this is covered in most basic rails tutorials.
Connecting without a model.
ActiveRecord::Base.establish_connection
con = ActiveRecord::Base.connection
res = con.execute('SELECT * FROM foo')
Although using ActiveRecord does not really make sense if you are not actually using a model per table and following the MVC conventions somewhat. It's possible, but it gives you no real benefits at all.
Likewise doing Rails without MVC is doable but what's the point?
Using a legacy database
let's say you have a legacy database written by someone who though using Apps Hungarian for database columns (shrug) was cool:
persons:
intPersonID: Integer, Autoincrement, Primary Key
And you want to map this to a Rails model
class User < ActiveRecord::Base
self.table_name 'persons'
self.primary_key 'intPersonID'
end
Or in your case:
class Test < ActiveRecord::Base
self.table_name 'test' # since it's not "tests"
end
Test.where(city: "New York")

Dynamic define_method throwing error in RSpec

I am pretty sure I am missing a basic mistake here, so I am hoping another set of eyes might help. I am using Rails 3, Ruby 1.9.2 and Rspec 2.
I would like to define dynamic class methods on a model so that I can return base roles for an assignable object (such as account) as they are added to the system. For example:
BaseRole.creator_for_account
Everything works fine via the console:
ruby-1.9.2-p180 :003 > BaseRole.respond_to?(:creator_for_account)
=> true
but when I run my specs for any of class methods, I get a NoMethodError wherever I call the method in the spec. I am assuming that something about how I am dynamically declaring the methods is not jiving with RSpec but I cannot seem to figure out why.
The lib dir is autoloaded path and the methods return true for respond_to?.
# /lib/assignable_base_role.rb
module AssignableBaseRole
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
BaseRole.all.each do |base_role|
role_type = RoleType.find(base_role.role_type_id)
assignable_name = base_role.assignable_type.downcase
method = "#{role_type.name}_for_#{assignable_name}"
define_method(method) do
self.where(:role_type_id => role_type.id,
:assignable_type => assignable_name).first
end
end
end
end
Then include the Module in BaseRole
# /models/base_role.rb
class BaseRole < ActiveRecord::Base
include AssignableBaseRole
belongs_to :role
belongs_to :role_type
......
......
end
Then in my spec:
it "adds correct authority for creator role" do
create_assignment
base_role = BaseRole.creator_for_account # <== NoMethodError here
user1 = Factory.create(:user)
account.users << user1
user1.roles_for_assignable(account).should include(base_role.role)
end
Did you have another class in your project or specs with the same name, but doesn't have the dynamic methods added? I had the exact same problem as you, and renaming one of the classes fixed it.
My guess is the other class is getting loaded first
It appears you are defining these methods based on values in the database:
BaseRole.all.each do |base_role|
.....
Could it be that "creator" doesn't exist in the test database as a role type, or "account" doesn't exist as assignable_type?
Presumably you are testing this in the console for development, not test, so the data could be mismatched. Might need to set up the data in a before hook.

Why does this 'validate' method raise an ArgumentError?

Folks,
I can't get validates_with in my (helloworld-y) rails app to work. Read through "callbacks and validators" section of the original RoR guides site and searched stackoverflow, found nothing.
Here's the stripped-down version of code I got after removing everything that can fail.
class BareBonesValidator < ActiveModel::Validator
def validate
# irrelevant logic. whatever i put here raises the same error - even no logic at all
end
end
class Unvalidable < ActiveRecord::Base
validates_with BareBonesValidator
end
Looks like textbook example, right? They have the very similar snippet on RoR guides. Then we go to the rails console and get an ArgumentError while validating new record:
ruby-1.9.2-p180 :022 > o = Unvalidable.new
=> #<Unvalidable id: nil, name: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :023 > o.save
ArgumentError: wrong number of arguments (1 for 0)
from /Users/ujn/src/yes/app/models/unvalidable.rb:3:in `validate'
from /Users/ujn/.rvm/gems/ruby-1.9.2-p180#wimmie/gems/activesupport-3.0.7/lib/active_support/callbacks.rb:315:in `_callback_before_43'
I know I'm missing something, but what?
(NB: To avoid putting BareBonesValidator into separate file I left it atop model/unvalidable.rb).
The validate function should take the record as parameter (otherwise you can't access it in the module). It's missing from the guide but the official doc is correct.
class BareBonesValidator < ActiveModel::Validator
def validate(record)
if some_complex_logic
record.errors[:base] = "This record is invalid"
end
end
end
Edit: And it's already fixed in the edge guide.
Error ArgumentError: wrong number of arguments (1 for 0) means that the validate method was called with 1 argument but the method has been defined to take 0 arguments.
So define your validate method like below and try again:
class BareBonesValidator < ActiveModel::Validator
def validate(record) #added record argument here - you are missing this in your code
# irrelevant logic. whatever i put here raises the same error - even no logic at all
end
end

Track dirty for not-persisted attribute in an ActiveRecord object in rails

I have an object that inherits from ActiveRecord, yet it has an attribute that is not persisted in the DB, like:
class Foo < ActiveRecord::Base
attr_accessor :bar
end
I would like to be able to track changes to 'bar', with methods like 'bar_changed?', as provided by ActiveModel Dirty. The problem is that when I try to implement Dirty on this object, as described in the docs, I'm getting an error as both ActiveRecord and ActiveModel have defined define_attribute_methods, but with different number of parameters, so I'm getting an error when trying to invoke define_attribute_methods [:bar].
I have tried aliasing define_attribute_methods before including ActiveModel::Dirty, but with no luck: I get a not defined method error.
Any ideas on how to deal with this? Of course I could write the required methods manually, but I was wondering if it was possible to do using Rails modules, by extending ActiveModel functionality to attributes not handled by ActiveRecord.
I'm using the attribute_will_change! method and things seem to be working fine.
It's a private method defined in active_model/dirty.rb, but ActiveRecord mixes it in all models.
This is what I ended up implementing in my model class:
def bar
#bar ||= init_bar
end
def bar=(value)
attribute_will_change!('bar') if bar != value
#bar = value
end
def bar_changed?
changed.include?('bar')
end
The init_bar method is just used to initialise the attribute. You may or may not need it.
I didn't need to specify any other method (such as define_attribute_methods) or include any modules.
You do have to reimplement some of the methods yourself, but at least the behaviour will be mostly consistent with ActiveModel.
I admit I haven't tested it thoroughly yet, but so far I've encountered no issues.
ActiveRecord has the #attribute method (source) which once invoked from your class will let ActiveModel::Dirty to create methods such as bar_was, bar_changed?, and many others.
Thus you would have to call attribute :bar within any class that extends from ActiveRecord (or ApplicationRecord for most recent versions of Rails) in order to create those helper methods upon bar.
Edit: Note that this approach should not be mixed with attr_accessor :bar
Edit 2: Another note is that unpersisted attributes defined with attribute (eg attribute :bar, :string) will be blown away on save. If you need attrs to hang around after save (as I did), you actually can (carefully) mix with attr_reader, like so:
attr_reader :bar
attribute :bar, :string
def bar=(val)
super
#bar = val
end
I figured out a solution that worked for me...
Save this file as lib/active_record/nonpersisted_attribute_methods.rb: https://gist.github.com/4600209
Then you can do something like this:
require 'active_record/nonpersisted_attribute_methods'
class Foo < ActiveRecord::Base
include ActiveRecord::NonPersistedAttributeMethods
define_nonpersisted_attribute_methods [:bar]
end
foo = Foo.new
foo.bar = 3
foo.bar_changed? # => true
foo.bar_was # => nil
foo.bar_change # => [nil, 3]
foo.changes[:bar] # => [nil, 3]
However, it looks like we get a warning when we do it this way:
DEPRECATION WARNING: You're trying to create an attribute `bar'. Writing arbitrary attributes on a model is deprecated. Please just use `attr_writer` etc.
So I don't know if this approach will break or be harder in Rails 4...
Write the bar= method yourself and use an instance variable to track changes.
def bar=(value)
#bar_changed = true
#bar = value
end
def bar_changed?
if #bar_changed
#bar_changed = false
return true
else
return false
end
end

How can I see the SQL that will be generated by a given ActiveRecord query in Ruby on Rails

I would like to see the SQL statement that a given ActiveRecord Query will generate. I recognize I can get this information from the log after the query has been issued, but I'm wondering if there is a method that can be called on and ActiveRecord Query.
For example:
SampleModel.find(:all, :select => "DISTINCT(*)", :conditions => ["`date` > #{self.date}"], :limit => 1, :order => '`date`', :group => "`date`")
I would like to open the irb console and tack a method on the end that would show the SQL that this query will generate, but not necessarily execute the query.
Similar to penger's, but works anytime in the console even after classes have been loaded and the logger has been cached:
For Rails 2:
ActiveRecord::Base.connection.instance_variable_set :#logger, Logger.new(STDOUT)
For Rails 3.0.x:
ActiveRecord::Base.logger = Logger.new(STDOUT)
For Rails >= 3.1.0 this is already done by default in consoles. In case it's too noisy and you want to turn it off you can do:
ActiveRecord::Base.logger = nil
Stick a puts query_object.class somewhere to see what type of object your working with, then lookup the docs.
For example, in Rails 3.0, scopes use ActiveRecord::Relation which has a #to_sql method. For example:
class Contact < ActiveRecord::Base
scope :frequently_contacted, where('messages_count > 10000')
end
Then, somewhere you can do:
puts Contact.frequently_contacted.to_sql
just use to_sql method and it'll output the sql query that will be run. it works on an active record relation.
irb(main):033:0> User.limit(10).where(:username => 'banana').to_sql
=> "SELECT "users".* FROM "users" WHERE "users"."username" = 'banana'
LIMIT 10"
when doing find, it won't work, so you'll need to add that id manually to the query or run it using where.
irb(main):037:0* User.where(id: 1).to_sql
=> "SELECT "users".* FROM "users" WHERE "users"."id" = 1"
This may be an old question but I use:
SampleModel.find(:all,
:select => "DISTINCT(*)",
:conditions => ["`date` > #{self.date}"],
:limit=> 1,
:order => '`date`',
:group => "`date`"
).explain
The explain method will give quite a detailed SQL statement on what its going to do
This is what I usually do to get SQL generated in console
-> script/console
Loading development environment (Rails 2.1.2)
>> ActiveRecord::Base.logger = Logger.new STDOUT
>> Event.first
You have to do this when you first start the console, if you do this after you have typed some code, it doesn't seem to work
Can't really take credit for this, found it long time ago from someone's blog and can't remember whose it is.
When last I tried to do this there was no official way to do it. I resorted to using the function that find and its friends use to generate their queries directly. It is private API so there is a huge risk that Rails 3 will totally break it, but for debugging, it is an ok solution.
The method is construct_finder_sql(options) (lib/active_record/base.rb:1681) you will have to use send because it is private.
Edit: construct_finder_sql was removed in Rails 5.1.0.beta1.
Create a .irbrc file in your home directory and paste this in:
if ENV.include?('RAILS_ENV') && !Object.const_defined?('RAILS_DEFAULT_LOGGER')
require 'logger'
RAILS_DEFAULT_LOGGER = Logger.new(STDOUT)
end
That will output SQL statements into your irb session as you go.
EDIT: Sorry that will execute the query still, but it's closest I know of.
EDIT: Now with arel, you can build up scopes/methods as long as the object returns ActiveRecord::Relation and call .to_sql on it and it will out put the sql that is going to be executed.
My typical way to see what sql it uses is to introduce a "bug" in the sql, then you'll get an error messages spit out to the normal logger (and web screen) that has the sql in question. No need to find where stdout is going...
Try the show_sql plugin. The plugin enables you to print the SQL without running it
SampleModel.sql(:select => "DISTINCT(*)", :conditions => ["`date` > #{self.date}"], :limit => 1, :order => '`date`', :group => "`date`")
You could change the connection's log method to raise an exception, preventing the query from being run.
It's a total hack, but it seems to work for me (Rails 2.2.2, MySQL):
module ActiveRecord
module ConnectionAdapters
class AbstractAdapter
def log_with_raise(sql, name, &block)
puts sql
raise 'aborting select' if caller.any? { |l| l =~ /`select'/ }
log_without_raise(sql, name, &block)
end
alias_method_chain :log, :raise
end
end
end
You can simply use to_sql() function with the active record
Form.where(status:"Active").to_sql
In Rails 3 you can add this line to the config/environments/development.rb
config.active_record.logger = Logger.new(STDOUT)
It will however execute the query. But half got answered :