#models.map(&:attributes)) returns a list of hashes from each column to its value in the db
How do I limit it so that only specific columns are returns (e.g. just name and id?).
Also, how do I combine multiple columns to a new key => value pair? For example, if a user has first_name and last_name, the above would return
[{"first_name" => "foo", "last_name" => "bar"}] but I want it to be [{"name" => "foo bar"}]
How do I achieve this transformation? Thanks!
For the first part (limiting the attributes in the hash):
#models.map {|model| model.attributes.slice(:id, :name)}
For combining multiple attribute into a new attribute, the cleanest way is usually an accessor method:
class User < ActiveRecord::Base
def name
"#{first_name} #{last_name}"
end
end
Then build your hash manually during iteration:
#models.map {|model| {:id => model.id, :name => model.name}}
If you're using more than one attribute from the attributes hash, you can use merge:
#models.map do |model|
model.attributes.slice(:id, :first_name, :last_name).merge(:name => model.name)
end
Related
I have this function to load several .csv files to an array of objects:
def load_csv_data_to_order_objects
orders = []
get_data_paths("../my/path", ".csv").each do |path|
CSV.foreach(path, :headers => :first_row, :col_sep => ',', encoding: "ISO8859-1:utf-8") do |row|
orders.push Order.new(
:date => row["ORDER_DATE"],
:seller_id => row["SELLER_ID"],
:order_number => row["ORDER_NUMBER"],
:product_id => row["PRODUCT_ID"],
:quantity => row["QUANTITY"].to_i,
:sales_price => row["SALES_PRICE"].to_f,
)
end
end
orders
end
This works, but I need to load .csv files with a different number of columns into different types of objects. The general "shape" of the function is the same, but the object attributes differs.
To minimize code duplication, how can I create a more generic version of this function?
I imagine something like this:
def load_csv_data_to_objects(search_path, file_extension, class_name)
objects = []
get_data_paths(search_path, file_extension).each do |path|
CSV.foreach(path, :headers => :first_row, :col_sep => ',', encoding: "ISO8859-1:utf-8") do |row|
objects.push class_name.new(
# How can I get a class's attributes and map them to .csv headers?
)
end
end
objects
end
The biggest problem, which I see, is, that your column headers are not named exactly like your model attributes. Example: 'date' vs. 'ORDER_DATE'. It is not trivially possible to guess, that the attribute date should be populated with the content of the column 'ORDER_DATE'. This makes the task way more complex.
My first idea would therefore be to just define on each class a mapping like
class Order < ActiveRecord::Base
CSV_IMPORT_MAPPING = {
date: 'ORDER_DATE',
seller_id: 'SELLER_ID',
# ...
}
and use this mapping to initialize the objects in a loop:
CSV.foreach(path, :headers => :first_row, :col_sep => ',', encoding: "ISO8859-1:utf-8") do |row|
objects.push class_name.new(class_name::CVS_IMPORT_MAPPING.tap { |hash| hash.map { |class_attr, csv_attr| hash[class_attr] = row[csv_attr] } })
)
end
This will basically convert the CSV_IMPORT_MAPPING from
{ date: 'ORDER_DATE', seller_id: 'SELLER_ID' }
to a hash with the column content as values
{ date: '2016-12-01', seller_id: 12345 }
and pass this hash to the new function to instantiate a new object.
Not cool, but you get a nice overview, which csv column matches to which model attribute and you gain flexibility (you could for example decide to not import all columns, but just the ones from the hash).
Another approach could be, to replace 'CSV.foreach' with 'CSV.read' to get access to the headers, iterate over the headers and use them to instantiate your object:
csv = CSV.read(path, :headers => :first_row, :col_sep => ',', encoding: "ISO8859-1:utf-8") do |row|
instance = class_name.new
csv.headers.each do |header|
instance.send("#{header.downcase}=", row[header])
end
objects.push instance
)
end
'header.downcase' will convert 'ORDER_DATE' to 'order_date', e.g. you will need to add an setter for each column name, that differs from your model attributes, on your model (which i consider to be really ugly):
class Object < ActiveRecord::Base
alias_method :order_date=, :date=
This would not be necessary if you could get a csv file, in which the column headers are named exactly like your model attributes.
I hope, that one of the ideas works for you.
I want to define a method_missing function for one of my classes, and I want to be able to pass in a hash as the argument list instead of an array. Like this:
MyClass::get_by_id {:id => id}
MyClass::get_by_id {:id => id, :filters => filters}
MyClass::get_by_id {:id => id, :filters => filters, :sort => sort}
As far as I can tell, the args list gets passed in as an array, so keys get dropped and there's no way to tell which arguments is which. Is there a way to force Ruby to treat the argument list in method_missing as a hash?
What issue are you having? This works for me:
class MyClass
def self.method_missing name, args
puts args.class
puts args.inspect
end
end
MyClass.foobar :id => 5, :filter => "bar"
# Hash
# {:id=>5, :filter=>"bar"}
Is this what you are looking for ?
class Foo
def self.method_missing(name,*args)
p args
p name
end
end
Foo.bar(1,2,3)
# >> [1, 2, 3]
# >> :bar
I'm answering my own question based on experimentation that I've done since asking. When using a hash splat on the arg list, you can pass in a hash like this:
MyClass::get_by_id(:id => id, :filters => filters)
And the argument list will look like this:
[
{
:id => id,
:filters => filters
}
]
Ruby places the key/value pairs into a single hash object at location args[0]. If you call the method like this:
MyClass::get_by_id(id, :filters => filters)
Your argument list will be this:
[
id,
{:filters => filters}
]
So basically, key/value pairs are merged together into a single hash and passed in order.
Is it possible to create an attribute for a class that is an array? I tried reading this but I didn't get much out of it. I want to do something like this:
class CreateArches < ActiveRecord::Migration
def change
create_table :arches do |t|
t.string :name
t.array :thearray
t.timestamps
end
end
end
such that when I call .thearray on an instance of Arch I get an array that I can add new elements to.
ruby-1.9.2-p290 :006 > arc = Arch.new
ruby-1.9.2-p290 :007 > arc.thearray
=> []
Create a model with a text field
> rails g model Arches thearray:text
invoke active_record
create db/migrate/20111111174052_create_arches.rb
create app/models/arches.rb
invoke test_unit
create test/unit/arches_test.rb
create test/fixtures/arches.yml
> rake db:migrate
== CreateArches: migrating ===================================================
-- create_table(:arches)
-> 0.0012s
== CreateArches: migrated (0.0013s) ==========================================
edit your model to make the field serialized to an array
class Arches < ActiveRecord::Base
serialize :thearray,Array
end
test it out
ruby-1.8.7-p299 :001 > a = Arches.new
=> #<Arches id: nil, thearray: [], created_at: nil, updated_at: nil>
ruby-1.8.7-p299 :002 > a.thearray
=> []
ruby-1.8.7-p299 :003 > a.thearray << "test"
=> ["test"]
While you can use a serialized array as tokland suggested, this is rarely a good idea in a relational database. You have three superior alternatives:
If the array holds entity objects, it's probably better modeled as a has_many relationship.
If the array is really just an array of values such as numbers, then you might want to put each value in a separate field and use composed_of.
If you're going to be using a lot of array values that aren't has_manys, you might want to investigate a DB that actually supports array fields. PostgreSQL does this (and array fields are supported in Rails 4 migrations), but you might want to use either a non-SQL database like MongoDB or object persistence such as MagLev is supposed to provide.
If you can describe your use case -- that is, what data you've got in the array -- we can try to help figure out what the best course of action is.
Migration:
t.text :thearray, :default => [].to_yaml
In the model use serialize:
class MyModel
serialize :thearray, Array
...
end
As Marnen says in his answer, it would be good to know what kind of info you want to store in that array, a serialized attribute may not be the best option.
[Marten Veldthuis' warning] Be careful about changing the serialized array. If you change it directly like this:
my_model.thearray = [1,2,3]
That works fine, but if you do this:
my_model.thearray << 4
Then ActiveRecord won't detect that the value of thearray has changed. To tell AR about that change, you need to do this:
my_model.thearray_will_change!
my_model.thearray << 4
If using Postgres, you can use its Array feature:
Migration:
add_column :model, :attribute, :text, array: true, default: []
And then just use it like an array:
model.attribute # []
model.attribute = ["d"] #["d"]
model.attribute << "e" # ["d", "e"]
This approach was mentioned by Marnen but I believe an example would be helpful here.
Rails 6+
In Rails 6 (and to a lesser degree Rails 5) you can use the Attribute API which will allow you to create a typed, "virtual"/non-db backed column and even a default attribute. For example:
attribute :categories, :jsonb, array: true, default: [{ foo: 'bar' }, { fizz: 'buzz' }]
Which results in:
Example.new
#<Example:0x00007fccda7920f8> {
"id" => nil,
"created_at" => nil,
"updated_at" => nil,
"categories" => [
[0] {
"foo" => "bar"
},
[1] {
"fizz" => "buzz"
}
]
}
Note that you can use any type, but if it's not already available, you'll have to register it. In the above case, I use PostgeSQL as the database and which has already registered :jsonb as a type.
I have two hashes, one looks like this:
{:id => "SG_5viWPcG0SLvszXbBxogLkT_51.514568_-0.126244#1300740367",
:name => "Shellys Shoes",
:lat => 51.5145683289,
:lng => -0.1262439936}
This is just one record, there are about 80,
The second hash I have is:
{"id":"SG_2zNWLdG9147g2ROvNWpDHr_51.512360_0.124480#1300740823",
"lat":51.5123596191,
"lng":-0.1244800016}
The hash above is again just one record, however it is a product of the HASH above after going through an API that does not return all the records, only the valid ones, what I want to do is compare the top hash with the bottom one and delete any records that are not present in the bottom hash,
for example if id:SG_5viWPcG0SLvszXbBxogLkT_51.514568_-0.126244#1300740367 is not in the the second hash then delete that record,
I can compare the hashes, but cant see how to delete if ID is not present?
Thanks guys!
edit:
these are the returned values....
{"points":[{"id":"SG_75oKOgvgFPLjwmdyAKA2rq_51.512825_-0.124655#1300740283","lat":51.5128250122,"lng":-0.1246550009},{"id":"SG_0Sz9CBF5t70tdAffTKYNSg_51.512360_-0.124388#1300740807","lat":51.5123596191,"lng":-0.1243880019},{"id":"SG_2zNWLdG9147g2ROvNWpDHr_51.512360_-0.124480#1300740823","lat":51.5123596191,"lng":-0.1244800016},{"id":"SG_5PvBx89sLPgplapegVJDFv_51.513100_-0.124809#1300740049","lat":51.5130996704,"lng":-0.1248089969},{"id":"SG_4luyHFi5R2f1w3cpjT61ik_51.513393_-0.124556#1300740719","lat":51.5133934021,"lng":-0.1245559976},{"id":"SG_4luyHFi5R2f1w3cpjT61ik_51.513393_-0.124556#1300740719","lat":51.5133934021,"lng":-0.1245559976},{"id":"SG_0pEvrpt7bs42jPAxFSrquC_51.512264_-0.124413#1300740807","lat":51.5122642517,"lng":-0.1244129986},]}
This is the original format:
[ { :id => "SG_2Km6LX3tEcFwx24eotTHIY_51.513016_-0.123721#1300740411",
:name => "French Connection Group Plc",
:lat => 51.5130157471,
:lng => -0.1237210035
}]
You can collect a list of the valid IDs from the returned values with something like:
valid_ids = returned["points"].collect { |point| point["id"] }
You can then remove the invalid values from the original with something like:
original.delete_if { |entry| !valid_ids.include? entry[:id] }
Greetings,
I want to store some data in a redis db and don't know which way I should go. The data is equivalent to something like an address with the variables name, street and number. They will be stored under the lower cased name as key, there won't be doublets.
Now, should I save it as a list or should I serialize the hash ({:name => 'foo', :street => 'bar', :number => 'baz'} for example) with JSON/Marshall and simply store that?
Regards
Tobias
Using an encoded json object is a pretty good idea. You can see some examples in hurl — check out how the models are saved.
Redis hashes are nice too, especially if you need atomic operations on hash values.
Also you can use something like Nest to help you DRY up your keys:
addresses = Nest.new("Address", Redis.new)
this_address = addresses[1]
# => "Address:1"
this_address.hset(:name, "foo")
this_address.hset(:street, "bar")
this_address.hgetall
# => {"name" => "foo", "street" => "bar"}
If you need something more advanced, there's Ohm, which maps Ruby classes to Redis:
class Address < Ohm::Model
attribute :name
attribute :street
attribute :number
end
# Create
Address.create(:name => "foo", :street => "bar")
# Find by ID
Address[1]
# Find all addresses with name "foo"
class Address < Ohm::Model
attribute :name
attribute :street
attribute :number
index :name
end
Address.find(:name => "foo")
# => Array-like with all the Address objects