Creating tables & models on the fly (dynamically) in Ruby on Rails 3 - ruby-on-rails

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

Related

Rails Ruby easiest way to get table names of my database [duplicate]

How do I get a list of all the tables defined for the database when using active record?
Call ActiveRecord::ConnectionAdapters::SchemaStatements#tables. This method is undocumented in the MySQL adapter, but is documented in the PostgreSQL adapter. SQLite/SQLite3 also has the method implemented, but undocumented.
>> ActiveRecord::Base.connection.tables
=> ["accounts", "assets", ...]
See activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb:21, as well as the implementations here:
activerecord/lib/active_record/connection_adapters/mysql_adapter.rb:412
activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:615
activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb:176
Based on the two previous answers, you could do:
ActiveRecord::Base.connection.tables.each do |table|
next if table.match(/\Aschema_migrations\Z/)
klass = table.singularize.camelize.constantize
puts "#{klass.name} has #{klass.count} records"
end
to list every model that abstracts a table, with the number of records.
An update for Rails 5.2
For Rails 5.2 you can also use ApplicationRecord to get an Array with your table' names. Just, as imechemi mentioned, be aware that this method will also return ar_internal_metadata and schema_migrations in that array.
ApplicationRecord.connection.tables
Keep in mind that you can remove ar_internal_metadata and schema_migrations from the array by calling:
ApplicationRecord.connection.tables - %w[ar_internal_metadata schema_migrations]
It seems like there should be a better way, but here is how I solved my problem:
Dir["app/models/*.rb"].each do |file_path|
require file_path # Make sure that the model has been loaded.
basename = File.basename(file_path, File.extname(file_path))
clazz = basename.camelize.constantize
clazz.find(:all).each do |rec|
# Important code here...
end
end
This code assumes that you are following the standard model naming conventions for classes and source code files.
Don't know about active record, but here's a simple query:
select table_name
from INFORMATION_SCHEMA.Tables
where TABLE_TYPE = 'BASE TABLE'

How to avoid duplicates from saving in database parsed from external JSON file with sidekiq in Rails

I have a small To Do list in a .json file that I´m reading, parsing, and saving to a rails app with Sidekiq. Everytime I refresh the browser, the worker executes and duplicates the entries on the database. How do I maintain a unique database that is synchronized with the .json file and avoid duplicate entries to save on my database and show on my browser?
Here's the worker:
class TodoWorker
include Sidekiq::Worker
def perform
json_text = File.read('todo_json.json')
json = JSON.parse(json_text, :headers => true)
json.each do |todo|
t = TodoList.create(name: todo["name"], done: todo["done"])
t.save
end
end
end
And the controller:
class TodoListsController < ApplicationController
def index
#todo_lists = TodoList.all
TodoWorker.perform_async
end
end
Thanks
This is a terrible solution btw, you have a huge race condition in your read/store code, and you're not going to be able to use a large part of what Rails is good at. If you want a simple DB why not just use sqlite?
That being said, you need some way of recognizing duplicates, in most DBs this is done with a primary key that is sent to the browser along with the rest of the data, and then back again with any changes. That primary key is used to ensure that existing data is updated, rather than duplicated.
You will need the same thing in your JSON file, and then you can change your create method to be something more like ActiveRecord's find_or_create_by

Random selecting for different databases in RoR

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.

Having lowercase method names with mixed case column names

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.

Creating a multi-tenant application using PostgreSQL's schemas and Rails

