How to make more efficient code in model? - performance

Any idea to refactor the code of the method self.import_data ? It's a method which allow the application to save CSV file in database (with some restriction on the user email). It's supposed to run every day at noon so it has to be quick.
Currently its very long to run when I have a big CSV file. I wonder if there a way to make this code more efficient and win some time (or to avoiding the loop or make less request...). I don't really know what makes the process so long actually and how to correct it.
Here is my model :
class Person < ActiveRecord::Base
has_paper_trail
validates :email, uniqueness: true
require 'csv'
def is_former_email?(update_email)
self.versions.each do |version|
next if version.object.nil?
return true if version.object.include?(update_email)
end
end
def self.import_data
filename = File.join Rails.root, '/vendor/people.csv'
CSV.foreach(filename, headers: true, col_sep: ',') do |row|
firstname, lastname, home_phone_number, mobile_phone_number, email, address = row
person = Person.find_or_create_by(firstname: row["firstname"], lastname: row['lastname'], address: row['address'] )
if person.is_former_email?(row['email']) == true
puts "not allowed"
else
person.update_attributes({firstname: row['firstname'], lastname: row['lastname'], home_phone_number: row['home_phone_number'], mobile_phone_number: row['mobile_phone_number'], address: row['address'], email: row['email']})
end
end
end
end

I was a little refactored your code, but for more efficiently I recommend to use gem activerecord-import and optimize versions model for search previous emails.
class Person < ActiveRecord::Base
require 'csv'
FILE_NAME = File.join Rails.root, '/vendor/people.csv'
validates :email, uniqueness: true
has_paper_trail
def self.import_data
people = CSV.new(File.new(FILE_NAME), headers: true, header_converters: :symbol, converters: :all).to_a.map(&:to_hash)
versions_by_item_id = Version.where(item_type: 'Person').select('item_id, object').group_by(&:item_id)
people.each do |person_params|
person = Person.find_or_create_by(person_params.slice(:firstname, :lastname, :address))
if versions_by_item_id[person.id] && versions_by_item_id[person.id].sum { |v| v.object.to_s }.include?(person_params[:email])
puts 'not allowed'
else
person.update_attributes(person_params.slice(:home_phone_number, :mobile_phone_number, :email))
end
end
end
end

Related

Why does Ruby Kirbybase doesn't work with if statement?

I recently started using kirbybase in ruby, but I ran into a problem using the if statement with a result set. Here's a semplified code that seems to have this problem:
require 'kirbybase'
db = KirbyBase.new
if db.table_exists?(:database)
db.drop_table(:database)
end
list = db.create_table(:database, :name, :String, :password, :String, :test, :String)
name = 'Test'
password = 'abcde'
list.insert(name, password, nil)
account = list.select { |r| r.name == name}
if account.test.nil?
puts 'right'
else
puts 'wrong'
end
Why does it output "wrong"?
It seems to be strange to answer my own question but I solved the problem: account.test is an array so the correct form of the if statement is:
if account.test[0].nil?
puts 'right'
else
puts 'wrong'
end

Geocoder doesn't get lat lon on model

