PostgreSQLを使っていて、金額のカラムをdecimalに変更した時に、小数が表示されるようになりました。to_iすれば正数になりますが、to_iがto_integerだとするとINT_MAX(2147483647)を超えたらどうなるんだろう?
結論は問題ないが、そもそもサイズを指定した方が良い。
ちなみにMySQLでは、decimalは正数になるので、小数使いたい場合はサイズ指定が必要。
to_iを試してみる
% rails c > (2147483647 + 1).to_d => 0.2147483648e10 > (2147483647 + 1).to_d.class => BigDecimal to_sして確認 > (2147483647 + 1).to_d.to_s => "2147483648.0" to_iしも溢れない > (2147483647 + 1).to_d.to_i => 2147483648 to_iの型はIntegerのまま > (2147483647 + 1).to_d.to_i.class => Integer
どうやら32bit以上の場合、裏で拡張されて、溢れない(オーバーフローしない)ようだ。
但し、DBには制限があるので、保存で失敗します。超えないように or 超えた時の考慮は必要。
各言語での整数型の最大値と最小値 – HHeLiBeXの日記 正道編
Ruby は 64-bit より大きい整数値をどう扱っているか? – ユユユユユ
to_i使う前に
ActiveRecordが便利すぎて、ついついデフォルトで作っちゃいますが、
特にdecimalはDBによりデフォルトが違うので、サイズを明示した方が良い。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.1.1 数値型の概要
DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL]
パックされた「正確な」固定小数点数。M は桁数の合計 (精度) で、D は小数点以下の桁数 (スケール) です。小数点と、負の数に対する「-」の記号は M にはカウントされません。D が 0 のときは、小数点や小数部はありません。DECIMAL の最大桁数 (M) は 65 です。サポートされる小数部の最大桁数 (D) は 30 です。D が省略された場合のデフォルトは 0 です。M が省略された場合のデフォルトは 10 です。
decimal 可変長 ユーザ指定精度、正確 小数点前までは131072桁、小数点以降は16383桁
numeric 可変長 ユーザ指定精度、正確 小数点前までは131072桁、小数点以降は16383桁
※PostgreSQLは、decimalでmigrateするとnumericになる。同じっぽいですね。
サイズ指定有り無しで試してみる
% rails g model test price:decimal Running via Spring preloader in process 52347 invoke active_record create db/migrate/20210802233629_create_tests.rb create app/models/test.rb invoke rspec create spec/models/test_spec.rb invoke factory_bot create spec/factories/tests.rb
db/migrate/20210802233629_create_tests.rb
class CreateTests < ActiveRecord::Migration[6.1] def change create_table :tests do |t| t.decimal :price + t.decimal :price65_0, precision: 65, scale: 0 + t.decimal :price65_2, precision: 65, scale: 2 t.timestamps end end end
% rails db:migrate == 20210802233629 CreateTests: migrating ====================================== -- create_table(:tests) -> 0.0729s == 20210802233629 CreateTests: migrated (0.0729s) =============================
MySQL
デフォルトはdecimal(10,0)
% rails db > SHOW CREATE TABLE tests\G *************************** 1. row *************************** Table: tests Create Table: CREATE TABLE `tests` ( `id` bigint NOT NULL AUTO_INCREMENT, `price` decimal(10,0) DEFAULT NULL, `price65_0` decimal(65,0) DEFAULT NULL, `price65_2` decimal(65,2) DEFAULT NULL, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin 1 row in set (0.005 sec)
MySQLのデフォルトは小数なし。
% rails c > test = Test.new(price: 100.5, price65_0: 100.5, price65_2: 100.5) => #<Test id: nil, price: 100, price65_0: 100, price65_2: 0.1005e3, created_at: nil, updated_at: nil> 小数は無視され、Integerになる。 > test.price => 100 > test.price.class => Integer 同様に小数は無視され、Integerになる。 > test.price65_0 => 100 > test.price65_0.class => Integer > test.price65_2 => 0.1005e3 > test.price65_2.class => BigDecimal > test.price65_2.to_s => "100.5"
PostgreSQL
デフォルトはnumeric(サイズの記載がないので最大)
% rails db # \d tests Table "public.tests" Column | Type | Collation | Nullable | Default ------------+--------------------------------+-----------+----------+----------------------------------- id | bigint | | not null | nextval('tests_id_seq'::regclass) price | numeric | | | price65_0 | numeric(65,0) | | | price65_2 | numeric(65,2) | | | created_at | timestamp(6) without time zone | | not null | updated_at | timestamp(6) without time zone | | not null | Indexes: "tests_pkey" PRIMARY KEY, btree (id)
PostgreSQLのデフォルトは小数あり。
% rails c > test = Test.new(price: 100.5, price65_0: 100.5, price65_2: 100.5) => #<Test id: nil, price: 0.1005e3, price65_0: 100, price65_2: 0.1005e3, created_at: nil, updated_at: nil> > test.price => 0.1005e3 > test.price.class => BigDecimal > test.price.to_s => "100.5" 小数は無視され、Integerになる。 > test.price65_0 => 100 > test.price65_0.class => Integer > test.price65_2 => 0.1005e3 > test.price65_2.class => BigDecimal > test.price65_2.to_s => "100.5"
SQLite3
デフォルトはdecimal(サイズの記載がないので最大)、但し・・・
% rails db sqlite> .schema tests CREATE TABLE IF NOT EXISTS "tests" ( "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "price" decimal, "price65_0" decimal(65,0), "price65_2" decimal(65,2), "created_at" datetime(6) NOT NULL, "updated_at" datetime(6) NOT NULL );
SQLite3は、PostgreSQLと変わらない。
% rails c > test = Test.new(price: 100.5, price65_0: 100.5, price65_2: 100.5) => #<Test id: nil, price: 0.1005e3, price65_0: 100, price65_2: 0.1005e3, created_at: nil, updated_at: nil> > test.price => 0.1005e3 > test.price.class => BigDecimal > test.price.to_s => "100.5" 小数は無視され、Integerになる。 > test.price65_0 => 100 > test.price65_0.class => Integer > test.price65_2 => 0.1005e3 > test.price65_2.class => BigDecimal > test.price65_2.to_s => "100.5"
ちなみにオーバーしたら
代入はできるが、保存に失敗する。
> test.price65_0 = 10 ** 65 => 100000000000000000000000000000000000000000000000000000000000000000 > test.save TRANSACTION (8.2ms) BEGIN Test Create (24.7ms) INSERT INTO `tests` (`price`, `price65_0`, `price65_2`, `created_at`, `updated_at`) VALUES (100, 100000000000000000000000000000000000000000000000000000000000000000, 100.5, '2021-08-04 09:42:50.914251', '2021-08-04 09:42:50.914251') TRANSACTION (3.3ms) ROLLBACK Traceback (most recent call last): 1: from (irb):10:in `<main>' ActiveRecord::RangeError (Mysql2::Error: Out of range value for column 'price65_0' at row 1) => false
オーバーしなければOK
> test.price65_0 = 10 ** 65 - 1 => 99999999999999999999999999999999999999999999999999999999999999999 > test.save TRANSACTION (2.8ms) BEGIN Test Update (18.8ms) UPDATE `tests` SET `tests`.`price65_0` = 99999999999999999999999999999999999999999999999999999999999999999, `tests`.`updated_at` = '2021-08-04 09:45:59.038654' WHERE `tests`.`id` = 1 PaperTrail::Version Create (4.9ms) INSERT INTO `versions` (`item_type`, `item_id`, `event`, `object`, `created_at`) VALUES ('Test', 1, 'update', '---\nid: 1\nprice: 100\nprice65_0: 10000000000000000000000000000000000000000000000000000000000000000\nprice65_2: !ruby/object:BigDecimal 27:0.1005e3\ncreated_at: &1 2021-08-04 09:42:50.914251000 Z\nupdated_at: *1\n', '2021-08-04 09:45:59') TRANSACTION (6.9ms) COMMIT => true
但し、SQLite3はオーバーしても保存出来てします。
stringもそうだけど、サイズ指定という概念がなさそう。
> test.price65_0 = 10 ** 65 => 100000000000000000000000000000000000000000000000000000000000000000 > test.save TRANSACTION (0.0ms) begin transaction Test Create (0.5ms) INSERT INTO "tests" ("price", "price65_0", "price65_2", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["price", 100.5], ["price65_0", 100000000000000000000000000000000000000000000000000000000000000000], ["price65_2", 100.5], ["created_at", "2021-08-04 09:56:49.499984"], ["updated_at", "2021-08-04 09:56:49.499984"]] PaperTrail::Version Create (0.3ms) INSERT INTO "versions" ("item_type", "item_id", "event", "created_at") VALUES (?, ?, ?, ?) [["item_type", "Test"], ["item_id", 2], ["event", "create"], ["created_at", "2021-08-04 09:56:49.499984"]] TRANSACTION (0.3ms) commit transaction => true
結論
decimalもサイズ指定すれば、DB変えても挙動は変わらなくなる。
そもそも、小数が不要なら指定した方が後の実装が楽。
昔なら、ほぼ全て最大サイズ意識していたが、今はリソースに余裕があるので、一部だけ意識すれば良さそう。