Field aliasing in Mongoid - ruby-on-rails

Does anyone know if Mongoid has built in support to alias field names? I've been running some tests where I have a collection with a minimal amount of fields (7 fields). If I use descriptive names and load real data and then use greatly shortened names and load the same real world data I see a 40% reduction in the total size of my collection. In looking at other drivers for MongoDB (non Ruby) I see that some of them have built in support where you can write code against a descriptive name but the persistence is smart enough to use developer defined aliases. I'm just trying to determine if Mongoid has anything similar.

actually the best way to do this is NOT using ruby alias but:
field :fn, :as => :firstname
as outlined here: http://groups.google.com/group/mongoid/browse_thread/thread/ce3298d6a167bd70

Actually, the following must also be included for passing a hash to new or update_attributes:
alias :filtered_process :process
def process(attrs = nil)
attrs[:fn] = attrs[:first_name] unless attrs.nil?
filtered_process(attrs)
end
This allows your alias to be mapped to the field on the create or update calls. It would be trivial to then track alias to field mappings in order to abstract this process.

According to this answer, you should be able to just use alias like this:
class Foo
include Mongoid::Document
field :fn, :type => String
alias :first_name :fn
end
Keep in mind that this will not let you use the alias in queries and may cause some bugs. It should be fine for simple usage in instance methods and views.

Related

How to choose a continent by the country?

I use Ruby on Rails 5.2 and mongoid 7.0
I need to choose a continent by the country
I understand that it should look something like this:
class Place
field :country, type: String
field :continent, type: String
after_save :update_continent
def update_continent
cont = self.country
case cont
when 'United States', 'Grenada'
'NA'
when 'Netherlands', 'Spain'
'EU'
end
self.continent = cont
end
end
Since you indicated you are using Mongoid:
Each Mongoid model class must include Mongoid::Document, per the documentation in https://docs.mongodb.com/mongoid/master/tutorials/mongoid-documents/.
after_save callbacks are normally used for things like creating external jobs, not for setting attributes, because the attribute changes won't be persisted (as the model was already saved). Usually attribute changes are done in before_validation or before_save callbacks. See https://guides.rubyonrails.org/active_record_callbacks.html for the list of available callbacks.
As pointed out by Toby, the case statement is not correctly used. Its result should be assigned like this:
.
def update_continent
self.continent = case self.country
when 'United States', 'Grenada'
'NA'
when 'Netherlands', 'Spain'
'EU'
end
end
You haven't given enough context to be able to answer your question, but since you just want to be pointed in the right direction, and since you seem to be new here I'm happy to give you some pointers.
You're class uses the after_save method as if it is an ActiveRecord Model, but without extending or including anything it's just a Plain Old Ruby Object. To make the after_save callback work you need to at least extend ActiveModel::Callbacks but probably you want to make it a full ActiveRecord Model. To do that in Rails 4 you subclass ActiveRecord::Base and in rails 6 you subclass ApplicationRecord But I don't actually know how it's done in Rails 5.
If you have a normal database in the back end as is usual for rails you don't need to declare the fields, it automatically gets them from the equivalent table in the database (though perhaps this is not true when using Mongoid. I don't know). if you run this command in your terminal in your app base directory: rails generate model Place country:string continent:string it will create the migration file needed to make the database table and the Model file (with whatever the correct superclass is) and you wont need to do all the boilerplate stuff yourself.
You have a variable named cont and you assign a country to it. This will get very confusing given that you also have a separate concept of "continent" Better to not abbreviate your variable names and choose sensible naming.
You're not using the case statement correctly. The output of the statement doesn't automatically get assigned to the the variable you're switching on. You need to read up on Ruby syntax.
Overall I suspect in the long run you would do well to have separate models for Continent and Country. With a Continent having many countries and a country belonging to a continent. Rails is a framework that makes that sort of thing very easy to do and manage. You probably need to read some more and look at examples and videos about the basics of Ruby on Rails.
I highly recommend The Rails Tutorial by Hartl. It's free online. Working through that or an equivalent should give you a much better understanding of how Rails is equipped to handle your situation and how to best utilise it to get the outcome you need. This was indispensable for me when I was first starting out with Rails.

Implementation for Rails model that might not need a database

