I need to connect to a legacy SQL Server 2000 database using their own conventions and specially CamelCase columns and Tables.
For tables it seems fine, Rails is asking it with lowercase and the database find it nicely. The issue is with the columns because Rails fetch their name with SQL and thus get whatever case their name is.
I'm dealing with 500+ tables with some dozen columns in each of them and several legacy applications running in production above them so renaming the columns is no solution.
Using alias_attribute is also a way-too-much-work solution.
I don't want to have some weird case in my code too like client.AccountId (just looks like Java code).
So my final question is: is there any way to have Rails dealing with lowercase methods and symbols which are then used in whatever-case the database uses when dealing with SQL ?
I'm looking for any existing solution or even a direction to the sensible area of ActiveRecord where all this mechanics is done (I've been searching but the source code is huge ...)
OKay some time after posting the question I had a flash idea that alias_attribute was actually the solution but just needed a bit of magic over it.
Here is the solution to my own problem:
module LegacyDatabase
module ClassMethods
def aliased_attributes
#aliased_attributes ||= {}
end
def alias_attribute(new_name, old_name)
self.aliased_attributes[new_name.to_sym] = old_name.to_sym
super(new_name, old_name)
end
end
module InstanceMethods
private
def read_attribute(attr_name)
attr_name = self.class.aliased_attributes[attr_name.to_sym] if self.class.aliased_attributes.has_key?(attr_name.to_sym)
super(attr_name)
end
def write_attribute(attr_name, value)
attr_name = self.class.aliased_attributes[attr_name.to_sym] if self.class.aliased_attributes.has_key?(attr_name.to_sym)
super(attr_name, value)
end
end
def self.included(base)
base.instance_eval do
extend(ClassMethods)
include(InstanceMethods)
end
base.columns.each do |column|
legacy_name = column.name
rails_name = column.name.underscore
if legacy_name != rails_name
base.alias_attribute rails_name, legacy_name
end
end
end
end
I think this is the minimum code modification possible to avoid messing all ActiveRecord code. I'd like your opinion on this and your comments if you see a wall I'm going to hit and I don't !
To describe the solution, I'm using the columns method of ActiveRecord to generate snake_case looking aliases for each column. I'm also giving alias_column a memory of the aliases, that way read and write attribute methods know when they are dealing with alias names.
Since in my legacy database the convention for the ID or the table Table is TableID, my solution will create a table_id alias found by ActiveRecord using the "table_name_with_underscore" convention, so the id method is working as expected.
I presume it's not going to work with all the SQL fetches, even with Squeel of something but I don't think there is any simple solution for this.
Related
I used to do this with an array condition inside the where method:
Article.where('title ILIKE ?','%today%')
This worked in Postgres but ILIKE is not present in MySQL and other DBMS.
What I need is to be able to perform case insensitive queries using a code like
Article.ilike(title:'%today%',author:'%john%')
Even if there's not builtin method to perform case insensitive queries, you can use the Arel library and the matches method, like in:
Article.where(Article.arel_table[:title].matches('%today%'))
This is DB agnostic and SQL Injection proof.
I've written an ilike method in my common scope file, that allows you to call it with a list of attributes and values, that's it:
module CommonScopes
extend ActiveSupport::Concern
module ClassMethods
def ilike( options={} )
raise ArgumentError unless options.is_a? Hash or options.empty?
if options.one?
where(arel_table[options.keys.first].matches(options.values.first))
else
key, value = options.shift
ilike( {key=>value} ).merge( ilike( options ) )
end
end
end
end
You can place this inside app/models/concerns/common_scopes.rb and include where you need it.
No, there isn't. You need to write driver-specific SQL to achieve this.
ActiveRecord's goal is to make database access fast and easy for 90% of usecases, not to make your models completely database-agnostic. Switching your entire database backend from one system to another is not something they optimize for.
You might consider looking at another gem like DataMapper which provides a Ruby-syntax for wrapping things like like (but which may or may not provide an equivalent to ilike):
# If the value of a pair is an Array, we do an IN-clause for you.
Person.all(:name.like => 'S%', :id => [ 1, 2, 3, 4, 5 ])
Rails don't have the direct case sensitive search. It's dependent on the DB level. For MySQL you can use LOWER method.
YourModel.where('lower(column_name) = ?', str.downcase)
I need to select random records from db. In Sqlite3, which I use on development, there is a function called Random(). However, in Postgresql it's called Rand(). I don't remember about MySql, but probably it's called so there.
So if I have a code of (for Sqlite3)
data = Items.where(pubshied: is_pubshied).order("RANDOM()").limit(count)
how do I ensure that it will work with different databases?
Rails doesn't support this out of the box. I believe I achieved this with a model extension (I dont use it anymore because I force the use of Postgresql), but something like this could work:
module Randomize
extend ActiveSupport::Concern
included do
scope :random, -> { order(rand_cmd) }
end
module ClassMethods
def rand_cmd
if connection.adapter_name =~ /mysql/i
'rand()'
else
'random()'
end
end
end
end
You can then do
class Item
include Randomize
end
Item.where(...).random.limit(...)
For a performant, non-adapter-specific way to order randomly, populate a random column, put an index on it and call it something like:
Foo.order("random_column > #{rand}").limit(1)
From the comments from the post that waldyr.ar mentions in his comment: https://stackoverflow.com/a/12038506/16784.
Tl;dr: you can use Items.all.sample(count). Of course that retrieves the entire table and may not be useful for large tables.
I want to use Cuisines like (Chinese, Indian, US) as constant values in my application which are defined in a config file. How can I set as constants and how can access in controllers?
This is explicitly not an answer to your question, but a suggestion that you look for alternatives. I think you would be far better off creating a database table with your cuisine names in it than to use constants. Leverage rails associations so that you can write nice readable code.
The problem with using constants is that under many circumstances, they aren't really constant. What happens if you want to add Japanese? What happens if you want to add Thai, but then 6 months later decide to drop it? What happens if you decide that Indian is too broad, and you want "Northern Indian" and "Southern Indian"?
With a database table, you can ensure that the class that are associated with those constants are always in a consistent state. When you need to get them all, they are just a line of code away with
my_cuisines = Cuisine.all
with nice built in iterators.
You can use gem 'settingslogic'
model settings.rb:
class Settings < Settingslogic
source "#{Rails.root}/config/settings.yml"
namespace Rails.env
end
then, use in controller:
Settings.cousines
First, consider what Marc Talbot said. Make sure that you really don't want a normal database model. If you're sure you want to use constants then continue on:
My preferred way to do this is with a pseudo-model.
In app/models/cuisine.rb
class Cuisine
# Should come before the constant declarations
def initialize(name)
#name = name
end
Mexican = new('Mexican')
Chinese = new('Chinese')
Indian = new('Indian')
def to_s
name
end
# other related methods
# like translations, descriptions, etc.
end
Then in the everywhere else in the app you can just reference Cuisine::Mexican or Cuisine::Indian
Also depending on how you are using it you might need a list of the cuisines.
class Cuisine
...
def self.all
[Mexican, Indian, Chinese, ...]
end
end
This technique keeps the code organized and keeps you from writing yet another initializer file.
Bear with me on this one.
In an app I'm working on users are able to upload CSV files into the system, with any headers they like and any columns in the data. The CSV is then used to generate a table in the database and the data written to it, it can then be accessed through the system for various uses, searches, sorts updates etc.
The old (and now defunct system) was in PHP and handled this fine, although quite messily with lots of raw sql to create the tables and the framework supported magic-models (if the table existed so did the object without a class being defined in a model file)
The new version is being written in RoR3, and I am yet to figure out a way to do this. I've managed to sort out the table creation by calling migration tools inside a model (not very Rails-y I know, but needs must...), but I cannot find a way to link to the new table once it's created to write in the data, build relationships or anything else.
What I'm hoping for is either,
a) someone on here has a better way of doing this than creating tables and models on the fly (a warning here, these files can contain 100'000's of records and different fields so a single table option doesn't work so well) i.e a better database design for this issue.
or
b) can tell me how to sort out the model issue.
I've looked at Dr Nic's Magic Model gem for RoR but it doesn't seem to work in RoR3 unless I'm doing something wrong
Sorry for the wall of text, look forward to any suggestions
Thanks in advance
OK, i got a solution i think, but if is nice thats different thing.
Basically you create a table on the fly by executing SQL thru Rail ActiveRecord.
Next use a Model and change its name (Model.table_name).
Something like this:
// SQL Create table
sql = "CREATE TABLE my_table (id int(11) NOT NULL AUTO_INCREMENT, code varchar(3) NOT NULL)"
ActiveRecord::Base.connection.execute(sql)
Then with a model you can change the table name on the fly, like:
MyModel.table_name = "my_table"
records = MyModel.all
Ok, one of your problems is the model logic & associations. You are kind of limited, but maybe you can workaround that.
Not really best practices i guess, but if you need this. I think it might work!
I have implemented app which is used to upload a csv file and convert the file into a active record model. you can checkout this repository.
Visit https://github.com/Athul92/Salary_Sheet_Comparison/blob/master/app/models/makemodel.rb
here is a small idea about how it was achieved:
class Makemodel < ActiveRecord::Migration
def self.import(file,project_name,file_name,start_row,end_row,unique,last_column)
spreadsheet = open_spreadsheet(file)
header = spreadsheet.row(start_row.to_i)
i=0
header.count.times do
unless header[i].nil?
header[i]= header[i].gsub(/[^0-9A-Za-z]/, '').downcase
end
i+=1
end
name = "#{project_name.downcase}"+"#{file_name.downcase}"
create_table name.pluralize do |t|
header.each do |head|
t.string head
end
end
model_file = File.join("app", "models", name.singularize+".rb")
model_name = name.singularize.capitalize
File.open(model_file, "w+") do |f|
f << "class #{model_name} < ActiveRecord::Base\nend"
end
((start_row.to_i+1)..end_row.to_i).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
#should find a logic to find the model class that is being created
product = Object.const_get(name.capitalize).new
product.attributes = row.to_hash
product.save!
end
end
def self.open_spreadsheet(file)
case File.extname(file.original_filename)
when ".csv" then Csv.new(file.path, nil, :ignore)
when ".xls" then Roo::Excel.new(file.path)
when ".xlsx" then Roo::Excelx.new(file.path)
else raise "Unknown file type: #{file.original_filename}"
end
end
end
There are also some glitches with this it is difficult to add association and also validations
There are a lot of ways to store site preferences in database. But what if I need to manage datatypes. So some preferences will be boolean, others strings, others integers.
How can I organize such store?
I wrote a gem that does exactly this, and recently updated it for Rails 3:
For Rails 3:
http://github.com/paulca/configurable_engine
For Rails 2.3.x
http://github.com/paulca/behavior
Enjoy!
I am quite lazy with preferences and store the data as serialized JSON or YAML Hashes. Works really well, and generally preserves the data types as well.
I used a single table with a single row, and each column representing one preference. This makes it possible to have different datatypes.
To be able to retrieve a preference, I overrode method_missing to be able to retrieve the preference value directly from the class name without requiring an instance, something like this:
class Setting < ActiveRecord::Base
##instance = self.first
def self.instance
##instance
end
def self.method_missing(method, *args)
option = method.to_s
if option.include? '='
var_name = option.gsub('=', '')
value = args.first
##instance[var_name] = value
else
##instance[option]
end
end
end
Thus, to retrive a setting, you would use:
a_setting = Setting.column_name
Rails Migrations are used to create and update the database.
http://guides.rubyonrails.org/migrations.html