How to store different site parameters in database with Rails? - ruby-on-rails

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

Related

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.

What is the best way to obfuscate numerical IDs in an application

Given I've got a site where most of the resources have numerical IDs (i.e. user.id question.id etc.) but that like the Germans looking back on WWII I'd rather not reveal these to the observers, what's the best way to obfuscate them?
I presume the method is going to involve the .to_param and then some symmetric encryption algorithm but I'm not sure what's the most efficient encryption to do and how it'll impact lookup times in the DB etc.
Any advice from the road trodden would be much appreciated.
I published a Rails plugin that does this called obfuscate_id. I didn't need it to be secure, but just to make the id in the url non-obvious to the casual user. I also wanted it to look cleaner than a long hash.
It also has the advantage of needing no migrations or database changes. It's pretty simple.
Just add the gem to your Gemfile:
gem 'obfuscate_id'
And add call the obfuscate id in your model:
class Post < ActiveRecord::Base
obfuscate_id
end
This will create urls like this:
# post 7000
http://example.com/posts/5270192353
# post 7001
http://example.com/posts/7107163820
# post 7002
http://example.com/posts/3296163828
You also don't need to look up the records in any special way, ActiveRecord find just works.
Post.find(params[:id])
More information here:
https://github.com/namick/obfuscate_id
I usually use a salted Hash and store it in the DB in an indexed field. It depends on the level of security you expect, but I use one salt for all.
This method makes the creation a bit more expensive, because you are going to have an INSERT and an UPDATE, but your lookups will be quite fast.
Pseudo code:
class MyModel << ActiveRecord::Base
MY_SALT = 'some secret string'
after_create :generate_hashed_id
def to_param
self.hashed_id
end
def generate_hashed_id
self.update_attributes(:hashed_id => Digest::SHA1.hexdigest("--#{MY_SALT}--#{self.id}--"))
end
end
Now you can look up the record with MyModel.find_by_hashed_id(params[:id]) without any performance repercussions.
Here's a solution. It's the same concept as Wukerplank's answer, but there's a couple of important differences.
1) There's no need to insert the record then update it. Just set the uuid before inserting by using the before_create callback. Also note the set_uuid callback is private.
2) There's a handy library called SecureRandom. Use it! I like to use uuid's, but SecureRandom can generate other types of random numbers as well.
3) To find the record use User.find_by_uuid!(params[:id]). Notice the "!". That will raise an error if the record is not found just like User.find(params[:id]) would.
class User
before_create :set_uuid
def to_param
uuid
end
private
def set_uuid
self.uuid = SecureRandom.uuid
end
end
Hashids is a great cross-platform option.
You can try using this gem,
https://github.com/wbasmayor/masked_id
it obfuscates your id and at the same time giving each model it's own obfuscated code so all no. 1 id won't have the same hash. Also, it does not override anything on the rails side, it just provides new method so it doesn't mess up your rails if your also extending them.
Faced with a similar problem, I created a gem to handle the obfuscation of Model ids using Blowfish. This allows the creation of nice 11 character obfuscated ids on the fly. The caveat is, the id must be within 99,999,999, e.g. a max length of 8.
https://github.com/mguymon/obfuscate
To use with Rails, create an initializer in config/initializers with:
require 'obfuscate/obfuscatable'
Obfuscate.setup do |config|
config.salt = "A weak salt ..."
end
Now add to models that you want to be Obfuscatable:
class Message < ActiveRecord::Base
obfuscatable # a hash of config overrides can be passed.
end
To get the 11 character obfuscated_id, which uses the Blowfish single block encryption:
message = Message.find(1)
obfuscated = message.obfuscated_id # "NuwhZTtHnko"
clarified = message.clarify_id( obfuscated ) # "1"
Message.find_by_obfuscated_id( obfuscated )
Or obfuscate a block of text using Blowfish string encryption, allowing longer blocks of text to be obfuscated:
obfuscated = message.obfuscate( "if you use your imagination, this is a long block of text" ) # "GoxjVCCuBQgaLvttm7mXNEN9U6A_xxBjM3CYWBrsWs640PVXmkuypo7S8rBHEv_z1jP3hhFqQzlI9L1s2DTQ6FYZwfop-xlA"
clarified = message.clarify( obfuscated ) # "if you use your imagination, this is a long block of text"

