Should I be using callbacks or should I override attributes? - ruby-on-rails

What is the more "rails-like"? If I want to modify a model's property when it's set, should I do this:
def url=(url)
#remove session id
self[:url] = url.split('?s=')[0]
end
or this?
before_save do |record|
#remove session id
record.url = record.url.split('?s=')[0]
end
Is there any benefit for doing it one way or the other? If so, why? If not, which one is generally more common?

Obviously these two have different use-cases.
The first one should be done if you need to access the modified attribute before the record is saved. For example, you want to set the url and at once check the modified value against some condition before saving it to DB.
The second one fits if you only want to do something with the attribute just before saving to the database. So if you access it between the moment of setting and the moment of saving, you'll get the unmodified value.

Related

Pass object or id

This is just a question about best practices.
Imagine you have a method that takes one parameter. This parameter is the id of an object. Ideally, I would like to be able to pass either the object's id directly, or just the object itself.
What is the most elegant way to do this?
I came up with the following:
def method_name object
object_id = object.to_param.to_i
### do whatever needs to be done with that object_id
end
So, if the parameter already is an id, it just pretty much stays the same; if it's an object, it gets its id.
This works, but I feel like this could be better. Also, to_param returns a string, which could in some cases return a "real" string (i.e. "string" instead of "2"), hence returning 0 upon calling to_i on it. This could happen, for example, when using the friendly id gem for classes.
Active record offers the same functionality. It doesn't matter if you say:
Table.where(user_id: User.first.id) # pass in id
or
Table.where(user_id: User.first) # pass in object and infer id
How do they do it? What is the best approach to achieve this effect?
If the process is cross controller actions or in session, better to use id. For example, you are going to save a cart in session, the better choice is id. It's hard to watch how big an object is, using id will help performance and avoid unnecessary error.
However, if the methods are inside same request and all actions are within memory, using object itself would be quicker. For example, you pass an user to an authority class to check if he can do something. Because all objects are just a reference in memory, extra step to extract id is unnecessary and inefficient.
My guess is that ActiveRecord does something like this. Or, rather, that's how I'd do it.
def my_method(oid)
oid = oid.id if oid.respond_to?(:id)
# proceed
end

accessing session data in rails models

i need to check whether a Thing's id is in my session variable.
my instinct is to add a method to the Thing model which checks to see whether the Thing's id is in an array stored in the session variable.
class Thing < ActiveRecord::Base
...
def pinned?
session[:pinned].include? self.id
end
end
The functionality I am trying to create is the user selects a bunch of different things for a list, which are then stored in a per-user session and then retrieved at a later date for printing.
Rails seems to prevent session access in the model without some hackery, which I'm keen to avoid. Maybe i'm approaching the problem the wrong way round, or the pinned? function belongs somewhere else?
Thanks.
Maybe pass it to the method as an argument instead of accessing it directly?
def pinned?(things)
things.include? self.id
end
thing.pinned?(session[:pinned])

