For single table inheritance, how do you force Rails to use an integer column for the 'type' column instead of string?
You can override the methods Rails uses to convert the table name to class name and vice versa:
The relevant methods are find_sti_class which is responsible for the translating the value stored in the type column to the respective ActiveRecord model and sti_name which is responsible for retriving the value stored in type column given an ActiveRecord subclass.
You can override them like this:
class Institution::Base < ActiveRecord::Base
ALLOWED_CLASSES = %w[Institution::NonProfit Institution::Commercial]
class << self
def find_sti_class type_name
idx = type_name.to_i
super if idx == 0
ALLOWED_CLASSES[idx-1].constantize
rescue NameError, TypeError
super
end
def sti_name
idx = ALLOWED_CLASSES.index(self.name)
if idx.nil?
super
else
idx + 1
end
end
end
end
I have written a post elaborating this in more detail.
You would have to find the part of ActiveRecord responsible for handling the "type" column and monkey patch it, i.e. override how it worked from within your application.
Related
Say a lot of my models implement a class method self.foo which returns a ::ActiveRecord_Relation.
From elsewhere in the code, I have access to the Model or the ::ActiveRecord_Relation returned by that method, and I would like to find out which columns are used in where expressions in the query.
If the query is simple, I can get it from where_values_hash on the ActiveRecord_Relation, but I'm having trouble figuring out how to get all the columns in more complex queries.
class A < ApplicationRecord
def self.foo(bar)
where(column1: bar)
end
end
class B < ApplicationRecord
def self.foo(bar)
joins(:c).
merge(C.where(column2: bar))
end
end
elsewhere
# this evaluates to ["column1"] - success!
A.foo(bar).where_values_hash.keys
# How can I get "c" or C and "column2" from B or B.foo(bar)?
# B or B.foo(bar).??? -> ["c", "column2"]
I could always get it from parsing the relation's .to_sql string, but there's got to be a better way.
The where_values_hash method takes a parameter which is the table you want the where clause columns from. If you would like all of them you can do some stuff with Arel to get all of the joined tables where_values_hash. The only trick will be that you likely want to keep the context of the table the columns come from.
def join_where_values(relation)
relation.arel.join_sources.map do |r|
{ r.left.name => relation.where_values_hash(r.left.name) }
end
end
{ 'b' => B.foo(bar).where_values_hash, 'joins' => join_where_values(B.foo(bar)) }
I have a class that looks like this:
class AwsAssets
attr_reader :AWS_INSTANCE, :AWS_INSTANCE_RESERVATION, :AWS_AVAILABILITY_ZONE
##AWS_INSTANCE = 1
##AWS_INSTANCE_RESERVATION = 2
##AWS_AVAILABILITY_ZONE = 3
end
I'm trying to access these variable ID's in another file. I want to do something along the lines of this:
def index
types = AwsAssets.attr_reader
#filter = "model_type_id NOT IN (#{types.join(', ')})"
end
This is obviously not correct syntax, I'm just wondering if I can access all the attr_reader variables somehow - I realize I could just put all of the attributes into an array, but there are like 100 variables that I would have to duplicate, which I would rather not do.
In your code you mixed couple of different properties that you can define on a class, so I am not sure which one you want to use.
You can use class instance variables
class AwsAssets
#AWS_INSTANCE = 1
#AWS_INSTANCE_RESERVATION = 2
#AWS_AVAILABILITY_ZONE = 3
end
AwsAssets.instance_variables
# => [:#AWS_INSTANCE, :#AWS_INSTANCE_RESERVATION, :#AWS_AVAILABILITY_ZONE]
AwsAssets.instance_variable_get "#AWS_INSTANCE"
# => 1
Or you can use class variables
class AwsAssets
##AWS_INSTANCE = 1
##AWS_INSTANCE_RESERVATION = 2
##AWS_AVAILABILITY_ZONE = 3
end
AwsAssets.class_variables
# [:##AWS_INSTANCE, :##AWS_INSTANCE_RESERVATION, :##AWS_AVAILABILITY_ZONE]
AwsAssets.class_variable_get "##AWS_INSTANCE""
# => 1
When you create class variable with ##, you will have the same value for this class and all subclasses. If you use class instance variables with # you can have different values for parent class and subclasses.
Some meta-programming could be helpful for you here.
class AwsAssets
# define your attributes here in a constant which is accessible from other classes
ATTRIBUTES = [:AWS_INSTANCE, :AWS_INSTANCE_RESERVATION, :AWS_AVAILABILITY_ZONE]
# use the definition above to call `attr_reader` for each attribute in the array
ATTRIBUTES.each { |attribute| attr_reader(attribute) }
# OR a shortcut: attr_reader(*ATTRIBUTES)
end
def index
# You can now access that array of attributes in other areas like so
types = AwsAssets.ATTRIBUTES
#filter = "model_type_id NOT IN (#{types.join(', ')})"
end
Here's a good resource for some more examples and information about meta-programming in Ruby: Introduction to Ruby Meta-Programming Techniques
I have two methods that are identical apart from the ActiveRecord class they are referencing:
def category_id_find(category_name)
category = Category.find_by_name(category_name)
if category != nil
return category.id
else
return nil
end
end
def brand_id_find(brand)
brand = Brand.find_by_name(brand)
if brand != nil
return brand.id
else
return nil
end
end
Now, I just know there must be a more Railsy/Ruby way to combine this into some kind of dynamically-created method that takes two arguments, the class and the string to find, so I tried (and failed) with something like this:
def id_find(class, to_find)
thing = (class.capitalize).find_by_name(to_find)
if thing.id != nil
return thing.id
else
return nil
end
end
which means I could call id_find(category, "Sports")
I am having to populate tables during seeding from a single, monster CSV file which contains all the data. So, for example, I am having to grab all the distinct categories from the CSV, punt them in a Category table then then assign each item's category_id based on the id from the just-populated category table, if that makes sense...
class is a reserved keyword in Ruby (it's used for class declarations only), so you can't use it to name your method parameter. Developers often change it to klass, which preserves the original meaning without colliding with this restriction. However, in this case, you'll probably be passing in the name of a class as a string, so I would call it class_name.
Rails' ActiveSupport has a number of built in inflection methods that you can use to turn a string into a constant. Depending on what your CSV data looks like, you might end up with something like this:
def id_find(class_name, to_find)
thing = (class_name.camelize.constantize).find_by_name(to_find)
...
end
If using a string, you can use constantize instead of capitalize and your code should work (in theory):
thing = passed_in_class.constantize.find_by_name(to_find)
But you can also pass the actual class itself to the method, no reason not to:
thing = passed_in_class.find_by_name(to_find)
I have a rails app with both a native rails database and a legacy database.
I query these legacy tables just fine, BUT I have to insert records into them as well.
But the Legacy table does not conform to rails convention WRT the key. Instead of an auto incrementing integer, the Legacy Key is a string (always numeric characters 0-9, but still a string attribute in the DB). The Legacy database has a table where the current index value is stored, and an insert operation requires reading from the 'current_index' table the current index value, incrementing it by 1, and saving the value back to 'index_table', then returning the new value, which is used in the insert statement for the new key value.
Right now I have the hand crafted SQL required to insert records scattered in various controllers. I'd like to clean that up, and move it into the model.
Currently I do this:
legacy_table.db:
class LegacyTable < Ldb
self.table_name = "legacyTableName"
self.primary_key "legacyKeyName"
end
in controller:
def insert_legacy_record(attrs)
result = Ldb.connection.exec_query("
DECLARE #key int;
EXEC dbo.getKeyField #key OUTPUT, 'RecordKey';
SELECT #key as ref_key;
")
newkey = result.first['ref_key']
result = Ldb.connection.exec_query("
INSERT INTO legacyTableName
(legacyKeyName,foo,...)
VALUES
('#{newkey}','#{attrs[:foo]}',...)
")
end
This is a pain, as I have to manually maintain the list of table attributes and their corresponding values input as attrs.
Can I just override the index generation part of activeRecord? Such that I could do this:
OPTION1
#item = LegacyTable.new
#item.attributes(data)
#item.save <=== somehow override new index value generation
Or should I just override the new method, and have it return the new key value it generates?
OPTION2
newkey = LegacyTable.new(data)
#new = LegacyTable.find(newkey)
I know how to do OPTION2, is OPTION1 possible?
You could create an initializer:
module Extensions
module LegacyModelId
extend ActiveSupport::Concern
included do
before_create :generate_legacy_id
def generate_legacy_id
result = Ldb.connection.exec_query("
DECLARE #key int;
EXEC dbo.getKeyField #key OUTPUT, 'RecordKey';
SELECT #key as ref_key;
")
self.id = result.first['ref_key']
end
end
end
end
In each of your legacy models:
class LegacyModelFoo < ActiveRecord::Base
include Extensions::LegacyModelId
...
end
This will add the before_create callback on each model you include the extension in, causing it to do the lookup and assign the new id.
How about overriding create method in your LegacyTable model like this:
def create
# new_id = The code you use to get/set a new id for insertion
self.id = new_id
super # continue as normal
end
I have a model for one of my database tables. I want to override the column name for that particular table. How would I achieve it.
For example, let my table be called DUMMY and it has one column called col_a
col_a
20
34
42
23
12
I would be doing a #dummy.col_a. Now this method should return me 0 for numbers ending with 0 and for everything else, it should return the original value. I could do that by defining a new method, but I want to override the column name itself. Please help.
You can override the col_a method. Use the read_attribute method to read the value in database. Something like this:
def col_a
if self.read_attribute(:col_a).to_s.end_with?('0')
0
else
self.read_attribute(:col_a)
end
end
You can simply define a method of the same name as the column. To get the actual column value, use self[column_name]. So something like this should work:
class Dummy < ActiveModel::Base
def col_a
self[:col_a] % 10 == 0 ? 0 : self[:col_a]
end
end
(This assumes col_a is an integer.)
I'm a little late to the party here, but a really elegant way to do it is to simply use super
class Dummy < ApplicationRecord
def col_a
super % 10 === 0 ? 0 : super
end
end
You can achieve that by overwriting default accessors as described in the documentation. All column values are automatically available through basic accessors on the Active Record object, but sometimes you want to specialize this behavior. This can be done by overwriting the default accessors (using the same name as the attribute) and calling read_attribute(attr_name) and write_attribute(attr_name, value) to actually change things.
Scroll to the Overwriting default accessors section for more info.