Datamapper: report why I can't destroy record - ruby

I'm setting up my db model using datamapper and dm-contraints. I have two models which have a many to many relationship but when I try to destroy one, the only message I get is false.
Is it possible to get datamapper to give me more feedback one which relationship is exactly causing the problem?

With datamapper 1.2.1:
def why_you_no_destroy? model
preventing = []
model.send(:relationships).each do |relationship|
next unless relationship.respond_to?(:enforce_destroy_constraint)
preventing << relationship.name unless relationship.enforce_destroy_constraint(model)
end
preventing
end

Unfortunately DM doesn't provide a way to report why destroy failed.
Most of time the destroy failed because of its associations. DM have a mechanism to avoid orphan records.
To avoid this kind of destroy failed, you can Use dm-constraints(https://github.com/datamapper/dm-constraints ) to set up true database level foreign key references, which default to protect, but can be set to cascade deletes instead.
class List
has n, :todos, :constraint => :destroy (or :destroy!)
end
Sadly, Currently dm-constraints only supports PostgreSQL and MySQL.
For other database, you can check all the associations manually and delete them first, then delete the model。

You can get information on DataMapper errors from
model.destroy
if model
model.errors.each do |error|
p error
end
end
Sometimes that doesn't tell you anything though, in which case you can put your code inside of a begin/rescue block e.g.
begin
model.destroy
rescue Exception => exc
p exc
end

Related

AES Decryption in ruby and activerecord

I have super ugly code that looks like this:
class User < ActiveRecord::Base
self.table_name = 'users'
def get_password
#test_password = User.find_by_sql "SELECT CAST(AES_DECRYPT(Pass, 'kkk') AS CHAR(50)) Pass From prod.sys_users Where Owner = '"+#owner+"' AND User = '"+#user+"'"
#test_password[0].Pass
end
end
This code works, but it makes me sick, since it is not written according to Ruby Coding Style. So I decided to fix this code, and here what I have so far:
class User < ActiveRecord::Base
self.table_name = 'users'
def get_pass
User.where(Owner: #owner, User: #user).pluck(:Pass).first
end
end
So, I am getting encrypted password, how can I decrypt it?
I tired OpenSSL, but key 'kkk' here is too short.
How can I resolve this issue?
In a situation like this, you might be better off converting the field values entirely. This could be done in a migration and once it's done, you never have to be concerned about how MySQL has stored the data. It's also one step toward database independence.
So, the migration would basically do 3 things:
add a flag column to track which records have been converted
iterate over each records, converting the encrypted value and setting the flag
remove the flag column once all records have been processed
The migration might look like this:
class ConvertMySqlEncryptedData < ActiveRecord::Migration
# Local proxy class to prevent interaction issues with the real User class
class User < ActiveRecord::Base
end
def up
# Check to see if the flag has already been created (indicates that migration may have failed midway through)
unless column_exists?(:users, :encrypted_field_converted)
# Add the flag field to the table
change_table :users do |t|
t.boolean :encrypted_field_converted, null: false, default: false
end
end
# Add an index to make the update step go much more quickly
add_index :users, :encrypted_field_converted, unique: false
# Make sure that ActiveRecord can see the new column
User.reset_column_information
# Setup for AES 256 bit cipher-block chaining symetric encryption
alg = "AES-256-CBC"
digest = Digest::SHA256.new
digest.update("symetric key")
key = digest.digest
iv = OpenSSL::Cipher::Cipher.new(alg).random_iv
key64 = Base64.encode(key)
# Don't update timestamps
ActiveRecord::Base.record_timestamps = false
begin
# Cycle through the users that haven't yet been updated
User.where(encrypted_field_converted: false).pluck("CAST(AES_DECRYPT(Pass, 'kkk') AS CHAR(50)) Pass").each do |user|
# Re-encode the password with OpenSSL AES, based on the setup above
new_pass = aes.update(user.pass).final
# Update the password on the row, and set the flag to indicate that conversion has occurred
user.update_attributes(pass: new_pass, encrypted_field_converted: true)
end
ensure
# Reset timestamp recording
ActiveRecord::Base.record_timestamps = true
end
end
def down
# To undo or not undo, that is the question...
end
end
This was off the top of my head, so there may be issues with the encryption. Structure-wise, it should be in good shape, and it takes into account a number of things:
Provides incremental database processing by using a flag to indicate progress
Uses an index on the flag field to improve query performance, particularly if multiple runs are required to complete processing
Avoids updating the updated_at column to prevent overwriting prior values that may be useful to keep (this is not a material change, so updated_at doesn't require updating)
Plucks only the pass field, so that transfer overhead is minimized
Now, you can query pass and encrypt/decrypt as needed by the application. You can document and support the field at the application level, rather than rely on the database implementation.
I spent a few years consulting and doing database conversion, either from one database product to another, or as part of a significant version upgrade. It also allows development to use lighter-weight databases (e.g. SQLite) or test viability with other products when upscaling is needed. Avoiding database-specific features, like the MySQL encryption, will save you (or your employer) a LOT of money and hassle in the long run. Database independence is your friend; embrace it and use what ActiveRecord provides to you.

Rails 4 update Type when migrating to Single Table Inheritance

Rails 4.0.4, Ruby 2.1.2
I want to use STI like so:
User < ActiveRecord::Base
Admin < User
But currently I have:
User < ActiveRecord::Base
Info < ActiveRecord::Base
So I changed my models, and then start writing my migration. In my migration, I first add a column to allow STI:
add_column :users, :type, :string
Then I want to update the Users currently in the database to be Admin
# Place I'm currently stuck
Then I move all my Info records into the Users table
Info.all.each { |info| User.create(name: info.name, email: info.email) }
Everything seems to work except turning the previous Users into Admins. Here are some things I've tried:
# Seems to work, but doesn't actually save type value
User.each do |user|
user.becomes!(Admin)
user.save! # evaluates to true, doesn't have any errors
end
# Seems to work, but doesn't actually save type value
# I've also tried a combo of this one and the above one
User.each do |user|
user.type = "Admin"
user.save! # evaluates to true, doesn't have any errors
end
User.each do |user|
user = user.becomes!(Admin)
user.save! # evaluates to true, doesn't have any errors
end
# Seems to work, but doesn't actually save type value
User.each do |user|
user.update_attributes(type: "Admin")
end
Each time the local user variables seems to have the correct type ("Admin"), along with save evaluating to true, but when I check Admin.count or check Users type value, it is always nil. I know you're not supposed to change them, but this is just to migrate the data over to STI and then I'll be able to start creating Users or Admin with the proper class.
At the very least I think Rails should raise an error, set an error or somehow let the developer know it's failing the save calls.
It turns out that while update_attributes doesn't work for type (I haven't researched why yet), update_column does work.
So the migration simply becomes:
User.each do |user|
user.update_columns(type: "Admin")
end
The reason this works and other updates don't can probably be traced back to either callbacks or validations not being run. I have no callbacks that would prevent it, but maybe there are default Rails ones for type
http://apidock.com/rails/ActiveRecord/Persistence/update_columns
If you had more rows in the database User.each would become quite slow as it makes an SQL call for each user.
Generally you could use User.update_all(field: value) to do this in one SQL call but there is another reason to avoid this: if the User model is later removed the migration will no longer run.
One way to update all rows at once without referencing the model is to use raw SQL in the migration:
def up
execute "UPDATE users SET type = 'Admin' WHERE type IS NULL"
end

Rspec and FactoryGirl Uniqueness Failing

I'll be brief with the code samples, as all of my tests pass except the one below. I got it to pass by changing things up a bit, but I'm not sure why version 1 fails and version 2 works.
My model:
# app/models/person.rb
class Person
validates :contact_number, uniqueness: true
end
Model spec
# spec/models/person_spec.rb
require 'spec_helper'
describe Person do
it 'is a valid factory' do
create(:person).should be_valid # passes
end
it 'has a unique phone number' do
create(:person)
build(:person).should_not be_valid # fails
end
it 'also has a unique phone number' do
person1 = create(:person)
person2 = person1.dup
person2.should_not be_valid # passes
end
end
As far as I can tell, the two uniqueness tests should be doing the same thing, however one passes and one fails.
If it matters, I am using mongoid, though I don't think that should have any effect. I'm also not doing anything with nested contexts or describes in my test, so I think the scope is correct. Any insight is appreciated.
UPDATE 1: I realized in my factory I am adding an initialize_with block like this:
initialize_with { Person.find_or_create_by(contact_number: contact_number) }
I realized that this may be the reason the validation was failing -- I was just getting the same person back. However, commenting out that line gives the following error:
Mongoid::Errors::Validations:
Problem:
Validation of Person failed.
Summary:
The following errors were found: Contact number is already taken
Resolution:
Try persisting the document with valid data or remove the validations.
Which, in theory is good, I suppose, since it won't let me save a second person with the same contact number, but I'd prefer my test to pass.
Probably your person factory has a sequence in contact_number making a diferent contact_number in each person.
Just realize that the build(:person) doesnt validate. The validation occurs only in create.
I strongly suggest use of shoulda-matchers for this kind of validations.
It is possible that your database is being cleaned (do you have database-cleaner in your Gemfile), or your tests are not being run in the order you think they are. (Check for :random in your spec_helper.rb)
While the above answer regarding using shoulda-matchers will help you run this particular test in RSpec more concisely, you probably want to have your unique phone number test be able to be run completely on its own without relying on another spec having executed. Your second test is an example of Obscure Test (and also a little bit of Mystery Guest http://robots.thoughtbot.com/mystery-guest), where it's not clear from the test code what is actually being tested. Your phone number parameter is defined in another file (the factory), and the prior data setup is being run in another spec somewhere else in the file.
Your second test is already better because it is more explicitly showing what you're testing, and doesn't rely on another spec having been run. I would actually write it like this to make it more explicit:
it 'has a unique phone number' do
person1 = create(:person, phone_number: '555-123-4567')
person2 = create(:person, phone_number: '555-123-4567')
# can use 'should' here instead
expect(person2).not_to be_valid
end
If you don't explicitly make it about the phone number, then if you change your factory this test might start failing even though your code is still sound. In addition, if you have other attributes for which you are validating uniqueness, your previous test might pass even though the phone number validation is missing.
I figured it out! On a whim, I checked out the test database and noticed that a Person object was lingering around. So, it actually wasn't the
build(:person).should_not be_valid that was raising the Mongoid exception. It was the create call on the line before. Clearing out the DB and running the spec again passed, but again the data was persisting. I double checked my spec_helper.rb file and noticed I wasn't calling start on the DatabaseCleaner. My updated spec_helper.rb file looks like this, and now everything works:
# Clean up the database
require 'database_cleaner'
config.mock_with :rspec
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.orm = "mongoid"
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end

Does adding a new object in a has_one relationship, not update the association?

I have 2 models, an example:
class Report ...
belongs_to :answer_sheet
end
class AnswerSheet ...
has_one :report
end
When I do a:
#answersheet.report = Report.create(:data => 'bleah')
#answersheet.save
# and then create another report and assign it to the same #answersheet
# assuming at this stage #answersheet is already reloaded
#answersheet.report = Report.create(:data => 'new data')
#answersheet.save
# (irb) #answersheet.report returns the first report with the data 'bleah' and not
# the one with the new data.
Is this supposed to be the correct behavior?
If I want to update the association to the later report, how should I go about doing it?
It took me a few tries to see what you were talking about. But I got it now.
Take a look at the SQL and you'll find ActiveRecord is doing a select and then adding ASC and LIMIT 1. There can be more than one report records that refer to the same answer_sheet.
You can prevent this situation by adding a validation that checks for uniqueness of answer_sheet_id.
You should also start using save! and create! (note the bang operators) so exceptions are thrown during validation.
Lastly, calling Report.create followed by #answersheet.save performs two database transactions, whereas Report.new followed by #answersheet.save would perform just one.

Odd behaviour when saving instances in Ruby DataMapper

having just started to look at DataMapper for Ruby ORM I've run into an issue that confuses me to no end.
The default behaviour when saving instances in DataMapper (through DataMapper::Resource.save) is, as far as I understand, to silently fail, return false from the save method and collect any errors in the errors collection. So far so good, this works as expected. The issue I see is with natural primary keys, where setting duplicate ones will throw an exception instead of silently returning false from the save method, blatantly disregarding the current setting of raise_on_save_failure. Consider the following snippet;
require 'data_mapper'
class Thing
include DataMapper::Resource
property :name , String, :key=> true
property :description, String, length: 2..5
end
DataMapper.setup :default, "sqlite:memory"
DataMapper.finalize
DataMapper.auto_migrate!
#Create a thing and have it properly silently fail saving
t1=Thing.new(:name=>"n1",:description=>"verylongdescription" )
t1.save #ok
puts("t1 is saved:"+t1.saved?.to_s)
t1.errors.each do |e|
puts e
end
#Create two things with the same key, and fail miserably in silently failing the second save...
t1=Thing.new(:name=>"n1",:description=>"ok" )
t2=Thing.new(:name=>"n1",:description=>"ok" )
t1.save #ok
t2.save #not ok, raises Exception event though it shouldn't?
puts("t2 is saved:"+t1.saved?.to_s)
t2.errors.each do |e|
puts e
end
The first save, on an instance failing a validation rule for the :description property behaves as expected. The third save, of an instance with duplicate keys, however does not, since it raises an execption instead of nicely just returning false.
Why is this? It is obviously possible to work around, but it doesn't feel very clear. Is the case that the silent behaviour will only apply to validation errors in the DataMapper layer, and any hard errors from the underlying datastore will be propagated as exceptions to the caller?
Thanks!
As another user pointed out in a comment, this happens because setting raise_on_save_failure to false doesn't mean that no exceptions will occur. It will always return false (with no exceptions) when validation errors occur.
Database errors will blow up, as you noticed, in the form of an exception. Such errors could happen due to a large number of factors (the connection failed, no disk space), including mundane ones like a duplicate key. DataMapper can't know whether the object you're trying to save possesses a duplicate key, so it just validates it and sends to the database, where the error actually occurs.

Resources