belongs_to association without an extra table - ruby-on-rails

I have a model Download, with a table downloads. downloads has a field called ip_address, which stores an ip address as an integer. I want to set up an IpAddress model, but without a ip_addresses table, so I can do stuff like
Download.find(1).ip_address.to_s # '127.0.0.1'
Download.find(1).ip_address.to_i # 2130706433
Download.find(1).ip_address.downloads # SELECT * FROM downloads WHERE ip_address='2130706433'
IpAddress.find(2130706433).downloads # SELECT * FROM downloads WHERE ip_address='2130706433'
I want it to behave like:
class Download < ActiveRecord::Base
belongs_to :ip_address, :foreign_key => :ip_address
end
class IpAddress < ActiveRecord::Base
set_primary_key :ip_address
has_many :downloads, :foreign_key => :ip_address
end
but without having a useless table of ip addresses.
Is this possible?
EDIT
I found that ruby already has a IPAddr class.
So I did this:
require 'ipaddr'
class Download < ActiveRecord::Base
attr_accessible :ip, ...
def ip
#ip ||= IPAddr.new(read_attribute(:ip), Socket::AF_INET)
end
def ip=(addr)
#ip = IPAddr.new(addr, Socket::AF_INET)
write_attribute(:ip, #ip.to_i)
end
def self.for_ip(addr)
where(:ip => IPAddr.new(addr, Socket::AF_INET).to_i)
end
end
Then I can do lots of cool stuff
Download.new(:ip => '127.0.0.1').save
Download.for_ip('127.0.0.1').first.ip.to_i # 2130706433

belongs_to is really meant to specify an association between objects in two tables. But you're right, unless you need to store other associated data, storing IP addresses in a table is fairly useless.
However, you can use scopes to accomplish what you will want. You could have something like this in your Download model:
class Download < ActiveRecord::Base
scope :for_ip, lambda { |x| where(:ip_address => x) }
end
Then you would call
Download.for_ip(2130706433)
To get a list of downloads for that IP.
You could also add a class method instead:
class Download < ActiveRecord::Base
def self.for_ip(x)
where(:ip_address => x)
end
end
That might be handy if you want to convert from string to numeric IP addresses.
And, if you want an IPAddress class, you can add a method like this:
class IPAddress
def initialize(ip)
#presumably do some stuff here
#ip = ip
end
def downloads
Download.for_ip(#ip)
end
end

IpAddress.find(2130706433).downloads # SELECT * FROM downloads WHERE ip_address='2130706433'
This is totally a semantics issue, but this should probably change if you have no IpAddress table (i.e. how can we find the IpAddress object 2130706433 in the database if there is no IpAddress table - unless you make IpAddress a container rather than a specific single ipaddress, otherwise do something like instantiate new ones with a constructer like IpAddress(2130706433).downloads).
Otherwise, though, I don't see any problems in not having the IpAddress table. Why do you need it to be belongs_to, rather than just another column?
You can keep the models/objects if you wish to access them in similar ways:
class Download < ActiveRecord::Base
##Whatever Download-model-specific code you have...
def ip_address
#If nil, initialize new object. Return object.
#ip_address ||= IpAddress(ip_address_val)
end
end
class IpAddress
def initialize(address)
#value = address
end
def downloads
Download.where(:ip_address_val => self.value)
end
end
EDIT:
You can override the accessor, like you're asking. You just have to be careful in your code to be particular about what you're asking for.
See this doc: http://ar.rubyonrails.org/classes/ActiveRecord/Base.html
Under section "Overwriting default accessors"
Basically, if you do override the value, and if you wish to access the DB value, you use read_attribute(attr_name), so the code might look like this:
class Download < ActiveRecord::Base
##Whatever Download-model-specific code you have...
def ip_address
#If nil, initialize new object. Return object.
#ip_address ||= IpAddress(read_attribute(:ip_address))
end
end
class IpAddress
def initialize(address)
#value = address
end
def downloads
Download.where(:ip_address => self.value)
end
end
Though things might get a little confusing in your code if you aren't careful.

Add set_table_name "downloads" to your IpAddress and remove the relationship between the 2 coz it already has the column name ip_address.
This will give you queries in following way
Download.find(1).ip_address.to_s # '127.0.0.1'
Download.find(1).ip_address.to_i # 2130706433
IpAddress.find(Download.find(1).ip_address) # SELECT * FROM downloads WHERE ip_address='2130706433'
IpAddress.find(2130706433) # SELECT * FROM downloads WHERE ip_address='2130706433'

Related

Access to model instance up the where chain in Rails

Is it possible to, within the record found through an association, retain access to the related model instance which found it?
Example:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
end
p = Person.first
p.info_of_the_moment = "I don't want this in the db"
assignment = p.assignments.first
assignment.somehow_get_p.info_of_the_moment # or some such magic!
And/or is there a way to "hang on to" the parameters of a scope and have access to them from within the found model instance? Like:
class Person < ApplicationRecord
has_many :assignments
attr_accessor :info_of_the_moment
scope :fun_assignments, -> (info) { where(fun: true) }
end
class Assignment < ApplicationRecord
belongs_to :person
def get_original_info
# When I was found, info was passed into the scope. What was it?
end
end
You can add your own extension methods to an association and those methods can get at the association's owner through proxy_association:
has_many :things do
def m
# Look at proxy_association.owner in here
end
end
So you could say things like:
class Person < ApplicationRecord
has_many :assignments do
def with_info
info = proxy_association.owner.info_of_the_moment
# Then we wave our hands and some magic happens to encode
# `info` into a properly escaped SQL literal that we can
# toss in a `select` call. If you're working with PostgreSQL
# then JSON would be a reasonable first choice if the info
# was, say, a hash.
#
# The `::jsonb` in the `select` call is there to tell everyone
# that the `info_of_the_moment` column is JSON and should be
# decoded as such by ActiveRecord.
encoded_info = ApplicationRecord.connection.quote(info.to_json)
select("assignments.*, #{encoded_info}::jsonb as info_of_the_moment")
end
end
#...
end
p = Person.first
p.info_of_the_moment = { 'some hash' => 'that does', 'not go in' => 'the database' }
assignment = p.assignments.with_info.first
assignment.info_of_the_moment # And out comes the hash but with stringified keys regardless of the original format.
# These will also include the `info_of_the_moment`
p.assignments.where(...).with_info
p.assignments.with_info.where(...)
Things of note:
All the columns in select show up as methods even when they're not part of the table in question.
You can add "extension" methods to an association by including a block with those methods when calling the association's method.
An SQL SELECT can include values that aren't columns, literals work just fine.
What format you use to tunnel your extra information through the association depends on the underlying database.
If the encoded extra information is large then this can get expensive.
This is admittedly a bit kludgey and brittle so I'd agree with you that rethinking your whole approach is a better idea.

Pulling attribute names from associated child model to parent model

class User
has_many :addresses
def csv_header
self.addresses.attribute_names
end
def csv_values
self.addresses.all do |addr|
addr.attributes.values
end
end
end
class Address
belongs_to :user
end
*i am trying to pull the attribute names of the address model to user model,but this method isn't working so can anyone help *
Not much needed here - I think you just need to map the addresses in csv_values.
class User
has_many :addresses
def csv_header
addresses.attribute_names
end
def csv_values
addresses.map do |addr|
addr.attributes.values
end
end
end
class Address
belongs_to :user
end
Does that fix this for you?
I'd be tempted to shift things around a little for clarity in the code and make use of delegate:
class user
...
delegate :attribute_names, to: :addresses, prefix: true, allow_nil: true
...
end
class Address
...
def self.mapped_values
all.map { |addr| addr.attributes.values }
end
...
end
Then you can just call user.addresses_attribute_names and user.addresses.mapped_values.
You can also just call Address.column_names to get the header array, if it will always stay consistent, as is likely to be the case.
Hope that helps!
Update based on comment:
To achieve the same for users, you can call the following:
Either call User.column_names or user.attribute_names to get the headers (on the class for the former, and an instance for the latter).
If you also need the users' mapped values, you can copy across the self.mapped_values method from the address model and use that. It's a little duplication, but for a pair of methods like this I wouldn't be inclined to separate these into a separate module.
Final tip - if you're calling the address methods from a collection of users (i.e. User.all) make sure you adjust it to include the addresses to avoid hitting your database in an inefficient way (User.includes(:addresses)...).

Grab original method/column name from Rails alias_attribute

Right now, I have some legacy classes with differently-named columns that I've aliased to a new, common name via Rails' alias_attribute, as below:
class User < ActiveRecord::Base
alias_attribute :id, :UserId
...
end
class Car < ActiveRecord::Base
alias_attribute :id, :CarId
...
end
For some logging purposes, I need to access the old column names (eg. CarId and UserId). Is there a general way to access the old name from alias_attribute via its alias? Renaming the old columns is not ideal, since many other parts of the app are still using the old column names.
alias_attribute is a very simple method. All it does is, well, define aliases.
def alias_attribute(new_name, old_name)
# The following reader methods use an explicit `self` receiver in order to
# support aliases that start with an uppercase letter. Otherwise, they would
# be resolved as constants instead.
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
STR
end
So no, there's no way to retrieve the original column name.

How to update instance variable in Rails model?

In my Rails app I have users who can have many payments.
class User < ActiveRecord::Base
has_many :invoices
has_many :payments
def year_ranges
...
end
def quarter_ranges
...
end
def month_ranges
...
end
def revenue_between(range, kind)
payments.sum_within_range(range, kind)
end
end
class Invoice < ActiveRecord::Base
belongs_to :user
has_many :items
has_many :payments
...
end
class Payment < ActiveRecord::Base
belongs_to :user
belongs_to :invoice
def net_amount
invoice.subtotal * percent_of_invoice_total / 100
end
def taxable_amount
invoice.total_tax * percent_of_invoice_total / 100
end
def gross_amount
invoice.total * percent_of_invoice_total / 100
end
def self.chart_data(ranges, unit)
ranges.map do |r| {
:range => range_label(r, unit),
:gross_revenue => sum_within_range(r, :gross),
:taxable_revenue => sum_within_range(r, :taxable),
:net_revenue => sum_within_range(r, :net) }
end
end
def self.sum_within_range(range, kind)
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount")
end
end
In my dashboard view I am listing the total payments for the ranges depending on the GET parameter that the user picked. The user can pick either years, quarters, or months.
class DashboardController < ApplicationController
def show
if %w[year quarter month].include?(params[:by])
#unit = params[:by]
else
#unit = 'year'
end
#ranges = #user.send("#{#unit}_ranges")
#paginated_ranges = #ranges.paginate(:page => params[:page], :per_page => 10)
#title = "All your payments"
end
end
The use of the instance variable (#sum) greatly reduced the number of SQL queries here because the database won't get hit for the same queries over and over again.
The problem is, however, that when a user creates, deletes or changes one of his payments, this is not reflected in the #sum instance variable. So how can I reset it? Or is there a better solution to this?
Thanks for any help.
This is incidental to your question, but don't use #select with a block.
What you're doing is selecting all payments, and then filtering the relation as an array. Use Arel to overcome this :
scope :within_range, ->(range){ where date: range }
This will build an SQL BETWEEN statement. Using #sum on the resulting relation will build an SQL SUM() statement, which is probably more efficient than loading all the records.
Instead of storing the association as an instance variable of the Class Payment, store it as an instance variable of a user (I know it sounds confusing, I have tried to explain below)
class User < ActiveRecord::Base
has_many :payments
def revenue_between(range)
#payments_with_invoices ||= payments.includes(:invoice => :items).all
# #payments_with_invoices is an array now so cannot use Payment's class method on it
#payments_with_invoices.select { |x| range.cover? x.date }.sum(&:total)
end
end
When you defined #sum in a class method (class methods are denoted by self.) it became an instance variable of Class Payment. That means you can potentially access it as Payment.sum. So this has nothing to do with a particular user and his/her payments. #sum is now an attribute of the class Payment and Rails would cache it the same way it caches the method definitions of a class.
Once #sum is initialized, it will stay the same, as you noticed, even after user creates new payment or if a different user logs in for that matter! It will change when the app is restarted.
However, if you define #payments_with_invoiceslike I show above, it becomes an attribute of a particular instance of User or in other words instance level instance variable. That means you can potentially access it as some_user.payments_with_invoices. Since an app can have many users these are not persisted in Rails memory across requests. So whenever the user instance changes its attributes are loaded again.
So if the user creates more payments the #payments_with_invoices variable would be refreshed since the user instance is re-initialized.
Maybe you could do it with observers:
# payment.rb
def self.cached_sum(force=false)
if #sum.blank? || force
#sum = includes(:invoice => :items)
end
#sum
end
def self.sum_within_range(range)
#sum = cached_sum
#sum.select { |x| range.cover? x.date }.sum(&total)
end
#payment_observer.rb
class PaymentObserver < ActiveRecord::Observer
# force #sum updating
def after_save(comment)
Payment.cached_sum(true)
end
def after_destroy(comment)
Payment.cached_sum(true)
end
end
You could find more about observers at http://apidock.com/rails/v3.2.13/ActiveRecord/Observer
Well your #sum is basically a cache of the values you need. Like any cache, you need to invalidate it if something happens to the values involved.
You could use after_save or after_create filters to call a function which sets #sum = nil. It may also be useful to also save the range your cache is covering and decide the invalidation by the date of the new or changed payment.
class Payment < ActiveRecord::Base
belongs_to :user
after_save :invalidate_cache
def self.sum_within_range(range)
#cached_range = range
#sum ||= includes(:invoice => :items)
#sum.select { |x| range.cover? x.date }.sum(&total)
end
def self.invalidate_cache
#sum = nil if #cached_range.includes?(payment_date)
end

Proper way to do this with ActiveRecord?

Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.

Resources