Rails - How can I save a field value (that uses a record's ID) though a callback?

I want to create a hash that combines the creating user's user_id + the record's ID to make a MD5 hash, but only on record creation. (Reasons are long-winded but this extracts it).
I am trying:
class BlogPost < ActiveRecord::Base
after_create :hash_this
private
def hash_this
self.info_md5_hashed = (id.to_str + creator_user_id).my_MD5_hash_method
end
end
How can I make sure that the info_md5_hashed field actually gets saved to the database?
If I use before_create I would assume that the ID is not yet available? :(
If I use after_create I would assume that the ID is now available
- but do I need to do another save somehow to store the newly calculated info_md5_hashed field value?
Yes, you will have to save the record twice, since ID generation happens in the database. The only way around that is to pre-sequence an ID, but there's almost certainly no point and you should just accept that you will have to save the record twice ;)
Just call the second save from inside your after_create hook- it's ok to do this (i.e. will not be a looping recurrence issue) because the second save won't re-trigger that hook again ;)
n.b. You could always base the hash on something you know to be unique, such as the username or email, if you want to avoid a double-save.

next available record id

#user = User.new
#user.id returns nil but i need to know it before i save. Is it possible ?
YES you can!
I had the same question and investigated the docs.
The ability to solve this question is very related to your database type in fact.
Oracle and Postgresql do have useful functions to easily solve this.
For MySQL(oracle) or SkySQL(open-source) it seems more complicated (but still possible). I would recommend you avoid using these (MySQL/SkySQL) databases if you need advanced database tools.
First you must try to avoid this situation as much as possible in your application design, as it is dangerous to play with IDs before they get saved.
There may be situation where you don't have any other choice:
For instance when two tables are referencing themselves and for security reason you don't allow DELETE or UPDATE on these tables.
When this is the case, you can use the (PostgreSQL, Oracle) database nextval function to generate the next ID number without actually inserting a new record.
Use it in conjunction with the find_by_sql rails method.
To do this with postgreSQL and Rails for instance, choose one of your rails models and add a class method (not an instance method!).
This is possible with the "self" word at the beginning of the method name.
self tells Ruby that this method is usable only by the class, not by its instance variables (the objects created with 'new').
My Rails model:
class MyToy < ActiveRecord::Base
...
def self.my_next_id_sequence
self.find_by_sql "SELECT nextval('my_toys_id_seq') AS my_next_id"
end
end
When you generate a table with a Rails migration, by default Rails automatically creates a column called id and sets it as the primary key's table. To ensure that you don't get any "duplicate primary key error", Rails automatically creates a sequence inside the database and applies it to the id column. For each new record (row) you insert in your table, the database will calculate by itself what will be the next id for your new record.
Rails names this sequence automatically with the table name append with "_id_seq".
The PostgreSQL nextval function must be applied to this sequence as explained here.
Now about find_by_sql, as explained here, it will create an array containing new objects instances of your class. Each of those objects will contain all the columns the SQL statement generates. Those columns will appear in each new object instance under the form of attributes. Even if those attributes don't exist in your class model !
As you wisely realized, our nextval function will only return a single value.
So find_by_sql will create an array containing a single object instance with a single attribute.
To make it easy to read the value of this very attribute, we will name the resulting SQL column with "my_next_id", so our attribute will have the same name.
So that's it. We can use our new method:
my_resulting_array = MyToy.my_next_id_sequence
my_toy_object = my_resulting_array[0]
my_next_id_value = my_toy_object.my_next_id
And use it to solve our dead lock situation :
my_dog = DogModel.create(:name => 'Dogy', :toy_id => my_next_id_value)
a_dog_toy = MyToy.new(:my_dog_id => my_dog.id)
a_dog_toy.id = my_next_id_value
a_dog_toy.save
Be aware that if you don't use your my_next_id_value this id number will be lost forever. (I mean, it won't be used by any record in the future).
The database doesn't wait on you to use it. If somewhere at any time, your application needs to insert a new record in your my_table_example (maybe at the same time as we are playing with my_next_id_sequence), the database will always assign an id number to this new record immediately following the one you generated with my_next_id_sequence, considering that your my_next_id_value is reserved.
This may lead to situations where the records in your my_table_example don't appear to be sorted by the time they were created.
No, you can't get the ID before saving. The ID number comes from the database but the database won't assign the ID until you call save. All this is assuming that you're using ActiveRecord of course.
I had a similar situation. I called the sequence using find_by_sql on my model which returns the model array. I got the id from the first object of the arry. something like below.
Class User < ActiveRecord::Base
set_primary_key 'user_id'
alias user_id= id=
def self.get_sequence_id
self.find_by_sql "select TEST_USER_ID_SEQ.nextval as contact_id from dual"
end
end
and on the class on which you reference the user model,
#users = User.get_sequence_id
user = users[0]
Normally the ID is filled from a database sequence automatically.
In rails you can use the after_create event, which gives you access to the object just after it has been saved (and thus it has the ID). This would cover most cases.
When using Oracle i had the case where I wanted to create the ID ourselves (and not use a sequence), and in this post i provide the details how i did that. In short the code:
# a small patch as proposed by the author of OracleEnhancedAdapter: http://blog.rayapps.com/2008/05/13/activerecord-oracle-enhanced-adapter/#comment-240
# if a ActiveRecord model has a sequence with name "autogenerated", the id will not be filled in from any sequence
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
alias_method :orig_next_sequence_value, :next_sequence_value
def next_sequence_value(sequence_name)
if sequence_name == 'autogenerated'
# we assume id must have gotten a good value before insert!
id
else
orig_next_sequence_value(sequence_name)
end
end
end
while this solution is specific to Oracle-enhanced, i am assuming the other databases will have a similar method that you could redefine.
So, while it is definitely not advised and you want to be absolutely sure why you would not want to use an id generated by a sequence, if it is needed it is most definitely possible.
It is why I love ruby and Ruby on Rails! :)
In Oracle you can get your current sequence value with this query:
SELECT last_number FROM user_sequences where sequence_name='your_sequence_name';
So in your model class, you can put something like this:
class MyModel < ActiveRecord::Base
self.sequence_name = 'your_sequence_name'
def self.my_next_id_sequence
get_data = self.find_by_sql "SELECT last_number FROM user_sequences where sequence_name='your_sequence_name'"
get_data[0].last_number
end
end
And finally, in controller you can get this value with this:
my_sequence_number = MyModel.my_next_id_sequence
So, there is no need to get your next value by using NEXTVAL and you won't lose you ID.
What you could do is User.max(id). which will return the highest ID in the database, you could then add 1. This is not reliable, although might meet your needs.
Since Rails 5 you can simply call next_sequence_value
Note: For Oracle when self.sequence_name is set, requesting next sequence value creates side effect by incrementing sequence value

Rails: Keep changes to objects/associations in memory without saving to database

I have a Rails model with various attributes and has_many relationships to other models. In the view I want the user to be able to enter/change values which will change the values in the loaded object and return results calculated from them whenever they make a change. However, I don't want the values to change in the database until they select 'Save'. The problem I'm running into is that the variables I associate with the collections pull from the database after each new change. So if I use something like the following:
#model.attributes = params[:model]
it lasts the length of that particular call to the model, but the next time the user makes a change the collection is reloaded, losing all the previous changes aside from the most recent one. I've tried using an instance variable, e.g.:
#costs ||= self.costs
But whenever I run an operation on it, it reloads from the database, overwriting previous changes. E.g.
#costs[i].monthly = 10
Is there any way to ensure that changes persist across the session without saving to the database? Right now it's looking like my best option may be to create a separate model to store the values in and then save them back to the original when the user is finished.
You could either resave it to the params hash whenever you make a change, or try using the session hash (but you'll have to remember to clear it).
http://guides.rubyonrails.org/security.html#what-are-sessions
In short, data does not persist between requests. At the time I was under the impression that attributes held values for the length of the session. I've since learned that the only things that persist are the values stored in the database and those passed back and forth in the params and session hashes.

Resources