I'm trying to construct a system in Rails where I've got a Project model with a "type" column, and I'm not sure whether if I should explicitly store the type as a string in the projects table, or if I should store a type_id instead. The thing is, I feel like it would be stupid to create a type model; Types cannot be created or destroyed, there are simply a fixed number of them. But if I don't create a model, the only other way I can think to do it would be to create a Type class in /lib which has a get_name(type_id) method, and this seems like total overkill.
If I decided to just store the string, I'd be using unnecessary space, and filtering by type would be weird.
Any thoughts?
If you're sure the types are a fixed set you can define some numeric constant in the Project model and just store these number in a column of your projects table.
Here an example (not tested of course) where I call the column category_id to avoid to use the type name that would cause problems as rjz said:
class Project < ActiveRecord::Base
# Project categories are constants
CHEAP_PROJECT = 1
SOUND_PROJECT = 2
GRAPHIC_PROJECT = 3
SECRET_PROJECT = 4
# Force project_category_id to be a valid category identifier
validates :category_id, :inclusion => {:in => 1..4}
# At this point you can use the constants
# Here an example of a scope to get the secret projects
scope :secret_projects, where(:category_id => SECRET_PROJECT)
end
Be sure to validate the category_id values to be one of which you defined.
Once you have these constans you can even use from other places using something like Project::SOUND_PROJECT.
I think is a solution pretty clear, but if your requirements change (they always change...) you have to create a model and insert these project categories maintaining these identifiers.

Rails Best Practice for User-Configurable Global Attribute "Defaults"

Sorry about the awkward phrasing of the title -- not quite sure of the best way to title this but here's what I'm seeing assistance with:
In a Rails app, let's say we've got a model for a Product, and one of the attributes of the product is Price.
On the admin side of my app, I'd like to be able to set a "default" price that could be referred to if any new Product created isn't assigned a Price. If a Product does have a value for Price, then it would be used.
This is, of course, an example -- and I ask this question because I've got to imagine this is a common pattern. This can be applied to any resource that might have user-configurable global defaults or resource-specific values.
In pure Ruby, this would be solved, I think, with a class variable, so I'd be able to define ##default_price within the Product class and be able to refer to Product.default_price if the instantiated object's value doesn't exist.
My research here has pointed me towards the rails-settings-cached gem, which would allow for something like MyApp.default_price, but I'm wondering if there's a more elegant (non-plugin) way to accomplish this within the base Rails framework.
Note I'd like to setup this structure in code, but I want to be able to define the actual values through my app (i.e. config files aren't the solution I'm looking for).
Can someone enlighten me with the Rails way of handling this?
ActiveRecord picks up default attribute values from the database schema. However, these are baked into the migration and table schema and not configurable.
If you want configurability, the pattern that I've used is a before_validation callback method to set a value if the attribute is blank, e.g.:
class Product < ActiveRecord::Base
before_validation :set_price_if_blank
validates :price, :presence => true # sanity check in case the default is missing
has_one :price
private
def set_price_if_blank
self.price = Price.default if self.price.blank?
end
end
class Price < ActiveRecord::Base
def self.default
##default ||= Price.where(:default => true).first
end
end
This assumes that your price table is populated with a row that has a default flag. You could achieve this, e.g. through a seeds.rb file. I've added a validation rule to make sure that you still get an error if no default exists. It adds robustness to your application.
Also note that it's best to use Integers or Decimals for price data, not floats. See this answer.

Creating readable models in rails

I have just started with Rails and coming from a .net background I find the model inheriting from ActiveRecord is hard to understand, since the don't contain the corresponding attributes for the model. I cannot imagine a new developer exposed to a large code where the models only contains references to other models and business logic.
From my point of view the DataMapper model is much easier to grasp but since ActiveRecord is the defacto standard it feels weird to change the ORM just for this little problem.
DataMapper
class Post
include DataMapper::Resource
property :id, Serial # An auto-increment integer key
property :title, String # A varchar type string, for short strings
property :body, Text # A text block, for longer string data.
property :created_at, DateTime # A DateTime, for any date you might like.
end
ActiveRecord
class Post < ActiveRecord::Base
end
I'm not sure if this is an issue and that people get used to the models without attributes, or how does experienced rails user handle this?
I don't think using the database manager or looking at loads of migrations scripts to find the attributes is an option?
Specifying attr_accessible will make the model more readable but I'm not sure if it's a proper solution for my problem?
Check out the annotate_models plugin on github. It will insert a commented schema for each model in a comment block. It can be installed to run when migrate is.
You don't have to "look at loads of migration scripts to find the attributes" - they're all defined in one place in db/schema.rb.
A few tips:
Load up the Rails console and enter
Post.column_names for a quick
reminder of the attribute names.
Post.columns gives you the column
objects, which shows the datatypes
db/schema.rb contains all the
migration code in one place, so you
can easily see all the column
definitions.
If you are using a
decent editor/IDE there should be a way to
allowing you to jump from the model file
to the migration file. (e.g. Emacs
with ROR or Rinari)