I am using the Sinatra ruby framework.I have a delivery model(see below). I am using the geocoder gem with ActiveRecord.
I have the fields latitude and longitude in my schema.
When I use the console to get the Geocode:
Geocoder.search delivery.address
I get the response from the google API.
But it doesn't populate the lat\lon fields. I can't imagine why.
I am using an API key in app.rb like so:
Geocoder.configure(
api_key: ENV['GEOCODER_API_KEY']
)
And I know the key works since I am getting responses for relatively high number of API calls per second.(Without the api key it's a call every 10 sec or so, or it returns an quota error)
This seems like a simple issue, but I can't figure it out. Would appreciate the help.
Delivery.rb
require 'sinatra/shopify-sinatra-app'
require 'geocoder'
# This is the delivery model. It holds all of the data
# associated with the delivery such as the orrder and shop it belongs to .
class Delivery < ActiveRecord::Base
extend Geocoder::Model::ActiveRecord
has_many :delivery_states, dependent: :destroy
belongs_to :shop
belongs_to :fulfillment_service
validates :order_id, uniqueness: true, presence: true
has_one :courier, through: :fulfillment_service
#after_create :add_delivery_state
attr_accessor :latitude, :longitude
#geocoder setup
geocoded_by :address
after_validation :geocode#, :if => lambda{ |obj| obj.address1_changed? || obj.city_changed? || obj.province_changed? || obj.country_changed? }
def address
#check address1 is not a variation of POB
addr = address1 unless address1.match(/(?:P(?:ost(?:al)?)?[\.\-\s]*(?:(?:O(?:ffice)?[\.\-\s]*)?B(?:ox|in|\b|\d)|o(?:ffice|\b)(?:[-\s]*\d)|code)|box[-\s\b]*\d)/i)
[addr, city, province, country].join(',')
end
def add_delivery_state(status=0,ref_number=nil)
if self.delivery_states.count>0
ref_number = self.delivery_states.last.courier_reference unless ref_number
else
ref_number = 0 unless ref_number
end
self.delivery_states<<DeliveryState.new(delivery_status:status,courier_reference:ref_number)
end
def delivery_state
self.delivery_states.last.delivery_status
end
def courier_reference
self.delivery_states.count>0 ? self.delivery_states.last.courier_reference : "0"
end
end

rom-rb form validation when using multiple relations

I'm trying out http://rom-rb.org/ and can't figure out how to get a presence validation to pass in the presence of multiple source models. I would expect the following script to save a new event and organiser, but instead it says that event_name is not present.
What am I missing?
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'rom'
gem 'rom-sql'
gem 'rom-rails'
gem 'activemodel'
gem 'sqlite3'
gem 'activesupport'
end
require 'rom'
require 'rom-rails'
`rm -Rf /tmp/romtest.sqlite`
ROM.setup(:sql, 'sqlite:///tmp/romtest.sqlite')
class Events < ROM::Relation[:sql]
end
class Organisers < ROM::Relation[:sql]
end
class CreateEvent < ROM::Commands::Create[:sql]
relation :events
register_as :create
result :one
associates :organiser, key: [:organiser_id, :id]
end
class CreateOrganiser < ROM::Commands::Create[:sql]
relation :organisers
register_as :create
result :one
end
class CreateEventWithOrganiser < ROM::Model::Form
commands organisers: :create, events: :create
input do
attribute :email
attribute :event_name
end
validations do
validates :event_name, presence: true
end
def commit!
command = organisers.create.with(
email: email,
) >> events.create.with(
name: event_name,
)
command.transaction do
command.call
end
end
end
ROM.finalize
rom = ROM.env
gateway = rom.gateways.fetch(:default)
migration = gateway.migration do
change do
create_table :organisers do
primary_key :id
column :email, String, null: false
end
create_table :events do
primary_key :id
column :name, String, null: false
column :organiser_id, Integer, null: false
end
end
end
migration.apply(gateway.connection, :up)
f = CreateEventWithOrganiser.build(
email: 'test#example.com',
event_name: 'Test Event'
)
# Unexpectedly fails
f.save
puts f.errors.full_messages
# => "Event name can't be blank"
Here's an updated version of your script which works:
require 'rom'
require 'rom-rails'
`rm -Rf /tmp/romtest.sqlite`
ROM.setup(:sql, 'sqlite:///tmp/romtest.sqlite')
class Events < ROM::Relation[:sql]
end
class Organisers < ROM::Relation[:sql]
end
class CreateEvent < ROM::Commands::Create[:sql]
relation :events
register_as :create
result :one
associates :organiser, key: [:organiser_id, :id]
end
class CreateOrganiser < ROM::Commands::Create[:sql]
relation :organisers
register_as :create
result :one
end
class CreateEventWithOrganiser < ROM::Model::Form
inject_commands_for :organisers, :events
input do
attribute :email
attribute :event_name
end
validations do
validates :event_name, presence: true
end
def commit!
validate!
return if errors.any?
command = organisers.create.with(
email: email
) >> events.create.with(
name: event_name
)
command.transaction do
command.call
end
end
end
ROM.finalize
rom = ROM.env
gateway = rom.gateways.fetch(:default)
migration = gateway.migration do
change do
create_table :organisers do
primary_key :id
column :email, String, null: false
end
create_table :events do
primary_key :id
column :name, String, null: false
column :organiser_id, Integer, null: false
end
end
end
migration.apply(gateway.connection, :up)
f = CreateEventWithOrganiser.build(
email: 'test#example.com',
event_name: 'Test Event'
)
puts f.save.result.inspect
# #<ROM::Commands::Result::Success:0x007fa92b589ea0 #value={:id=>1, :name=>"Test Event", :organiser_id=>1}>
The reason why it didn't work with commands is because this method will generate command objects for your form and set provided validations for each command, which will only work correctly if you used a single command. Otherwise same validator is used for each command which doesn't make sense. When you use inject_commands_for it will grab your own commands where validators are not set so you are free to handle validations yourself.
I think we should stop setting validators on commands which would make your original sample work but notice that you need to call validate! yourself.
I hope this helps.
I also created a gist showing how to do the same without a form: https://gist.github.com/solnic/3b68342482cf1414f719

How to filter by foreign id and local attribute via belongs_to?

The following models are linked via belongs_to:
require 'mongoid'
class Sensor
include Mongoid::Document
field :sensor_id, type: String
validates_uniqueness_of :sensor_id
end
...
require 'mongoid'
require_relative 'sensor.rb'
class SensorData
include Mongoid::Document
belongs_to :sensor
field :date, type: Date
field :ozonMax1h, type: Float
field :ozonMax8hMittel, type: Float
index({ date: 1, sensor_id: 1 }, { unique: true })
end
Here is a Sinatra app which provides a few API paths based on these models:
require 'sinatra'
require 'csv'
require_relative './models/sensor.rb'
require_relative './models/sensor_data.rb'
configure do
Mongoid.load!('./mongoid.yml')
end
def prepare_for_export(sensor_data)
converted_data = sensor_data.asc(:date).map do |e|
{
sensor_id: e.sensor.nil? ? :null : e.sensor.sensor_id,
date: e.date,
ozonMax1h: e.ozonMax1h,
ozonMax8hMittel: e.ozonMax8hMittel
}
end
converted_data
end
def convert_to_json(sensor_data)
prepare_for_export(sensor_data).to_json
end
def convert_to_csv(sensor_data)
data = prepare_for_export sensor_data
csv_string = CSV.generate do |csv|
csv << data.first.keys
data.each do |hash|
csv << hash.values
end
end
csv_string
end
def get_recent
max_date = SensorData.max(:date)
SensorData.where(date: max_date)
end
def get_for_year(year)
SensorData.where(:date.gte => Date.new(year, 1, 1)).where(:date.lte => Date.new(year, 12, 31))
end
def get_for_sensor(sensor)
foo = SensorData.where(sensor_id: sensor)
puts "hallo"
return foo
end
get '/api/v1/stations' do
content_type :json
Sensor.all.map { |e| {sensor_id: e.sensor_id} }.to_json
end
get '/api/v1/sensordata/:year' do
content_type :json
convert_to_json get_for_year(params[:year].to_i)
end
get '/api/v1/sensordata/:year/csv' do
convert_to_csv get_for_year(params[:year].to_i)
end
get '/api/v1/recent' do
content_type :json
convert_to_json get_recent
end
I would like to output the SensorData for a particular sensor such as here:
/api/v1/stations/:sensor_id/sensordata/:year/csv
I am not sure what you are trying to do or even if you are still looking for an answer but here it goes. Something seems wrong with the models in the example you have here. Sounds like part of what you are doing would work if Sensor knows about sensor_data. So might need to add this to Sensor class:
has_many :sensor_data
Though the singular of data is datum. The class would be expected to be SensorDatum. If you can't change it, you need to tell Mongoid the class_name to expect in the has_many is actuall SensorData.
You CAN specify foreign_key in Mongoid with belongs_to.
You CANNOT filter with the belongs_to like you can with ActiveRecord, but you can use scopes outside of the belongs_to to get the same effect. Exampe:
belongs_to :sensor
scope :for_year, -> (year) { where(:date.gte => Date.new(2015,1,1)).where(:date.lte => Date.new(2015, 12, 31))}
or
belongs_to :sensor
def self.for_year year
where(:date.gte => Date.new(year,1,1)).where(:date.lte => Date.new(year, 12, 31))
end
So your query would become something like this:
sensor = Sensor.find_by(sensor_id: params[:sensor_id])
sensor.sensor_data.for_year(2015)

Need Assistance With Initializing Person Object

The Goal:
Build a Person object for each row, with first_name, last_name, and email attributes. This list of people should be encapsulated in a class that can do stuff like return unique by different attributes, ie: people.unique_by_first_name. Also the people object should be Enumberable.
Attempt #3:
#!/usr/bin/env ruby
class Person
attr_accessor :first_name, :last_name, :email
def self.unique_by_first_name(people_array)
people_array.uniq{ |x| x.first_name } # see result below
end
def self.import_data
people_array = Array.new
file = DATA.read
file.each_line do |line|
person = Person.new
fields = line.split(',')
fields.each_with_index do |field, i|
if field.include? '#'
person.email = field.to_s.strip
fields.delete_at(i)
elsif field.empty?
fields.delete_at(i)
else
person.first_name = fields[0].to_s.strip
person.last_name = fields[1].to_s.strip
end
end
people_array.push(person)
end
Person.unique_by_first_name(people_array)
end
end
Person.import_data
__END__
John,Smith,john#foo.com
james#data.net,James
Phillip,Jones,phil#msn.com
,Irene,Smith
Robert,Chorley,rob#foo.com,
emma#hotmail.com,Emma,
David,Smith
james#game.net,James,Bond
james#game.net,James,Bond
Result:
[#<Person:0x007fd30228fdc0 #email="john#foo.com", #first_name="John", #last_name="Smith">,
#<Person:0x007fd30228f870 #email="james#data.net">,
#<Person:0x007fd30228f438 #email="phil#msn.com", #first_name="Phillip", #last_name="Jones">,
#<Person:0x007fd30228ea60 #first_name="Irene", #last_name="Smith">,
#<Person:0x007fd30228e420 #email="rob#foo.com", #first_name="Robert", #last_name="Chorley">,
#<Person:0x007fd3022879e0 #email="emma#hotmail.com", #first_name="Emma", #last_name="">,
#<Person:0x007fd302286680 #first_name="David", #last_name="Smith">,
#<Person:0x007fd302285e38 #email="james#game.net", #first_name="James", #last_name="Bond">]
Any Ideas for Improvement?
The code above produces the following results ^. Do you guys have any recommendations on how to improve this logic?
In order for you to access those attributes on the Person class you need to add:
attr_reader :first_name, :last_name, :email to the top of your class
Like this:
class Person
attr_reader :first_name, :last_name, :email
def initialize(first_name, last_name, email)
first_name = first_name
last_name = last_name
email = email
end
Now you can call your x.first_name on an object because you're explicitly allowing your program to read it. Here's a really famous SO answer on the subject.

Resources