Rails store numbers with 2 decimal places after comma in database - ruby-on-rails

I am doing PDFs for invoices in my system and I would like to be able to store numbers with two decimal places in the database. I am using MoneyRails gem for dealing with currencies, I have setup precision: 10 and scale: 2 on the database level (I use postgres as my DB) but I am getting only 1 decimal place after comma. Why?
class AddPrecisionToInvoices < ActiveRecord::Migration[5.2]
def self.up
change_column :invoices, :total_net_amount_cents, :decimal, precision: 10, scale: 2, default: 0.00
change_column :invoices, :total_gross_amount_cents, :decimal, precision: 10, scale: 2, default: 0.00
end
def self.down
change_column :invoices, :total_net_amount_cents, :bigint
change_column :invoices, :total_gross_amount_cents, :bigint
end
end
invoice.rb
monetize :total_net_amount_cents
monetize :total_gross_amount_cents
In rails console,
invoice.total_gross_amount_cents = Money.new(20_000_00)
invoice.total_gross_amount.to_f #=> 2000.0
Is it possible to store numbers with two decimal places in DB, like 20,000.00?
I don't want to display the PDF in a view so I want to be able to drop the number into my DB as I got it from params from my front-end application without further formatting it in a view.

You can try following, (using in model)
ActiveSupport::NumberHelper::number_to_delimited('%.2f' % '3423432.43234', delimiter: ",", separator: ".")
# => "3,423,432.43"
Here, in above input 3423432.43234 is provided as string, you can provide it as number also.
You can directly use number_with_delimiter in view

The money-rails gem requires monetize columns to be numeric in the database. However, it comes with some helper methods that you could use to re-format as you wish in your model:
# inside Invoice model
require "money-rails/helpers/action_view_extension"
class Invoice < ApplicationRecord
include MoneyRails::ActionViewExtension
# ...
def total_gross_amount_formatted
humanized_money total_gross_amount
end
end
Then in your PDF you can just reference the new formatted attribute:
#invoice_instance.total_gross_amount_formatted

Related

When I run a schema migration before a data migration, with ActiveRecord, data does not properly update in DB

As of now, I have a users table with columns
id, name, email, status
status field is an integer type with values 1 and 2 representing an Active and Inactive user, respectively.
I would like to change the status field to a string type and migrate the data -- Convert 1 to "Active" and 2 to "Inactive"
I generated 2 migration files with rails g migration
user.rb
class User < ApplicationRecord
module Status
ACTIVE = 'Active'.freeze
INACTIVE = 'Inactive'.freeze
ALL = [ACTIVE, INACTIVE].freeze
end
validates :status, presence: true
validates :status, inclusion: Status::ALL
end
db/migrate/20190906115523_update_user_status_type.rb
def UpdateUserStatusType < ActiveRecord::Migration[5.2]
def up
change_column :users, :status, :string, default: User::Status::ACTIVE,
end
def down
User.where(status: User::Status::ACTIVE).update_all(status: 1)
User.where(status: User::Status::INACTIVE).update_all(status: 2)
change_column :users, :status, :integer, default: 1
end
end
db/migrate/20190906115828_update_user_statuses.rb
def UpdateUserStatuses < ActiveRecord::Migration[5.2]
def data
User.where(status: 1).update_all(status: User::Status::ACTIVE)
User.where(status: 2).update_all(status: User::Status::INACTIVE)
end
end
After running rails db:migrate
Expected: Each user's status should be converted to either "Active" or "Inactive" after migrations are finished.
Actual Results: Each user's status are converted to "0" of string type.
You're assuming after the first migration runs (change_column :users, :status, :string, default: User::Status::ACTIVE) you can still fetch the old values from the status column which is not the case. When you change the type of that column to string all the integer values are invalid so I suspect your database just changes all the invalid values to be "0" instead.
If I was told to make this change to an application that is heavily used in production, I would be roll out this change in a few separate pull requests/migrations. I'd create a whole new separate column, iterate through all the users, set the value of the new column depending on what the value in the old column is, and then delete the old column. This is a much safer way to make this change.

How do I work with dollar amounts not cents with Rails Money gem?

I'm trying to get the money-rails gem working, and I'm having problems...
Other similar stackoverflow questions are 6 years old.
Here's the product I have the appropriate columns on:
class Transactions < ActiveRecord::Base
belongs_to :user, optional: true
validates :trans_id, uniqueness: true
monetize :price_cents
end
I've got the gem in my Gemfile, and have run bundle install successfully.
When I create a new item and look at it with pry,
create(vendor:"foo",amount:2.6,trans_id:'123cccc')
id: nil,
vendor: "foo",
amount_cents: 260,
amount_currency: "USD",
trans_id: "123cccc",
tax_cents: 150,
total_cents:410,
How do I work with it in dollar amounts? I.e. I want to add amount_cents to tax_cents for total_cents. amount 2.60 instead of amount_cents: 260,
Do I need to add a 'composed_of'?
Also, why is 'cent's in the naming? I thought it was supposed to be removed as the vague documentation states:
In this case the name of the money attribute is created automagically by removing the _cents suffix from the column name.
Cents issue
The money gem is storing the the amount in cents and in the table definition, 2 fields will define the property.
For e.g., consider having the property amount in Transaction. In schema.rb you will find 2 fields: amount_cents and amount_currency.
So, now you will have a transaction.amount with a money object in it.
With money object you can:
use humanized_money #money_object from money_rails helpers to display the amount formatted
you can do operations, like addition, subtraction, even conversion to other currency
3) 'automagically attribute'
Having a migration:
class AddAmountToClient < ActiveRecord::Migration
def change
add_monetize :clients, :amount
end
end
After the migration you can find in schema.rb
create_table "clients", force: :cascade do |t|
t.integer "amount_cents", limit: 8, default: 0, null: false
t.string "amount_currency", default: "USD", null: false
end
What it's saying with attribute is created automagically by removing the _cents, it means that you can access amount property from the Client class with client.amount having a money object.