Best practice for converting integer value column to string representation

Lets say you have a model like the following:
class Stock < ActiveRecord::Base
# Positions
BUY = 1
SELL = 2
end
And in that class as an attribute of type integer called 'position' that can hold any of the above values. What is the Rails best practice for converting those integer values into human readable strings?
a) Use a helper method, but then you're force to make sure that you keep the helper method and model in sync
def stock_position_to_s(position)
case position
when Stock::BUY
'buy'
when Stock::SELL
'sell'
end
''
end
b) Create a method in the model, which sort of breaks a clean MVC approach.
class Stock < ActiveRecord::Base
def position_as_string
...snip
end
end
c) A newer way using the new I18N stuff in Rails 2.2?
Just curious what other people are doing when they have an integer column in the database that needs to be output as a user friendly string.
Thanks,
Kenny
Sounds to me like something that belongs in the views as it is a presentation issue.
If it is used widely, then in a helper method for DRY purposes, and use I18N if you need it.
Try out something like this
class Stock < ActiveRecord::Base
##positions => {"Buy" => 1, "Sell" => 2}
cattr_reader :positions
validates_inclusion_of :position, :in => positions.values
end
It lets you to save position as an integer, as well as use select helpers easily.
Of course, views are still a problem. You might want to either use helpers or create position_name for this purpose method
class Stock < ActiveRecord::Base
##positions => {"Buy" => 1, "Sell" => 2}
cattr_reader :positions
validates_inclusion_of :position, :in => positions.values
def position_name
positions.index(position)
end
end
Is there a good reason for the app be converting the integer to the human readable string programmatically?
I would make the positions objects which have a position integer attribute and a name attribute.
Then you can just do
stock.position.name
#HermanD: I think it's a lot better to store the values in an integer column rather than a string column for numerous reasons.
It saves database space.
Easier/faster to index on an integer than a string.
Your not hard coding a human readable string as values in a database. (What happens if the client says that "Buy" should become "Purchase"? Now the UI shows "Purchase" everywhere but you need to keep setting "Buy" in the database.)
So, if you store certain values in the database as integers, then at some point, you're going to need to show them to the user as strings, and I think the only way you can do that is programatically.
You could move this info into another object but, IMHO, I'd say this is overkill. You'd then have to add another database table. Add another 'admin' section for adding, removing and renaming these values and so on. Not to mention that if you had several columns, in different models that needed this behavior, you'd either have to create lots of these objects (ex: stock_positions, stock_actions, transaction_kinds, etc...) or you'd have to design it generically enough to use polymorphic associations. Finally, if the position name is hard coded, then you lose the ability to easy localize it at a later date.
#frankodwyer: I'd have to agree that using a helper method is probably the best way to go. I was hoping their might be a "slicker" way to do this, but it doesn't look like it. For now, I think the best method is to create a new helper module, maybe something like StringsHelper, and stuff a bunch of methods in their for converting model constants to strings. That way I can use all the I18N stuff in the helper to pull out the localized string if I need to in the future. The annoying part is that if someone needs to add a new value to the models column, then they will also have to add a check for that in the helper. Not 100% DRY, but I guess "close enough"...
Thanks to both of you for the input.
Kenny
Why not use the properties of a native data structure? example:
class Stock < ActiveRecord::Base
ACTIONS = [nil,'buy','sell']
end
Then you could grab them using Stock::ACTIONS[1] #=> 'buy' or Stock::ACTIONS[2] #=> 'sell'
or, you could use a hash {:buy => 1, :sell => 2} and access it as Stock::ACTIONS[:buy] #=> 1
you get the idea.
#Derek P. That's the implementation I first went with and while it definitely works, it sort of breaks the MVC metaphor because the model, now has view related info defined in its class. Strings in controllers are one thing, but strings in models (in my opinion) are definitely against the spirit of clean MVC.
It also doesn't really work if you want to start localizing, so while it was the method I originally used, I don't think it's the method for future development (and definitely not in an I18N world.)
Thanks for the input though.
Sincerely,
Kenny
I wrote a plugin that may help a while ago. See this. It lets you define lists and gives you nice methods ending in _str for display purposes.

Resources