Stuff I've already figured out
I'm learning how to create a multi-tenant application in Rails that serves data from different schemas based on what domain or subdomain is used to view the application.
I already have a few concerns answered:
How can you get subdomain-fu to work with domains as well? Here's someone that asked the same question which leads you to this blog.
What database, and how will it be structured? Here's an excellent talk by Guy Naor, and good question about PostgreSQL and schemas.
I already know my schemas will all have the same structure. They will differ in the data they hold. So, how can you run migrations for all schemas? Here's an answer.
Those three points cover a lot of the general stuff I need to know. However, in the next steps I seem to have many ways of implementing things. I'm hoping that there's a better, easier way.
Finally, to my question
When a new user signs up, I can easily create the schema. However, what would be the best and easiest way to load the structure that the rest of the schemas already have? Here are some questions/scenarios that might give you a better idea.
Should I pass it on to a shell script that dumps the public schema into a temporary one, and imports it back to my main database (pretty much like what Guy Naor says in his video)? Here's a quick summary/script I got from the helpful #postgres on freenode. While this will probably work, I'm gonna have to do a lot of stuff outside of Rails, which makes me a bit uncomfortable.. which also brings me to the next question.
Is there a way to do this straight from Ruby on Rails? Like create a PostgreSQL schema, then just load the Rails database schema (schema.rb - I know, it's confusing) into that PostgreSQL schema.
Is there a gem/plugin that has these things already? Methods like "create_pg_schema_and_load_rails_schema(the_new_schema_name)". If there's none, I'll probably work at making one, but I'm doubtful about how well tested it'll be with all the moving parts (especially if I end up using a shell script to create and manage new PostgreSQL schemas).
Thanks, and I hope that wasn't too long!
Update Dec 5, 2011
Thanks to Brad Robertson and his team, there's the Apartment gem. It's very useful and does a lot of the heavy lifting.
However, if you'll be tinkering with schemas, I strongly suggest knowing how it actually works. Familiarize yourself with Jerod Santo's walkthrough , so you'll know what the Apartment gem is more or less doing.
Update Aug 20, 2011 11:23 GMT+8
Someone created a blog post and walks though this whole process pretty well.
Update May 11, 2010 11:26 GMT+8
Since last night I've been able to get a method to work that creates a new schema and loads schema.rb into it. Not sure if what I'm doing is correct (seems to work fine, so far) but it's a step closer at least. If there's a better way please let me know.
module SchemaUtils
def self.add_schema_to_path(schema)
conn = ActiveRecord::Base.connection
conn.execute "SET search_path TO #{schema}, #{conn.schema_search_path}"
end
def self.reset_search_path
conn = ActiveRecord::Base.connection
conn.execute "SET search_path TO #{conn.schema_search_path}"
end
def self.create_and_migrate_schema(schema_name)
conn = ActiveRecord::Base.connection
schemas = conn.select_values("select * from pg_namespace where nspname != 'information_schema' AND nspname NOT LIKE 'pg%'")
if schemas.include?(schema_name)
tables = conn.tables
Rails.logger.info "#{schema_name} exists already with these tables #{tables.inspect}"
else
Rails.logger.info "About to create #{schema_name}"
conn.execute "create schema #{schema_name}"
end
# Save the old search path so we can set it back at the end of this method
old_search_path = conn.schema_search_path
# Tried to set the search path like in the methods above (from Guy Naor)
# [METHOD 1]: conn.execute "SET search_path TO #{schema_name}"
# But the connection itself seems to remember the old search path.
# When Rails executes a schema it first asks if the table it will load in already exists and if :force => true.
# If both true, it will drop the table and then load it.
# The problem is that in the METHOD 1 way of setting things, ActiveRecord::Base.connection.schema_search_path still returns $user,public.
# That means that when Rails tries to load the schema, and asks if the tables exist, it searches for these tables in the public schema.
# See line 655 in Rails 2.3.5 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
# That's why I kept running into this error of the table existing when it didn't (in the newly created schema).
# If used this way [METHOD 2], it works. ActiveRecord::Base.connection.schema_search_path returns the string we pass it.
conn.schema_search_path = schema_name
# Directly from databases.rake.
# In Rails 2.3.5 databases.rake can be found in railties/lib/tasks/databases.rake
file = "#{Rails.root}/db/schema.rb"
if File.exists?(file)
Rails.logger.info "About to load the schema #{file}"
load(file)
else
abort %{#{file} doesn't exist yet. It's possible that you just ran a migration!}
end
Rails.logger.info "About to set search path back to #{old_search_path}."
conn.schema_search_path = old_search_path
end
end
Change line 38 to:
conn.schema_search_path = "#{schema_name}, #{old_search_path}"
I presume that postgres is trying to lookup existing table names when loading schema.rb and since you've set the search_path to only contain the new schema, it fails. This of course, is presuming you still have the public schema in your database.
Hope that helps.
Is there a gem/plugin that has these things already?
pg_power provides this functionality to create/drop PostgreSQL schemas in migration, like this:
def change
# Create schema
create_schema 'demography'
# Create new table in specific schema
create_table "countries", :schema => "demography" do |t|
# columns goes here
end
# Drop schema
drop_schema 'politics'
end
Also it takes care about correctly dumping schemas into schema.rb file.

Resources