How to store enum as string to database in rails

How do I create a migration in ruby where the default is a string rather than an Integer, I want to store enum into the database, but I do not want to store it as Integer, because then it does not make sense to another application that wants to use the same table. How do I do default: "female" instead of default:0
class AddSexToUsers < ActiveRecord::Migration
def change
add_column :users, :sex, :integer, default: 0
end
end
class User < ActiveRecord::Base
enum sex: [:female, :male]
has_secure_password
end
I
Reading the enum documentation, you can see Rails use the value index of the Array explained as:
Note that when an Array is used, the implicit mapping from the values to database integers is derived from the order the values appear in the array.
But it is also stated that you can use a Hash:
it's also possible to explicitly map the relation between attribute and database integer with a Hash.
With the example:
class Conversation < ActiveRecord::Base
enum status: { active: 0, archived: 1 }
end
So I tested using Rails 4.2.4 and sqlite3 and created an User class with a string type for sex type and a Hash in the enum with string values(I am using fem and mal values to differ from female and male):
Migration:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :sex, default: 'fem'
end
end
end
Model:
class User < ActiveRecord::Base
enum sex: { female: 'fem', male: 'mal' }
end
And in console:
u = User.new
#=> #<User id: nil, sex: "fem">
u.male?
#=> false
u.female?
#=> true
u.sex
#=> "female"
u[:sex]
#=> "fem"
u.male!
# INSERT transaction...
u.sex
#=> "male"
u[:sex]
#=> "mal"
I normally do the following:
# in the migration in db/migrate/…
def self.up
add_column :works, :status, :string, null: false, default: 'offering'
end
# in app/models/work.rb
class Work < ApplicationRecord
ALL_STATES = %w[canceled offering running payment rating done].freeze
enum status: ALL_STATES.zip(ALL_STATES).to_h
end
By using a hash as argument for enum (see docs) this stores strings in the database. At the same time this still allows you to use all the cool Rails helper methods:
w = Work.new
#=> #<Work id: nil, status: "offering">
w.rating?
#=> false
w.offering?
#=> true
w.status
#=> "offering"
w[:status]
#=> "offering"
w.done!
# INSERT transaction...
w.status
#=> "done"
w[:status]
#=> "done"
Update for one-liner:
I overlooked completely we've got index_by since Rails 1.2.6. This makes the solution a one-liner even:
enum status: %w[canceled offering running payment rating done].index_by(&:to_sym)
Alternatively we've got index_with since Rails 6.0.0:
enum status: %i[canceled offering running payment rating done].index_with(&:to_s)
enum in Rails and ENUM type in MySQL are 2 different things.
enum in Rails is just a wrapper around your integer column so it's easier for you to use strings in queries, rather than integers. But on database level it's all converted to integers (automatically by Rails), since that's the type of the column.
ENUM type in MySQL is vendor-specific column type (for example, SQLite doesn't support it, but PostgreSQL does). In MySQL :
An ENUM is a string object with a value chosen from a list of permitted values that are enumerated explicitly in the column specification at table creation time.
CREATE TABLE shirts (
name VARCHAR(40),
size ENUM('x-small', 'small', 'medium', 'large', 'x-large')
);
INSERT INTO shirts (name, size) VALUES ('dress shirt','large'), ('t-shirt','medium'),
('polo shirt','small');
SELECT name, size FROM shirts WHERE size = 'medium';
+---------+--------+
| name | size |
+---------+--------+
| t-shirt | medium |
+---------+--------+
For the migration, you need to do this:
class AddSexToUsers < ActiveRecord::Migration
def change
add_column :users, :sex, "ENUM('female', 'male') DEFAULT 'female'"
end
end
Take a look at this Gist, Rails doesn't provide it out of the box so you have to use a concern:
https://gist.github.com/mani47/86096220ccd06fe46f0c09306e9d382d
There's steps to add enum as string to model Company
bin/rails g migration AddStatusToCompanies status
class AddStatusToCompanies < ActiveRecord::Migration[7.0]
def change
add_column :companies, :status, :string, null: false, default: 'claimed'
add_index :companies, :status
end
end
bin/rails db:migrate
Values are strings (symbols not working)
add Default
add Prefix
enum status: {
claimed: 'claimed',
unverified: 'unverified',
verified: 'verified',
}, default: 'claimed'
Add validation (or will raise sql exception)
validates :status, inclusion: { in: statuses.keys }, allow_nil: true
To my knowledge it is not possible with standard Rails enum. Look at https://github.com/lwe/simple_enum, it is more functionally rich, and also allows storing of enum values as strings to DB (column type string, i.e. varchar in terms of DB).

What datatype to use for Facebook user id in Rails and PostgreSQL

I have a PostgreSQL database for a Rails application.
I want to store the Facebook user id so I thought I could use integer but its not big enough so I chose float.
However now Rails adds .0 to the end of my user id's
What datatype can I use so this does not happen for Facebook user ids which are very long example: 100002496803785
You can use :limit => 8 on your integer column to get a bigint. For example:
class Pancakes < ActiveRecord::Migration
def change
create_table :pancakes do |t|
t.integer :c, :limit => 8
end
end
end
And then, from psql:
=> \d pancakes
Table "public.pancakes"
Column | Type | Modifiers
--------+---------+-------------------------------------------------------
id | integer | not null default nextval('pancakes_id_seq'::regclass)
c | bigint | not null
Indexes:
"pancakes_pkey" PRIMARY KEY, btree (id)
And there's your eight byte bigint column.
You could also use a string for the Facebook ID. You're not doing any arithmetic on the IDs so they're really just opaque bags of bits that happen to look like large integers, strings will sort and compare just fine so they might be the best option. There would be some storage and access overhead due to the increased size of a string over the integer but it probably wouldn't be enough to make any noticeable difference.
Never use a double for something that needs to be exact. You'd probably be fine (except for the trailing .0 of course) in this case because you'd have 52 bits of mantissa and that means that the double would act like a 52 bit integer until your values got large enough to require the exponent. Even so, using double for this would be an awful idea and an abuse of the type system.
I don't use postgresql but in mysql I use BIGINT
According to postgresql data types, BIGINT for postgresql as well.
mu is too short has a great answer, I only want to add that if you want to use the ID as a foreign key between tables then you should stick to the BIGINT solution he describes, not use a string. This is what I use, essentially:
Example:
create_table(:photos) do |t|
t.integer :fb_uid, :limit => 8 # Facebook ID of the photo record
t.integer :facebook_profile_uid, :limit => 8, :null => false # foreign key to user
# ...
end
create_table(:users) do |t|
t.integer :fb_uid, :limit => 8, :null => false # Facebook ID of the user record
t.integer :photos_count, :integer, :default => 0
# ...
end
class User < ActiveRecord::Base
has_many :photos, foreign_key: :facebook_profile_uid, primary_key: :fb_uid
# ...
end
class Photo < ActiveRecord::Base
belongs_to :facebook_profile, foreign_key: :facebook_profile_uid, primary_key: :fb_uid, :counter_cache => true
end
Ran into this problem while using the Google uid which also is quite large.
I found the this answer to be most useful:
Getting error indicating number is "out of range for ActiveRecord::Type::Integer with limit 4" when attempting to save large(ish) integer value
Run a migration to change your table column.
Edit the generated migration -> add, limit: 8
Run db:migrate to migrate to the database.
Restart the rails server.
This will allow you to change the limit of your table column.

Storing arrays in database using ActiveRecord

I am on rails 2.3.8 & I am using mysql as db adapter.
I want to store arrays in my database. After searching I could come up with this very useful article.
Now I need to use GUI for input & not only server console. So say I have a text field called nums which logically should have int array. What should be the format of nums so that it becomes easy to retrieve & store the array out of that string ?
If you use serialize then you shouldn't have to worry about how the data is stored within the text field, although it's actually YAML.
serialize is documented in the Rails/ActiveRecord API (scroll down to the section headed "Saving arrays, hashes, and other non-mappable objects in text columns")
For display, you need a format that is understandable to users and that can be easily converted back into an array in your code. Comma- or space-delimited?
Formatting for output:
delim = ',' # or ' ' for spaces, or whatever you choose
array.join(delim)
Converting back into an array might work as follows:
num_array = nums.split(delim).map(&:to_i) # or to_f if not integers
or perhaps using String#scan?
num_array = nums.scan(/\d+/).map(&:to_i) # for positive integers
If you're using postgres and rails 4, now you have a better native option.
# db/migrate/20140207133952_create_books.rb
create_table :books do |t|
t.string 'title'
t.string 'tags', array: true
t.integer 'ratings', array: true
end
add_index :books, :tags, using: 'gin'
add_index :books, :ratings, using: 'gin'
# app/models/book.rb
class Book < ActiveRecord::Base
end
# Usage
Book.create title: "Brave New World",
tags: ["fantasy", "fiction"],
ratings: [4, 5]
## Books for a single tag
Book.where("'fantasy' = ANY (tags)")
## Books for multiple tags
Book.where("tags #> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
## Books with 3 or more ratings
Book.where("array_length(ratings, 1) >= 3")
http://edgeguides.rubyonrails.org/active_record_postgresql.html

Resources