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
Related
I was under the impression that we could give Rails a model (anything responding to to_param or cache_key) to Rails.cache.fetch and it would create a key and cache the response of the block.
I have this code:
class ResultsPresenter
def initialize(project)
#project = project
end
def results
results = Rails.cache.fetch("#{#project}/results") do
sleep 3
a = long_running_query
b = another_long_query
c = a + b
end
end
end
# called
project = Project.find(params[:project_id]_
presenter = ResultsPresenter.new(project)
presenter.results
#project was passed to ResultsPresenter and is an ActiveRecord model. When I specify "#{#project.to_param}/results" or "#{#project.cache_key}/results" everything works just fine. I also checked if the #project was being updated but it's not.
Anyone know why it does not take an ActiveRecord model?
You want your cache key to be an array, probably Rails.cache.fetch([#project, 'results']).
This will give a cache key along the lines of "project/5-20190812000000/results". The format for the model is "model_name/model_id-updated_at" with the rest of the array values joined with /.
If you were to look at the key generated from your example, it would look something like "#<Project:0x007fbceaadbbc90>/results". This is happening because you are baking the value of #project.to_s into the value of the key you are passing into fetch.
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'm trying to do something like:
if filter_1
#field = #field.where()
else
#field = #field.where()
end
if filter_2
#field = #field.order()
end
etc.
But how do I init #field with an empty query? I tried #field = Field.all but that gives an array so not allowing chaining.
Try scopedon Model class e.g.
#fields = Field.scoped
#fields = #fields.where("your conditions") if filter_1
#fields = #fiels.order("your conditions") if filter_2
The first time you are initializing the #field instance variable, Please try referring to the class Field, i.e.
Filter1: #field = Field.where(...)
Afterwards if you need to keep adding further filters you can refer to your variable field as many times as you want to.
Filter2 onward: #field = #field.where(...)
As Filter1 would return an active Record relation, you can nest more condition clauses onto it. Also do not worry about performance issues as the SQL will only be generated and processed once it is actually needed.(lazy loading)
If you to #field.to_sql at the end of your filters, you'll be able to see that all of your where clauses have conveniently been nested together into one SQL statement.
Also, I'd recommend you to read Active Record Query Interface
EDIT
Create a method get_field. And use that to add filter results.
def get_field(field)
#field.is_a?(ActiveRecord::Relation) ? Field : field
end
get_field(#field).where(....)
get_field(#field).where(....)
For instance, I have a table of text_fields like this:
Entry 1 | Value (with flag=TRUE) | Value(with flag=FALSE)
Entry 2 ...
.
.
.
I need to be able to assign the "Value" whether it is in the left or right hand column (and set the corresponding flag).
Then on that same row if one column has an entry, then the other column should be grayed out (otherwise it would overwrite the other one).
I'd do this with extra, non-DB attributes on the model. Something like this:
class MyModel < ActiveRecord
attr_accessor :val1, :val2
def val1=(value)
self.real_value = value # Make sure your real database column is updated
self.the_flag = true
end
def val1
the_flag ? real_value : nil # Return real database column when asked
end
def val2=(value)
self.real_value = value # Make sure your real database column is updated
self.the_flag = false
end
def val2
the_flag ? nil : real_value # Return real database column when asked
end
Then in your view, you hook up to val1 and val2 instead of the real column and use your flag to determine what's grayed out.
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.