Rename ActiveResource properties

I am consuming JSON data from a third party API, doing a little bit of processing on that data and then sending the models to the client as JSON. The keys for the incoming data are not named very well. Some of them are acronyms, some just seem to be random characters. For example:
{
aikd: "some value"
lrdf: 1 // I guess this is the ID
}
I am creating a rails ActiveResource model to wrap this resource, but would not like to access these properties through model.lrdf as its not obvious what lrdf really is! Instead, I would like some way to alias these properties to another property that is named better. Something so that I can say model.id = 1 and have that automatically set lrdf to 1 or puts model.id and have that automatically return 1. Also, when I call model.to_json to send the model to the client, I dont want my javascript to have to understand these odd naming conventions.
I tried
alias id lrdf
but that gave me an error saying method lrdf did not exist.
The other option is to just wrap the properties:
def id
lrdf
end
This works, but when I call model.to_json, I see lrdf as the keys again.
Has anyone done anything like this before? What do you recommend?
Have you tried with some before_save magic? Maybe you could define attr_accessible :ldrf, and then, in your before_save filter, assign ldrf to your id field. Haven't tried it, but I think it should works.
attr_accessible :ldrf
before_save :map_attributes
protected
def map_attributes
{:ldrf=>:id}.each do |key, value|
self.send("#{value}=", self.send(key))
end
end
Let me know!
You could try creating a formatter module based on ActiveResource::Formats::JsonFormat and override decode(). If you had to update the data, you'd have to override encode() also. Look at your local gems/activeresource-N.N.N/lib/active_resource/formats/json_format.rb to see what the original json formatter does.
If your model's name is Model and your formatter is CleanupFormatter, just do Model.format = CleanupFormatter.
module CleanupFormatter
include ::ActiveResource::Formats::JsonFormat
extend self
# Set a constant for the mapping.
# I'm pretty sure these should be strings. If not, try symbols.
MAP = [['lrdf', 'id']]
def decode(json)
orig_hash = super
new_hash = {}
MAP.each {|old_name, new_name| new_hash[new_name] = orig_hash.delete(old_name) }
# Comment the next line if you don't want to carry over fields missing from MAP
new_hash.merge!(orig_hash)
new_hash
end
end
This doesn't involve aliasing as you asked, but I think it helps to isolate the gibberish names from your model, which would never have to know those original names existed. And "to_json" will display the readable names.

Serialization to blob in rails 3

I have an rails app where one of the attributes on an object is a data set which consists of an array of x,y coordinates. I am currently storring this in the sql database using the rails serialize helper :
serialize :data, Array
This converts the array to yaml and then stores it in a string field in the sql database. The problem is that our database is getting really big doing this and we need to keep it smaller. Is it possible to serialize to raw binary instead of a string and store in a blob?, this would dramatically reduce the size and help our problem.
I have had a search for a gem to do this, or even a ruby method that will turn an array in to binary data without much help. Any suggestions would be appreciated.
You may be interested in Array.pack and String.unpack methods. See ruby documentation for it: type ri Array.pack
You may want to use a 'packed_data' attribute in your database, then add accessors to pack/unpack it:
def data
packed_data.unpack('....')
end
def data=(v)
self.packed_data = v.pack('....')
end
To make it more useful, you may store the unpacked form in a variable, but you have to remember to clear it when the packed_data attribute changes, like when you call .reload
before_validation :pack_data
UNPACK_FORMAT = '.....' # See ri Array.pack
def data
#data ||= packed_data.unpack(UNPACK_FORMAT)
end
def data=(v)
#data = v
end
def reload(options=nil)
#data = nil
super
end
def pack_data
self.packed_data = self.data.pack(UNPACK_FORMAT)
true # Because we are in a before_.. callback
end
The format of the magic string used to pack/unpack the data depends on the data you have in your array. The documentation will help you to choose the right one.
I believe the format for pack and unpack will be the same, but don't trust me too much. ;)

Resources