-
Notifications
You must be signed in to change notification settings - Fork 0
/
active_record_validations.html
1116 lines (991 loc) · 61.7 KB
/
active_record_validations.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Active Record 驗證 — Ruby on Rails 指南</title>
<meta name="description" content="Ruby on Rails 指南:系統學習 Rails(Rails 4.2 版本)" >
<meta name="keywords" content="Ruby on Rails Guides 指南 中文 學習 免費 網路 Web 開發" >
<meta name="author" content="http://git.io/G_R1sA">
<meta property="fb:admins" content="1340181291">
<meta property="og:title" content="Active Record 驗證 — Ruby on Rails 指南" >
<meta property="og:site_name" content="Ruby on Rails 指南">
<meta property="og:image" content="http://rails.ruby.tw/images/rails_guides_cover.jpg">
<meta property="og:url" content="http://rails.ruby.tw/">
<meta property="og:type" content="article">
<meta property="og:description" content="Ruby on Rails 指南:系統學習 Rails(Rails 4.2 版本)">
<link rel="stylesheet" href="stylesheets/application.css">
<link href="http://fonts.googleapis.com/css?family=Noto+Sans:400,700|Noto+Serif:700|Source+Code+Pro" rel="stylesheet">
<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon">
</head>
<body class="guide">
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/zh-TW/sdk.js#xfbml=1&appId=837401439623727&version=v2.0";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<script type="text/javascript">
window.twttr=(function(d,s,id){var t,js,fjs=d.getElementsByTagName(s)[0];if(d.getElementById(id)){return}js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);return window.twttr||(t={_e:[],ready:function(f){t._e.push(f)}})}(document,"script","twitter-wjs"));
</script>
<div id="topNav">
<div class="wrapper">
<strong class="more-info-label">更多內容 <a href="http://rubyonrails.org/">rubyonrails.org:</a></strong>
<span class="red-button more-info-button">
更多內容
</span>
<ul class="more-info-links s-hidden">
<li class="more-info"><a href="http://rubyonrails.org/">綜覽</a></li>
<li class="more-info"><a href="http://rubyonrails.org/download">下載</a></li>
<li class="more-info"><a href="http://rubyonrails.org/deploy">部署</a></li>
<li class="more-info"><a href="https://github.com/rails/rails">原始碼</a></li>
<li class="more-info"><a href="http://rubyonrails.org/screencasts">影片</a></li>
<li class="more-info"><a href="http://rubyonrails.org/documentation">文件</a></li>
<li class="more-info"><a href="http://rubyonrails.org/community">社群</a></li>
<li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
</ul>
</div>
</div>
<div id="header">
<div class="wrapper clearfix">
<h1><a href="index.html" title="回首頁">Guides.rubyonrails.org</a></h1>
<ul class="nav">
<li><a class="nav-item" href="index.html">首頁</a></li>
<li class="guides-index guides-index-large">
<a href="index.html" id="guidesMenu" class="guides-index-item nav-item">指南目錄</a>
<div id="guides" class="clearfix" style="display: none;">
<hr>
<dl class="L">
<dt>起步走</dt>
<dd><a href="getting_started.html">Rails 起步走</a></dd>
<dt>Models</dt>
<dd><a href="active_record_basics.html">Active Record 基礎</a></dd>
<dd><a href="active_record_migrations.html">Active Record 遷移</a></dd>
<dd><a href="active_record_validations.html">Active Record 驗證</a></dd>
<dd><a href="active_record_callbacks.html">Active Record 回呼</a></dd>
<dd><a href="association_basics.html">Active Record 關聯</a></dd>
<dd><a href="active_record_querying.html">Active Record 查詢</a></dd>
<dt>Views</dt>
<dd><a href="layouts_and_rendering.html">Rails 算繪與版型</a></dd>
<dd><a href="form_helpers.html">Action View 表單輔助方法</a></dd>
<dt>Controllers</dt>
<dd><a href="action_controller_overview.html">Action Controller 綜覽</a></dd>
<dd><a href="routing.html">Rails 路由:深入淺出</a></dd>
</dl>
<dl class="R">
<dt>深入了解</dt>
<dd><a href="active_support_core_extensions.html">Active Support 核心擴展</a></dd>
<dd><a href="i18n.html">Rails 國際化 API</a></dd>
<dd><a href="action_mailer_basics.html">Action Mailer 基礎</a></dd>
<dd><a href="active_job_basics.html">Active Job 基礎</a></dd>
<dd><a href="security.html">Rails 安全指南</a></dd>
<dd><a href="debugging_rails_applications.html">除錯 Rails 應用程式</a></dd>
<dd><a href="configuring.html">Rails 應用程式設定</a></dd>
<dd><a href="command_line.html">Rake 任務與 Rails 命令列工具</a></dd>
<dd><a href="asset_pipeline.html">Asset Pipeline</a></dd>
<dd><a href="working_with_javascript_in_rails.html">在 Rails 使用 JavaScript</a></dd>
<dd><a href="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</a></dd>
<dt>擴充 Rails</dt>
<dd><a href="rails_on_rack.html">Rails on Rack</a></dd>
<dd><a href="generators.html">客製與新建 Rails 產生器</a></dd>
<dd><a href="rails_application_templates.html">Rails 應用程式模版</a></dd>
<dt>貢獻 Ruby on Rails</dt>
<dd><a href="contributing_to_ruby_on_rails.html">貢獻 Ruby on Rails</a></dd>
<dd><a href="api_documentation_guidelines.html">API 文件準則</a></dd>
<dd><a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南準則</a></dd>
<dt>維護方針</dt>
<dd><a href="maintenance_policy.html">維護方針</a></dd>
<dt>發佈記</dt>
<dd><a href="upgrading_ruby_on_rails.html">升級 Ruby on Rails</a></dd>
<dd><a href="4_2_release_notes.html">Ruby on Rails 4.2 發佈記</a></dd>
<dd><a href="4_1_release_notes.html">Ruby on Rails 4.1 發佈記</a></dd>
<dd><a href="4_0_release_notes.html">Ruby on Rails 4.0 發佈記</a></dd>
<dd><a href="3_2_release_notes.html">Ruby on Rails 3.2 發佈記</a></dd>
<dd><a href="3_1_release_notes.html">Ruby on Rails 3.1 發佈記</a></dd>
<dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 發佈記</a></dd>
<dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 發佈記</a></dd>
<dd><a href="2_2_release_notes.html">Ruby on Rails 2.2 發佈記</a></dd>
<dt>Rails 指南翻譯術語</dt>
<dd><a href="translation_terms.html">翻譯術語</a></dd>
</dl>
</div>
</li>
<li><a class="nav-item" href="//github.com/docrails-tw/guides">貢獻翻譯</a></li>
<li><a class="nav-item" href="contributing_to_ruby_on_rails.html">貢獻</a></li>
<li><a class="nav-item" href="credits.html">致謝</a></li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">指南目錄</option>
<optgroup label="起步走">
<option value="getting_started.html">Rails 起步走</option>
</optgroup>
<optgroup label="Models">
<option value="active_record_basics.html">Active Record 基礎</option>
<option value="active_record_migrations.html">Active Record 遷移</option>
<option value="active_record_validations.html">Active Record 驗證</option>
<option value="active_record_callbacks.html">Active Record 回呼</option>
<option value="association_basics.html">Active Record 關聯</option>
<option value="active_record_querying.html">Active Record 查詢</option>
</optgroup>
<optgroup label="Views">
<option value="layouts_and_rendering.html">Rails 算繪與版型</option>
<option value="form_helpers.html">Action View 表單輔助方法</option>
</optgroup>
<optgroup label="Controllers">
<option value="action_controller_overview.html">Action Controller 綜覽</option>
<option value="routing.html">Rails 路由:深入淺出</option>
</optgroup>
<optgroup label="深入了解">
<option value="active_support_core_extensions.html">Active Support 核心擴展</option>
<option value="i18n.html">Rails 國際化 API</option>
<option value="action_mailer_basics.html">Action Mailer 基礎</option>
<option value="active_job_basics.html">Active Job 基礎</option>
<option value="security.html">Rails 安全指南</option>
<option value="debugging_rails_applications.html">除錯 Rails 應用程式</option>
<option value="configuring.html">Rails 應用程式設定</option>
<option value="command_line.html">Rake 任務與 Rails 命令列工具</option>
<option value="asset_pipeline.html">Asset Pipeline</option>
<option value="working_with_javascript_in_rails.html">在 Rails 使用 JavaScript</option>
<option value="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</option>
</optgroup>
<optgroup label="擴充 Rails">
<option value="rails_on_rack.html">Rails on Rack</option>
<option value="generators.html">客製與新建 Rails 產生器</option>
<option value="rails_application_templates.html">Rails 應用程式模版</option>
</optgroup>
<optgroup label="貢獻 Ruby on Rails">
<option value="contributing_to_ruby_on_rails.html">貢獻 Ruby on Rails</option>
<option value="api_documentation_guidelines.html">API 文件準則</option>
<option value="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南準則</option>
</optgroup>
<optgroup label="維護方針">
<option value="maintenance_policy.html">維護方針</option>
</optgroup>
<optgroup label="發佈記">
<option value="upgrading_ruby_on_rails.html">升級 Ruby on Rails</option>
<option value="4_2_release_notes.html">Ruby on Rails 4.2 發佈記</option>
<option value="4_1_release_notes.html">Ruby on Rails 4.1 發佈記</option>
<option value="4_0_release_notes.html">Ruby on Rails 4.0 發佈記</option>
<option value="3_2_release_notes.html">Ruby on Rails 3.2 發佈記</option>
<option value="3_1_release_notes.html">Ruby on Rails 3.1 發佈記</option>
<option value="3_0_release_notes.html">Ruby on Rails 3.0 發佈記</option>
<option value="2_3_release_notes.html">Ruby on Rails 2.3 發佈記</option>
<option value="2_2_release_notes.html">Ruby on Rails 2.2 發佈記</option>
</optgroup>
<optgroup label="Rails 指南翻譯術語">
<option value="translation_terms.html">翻譯術語</option>
</optgroup>
</select>
</li>
</ul>
</div>
</div>
</div>
<hr class="hide">
<div id="feature">
<div class="wrapper">
<h2>Active Record 驗證</h2><p>本篇教您如何使用 Active Record 的驗證功能,在資料存入資料庫前,驗證物件的狀態。</p><p>讀完本篇,您將了解:</p>
<ul>
<li>如何使用 Active Record 內建的驗證輔助方法。</li>
<li>如何新建自己的驗證方法。</li>
<li>如何處理驗證時所產生的錯誤訊息。</li>
</ul>
<div id="subCol">
<h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />Chapters</h3>
<ol class="chapters">
<li>
<a href="#%E9%A9%97%E8%AD%89%E7%B6%9C%E8%A6%BD">驗證綜覽</a>
<ul>
<li><a href="#%E7%82%BA%E4%BB%80%E9%BA%BC%E8%A6%81%E9%A9%97%E8%AD%89%EF%BC%9F">為什麼要驗證?</a></li>
<li><a href="#%E9%A9%97%E8%AD%89%E4%BD%95%E6%99%82%E7%99%BC%E7%94%9F%EF%BC%9F">驗證何時發生?</a></li>
<li><a href="#%E7%95%A5%E9%81%8E%E9%A9%97%E8%AD%89">略過驗證</a></li>
<li><a href="#valid-questionmark-%E8%88%87-invalid-questionmark"><code>valid?</code> 與 <code>invalid?</code></a></li>
<li><a href="#%E9%A9%97%E8%AD%89%E7%B6%9C%E8%A6%BD-errors"><code>errors[]</code></a></li>
</ul>
</li>
<li>
<a href="#%E9%A9%97%E8%AD%89%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95">驗證輔助方法</a>
<ul>
<li><a href="#acceptance"><code>acceptance</code></a></li>
<li><a href="#validates-associated"><code>validates_associated</code></a></li>
<li><a href="#confirmation"><code>confirmation</code></a></li>
<li><a href="#exclusion"><code>exclusion</code></a></li>
<li><a href="#format"><code>format</code></a></li>
<li><a href="#inclusion"><code>inclusion</code></a></li>
<li><a href="#length"><code>length</code></a></li>
<li><a href="#numericality"><code>numericality</code></a></li>
<li><a href="#presence"><code>presence</code></a></li>
<li><a href="#absence"><code>absence</code></a></li>
<li><a href="#uniqueness"><code>uniqueness</code></a></li>
<li><a href="#validates-with"><code>validates_with</code></a></li>
<li><a href="#validates-each"><code>validates_each</code></a></li>
</ul>
</li>
<li>
<a href="#%E5%B8%B8%E8%A6%8B%E9%A9%97%E8%AD%89%E9%81%B8%E9%A0%85">常見驗證選項</a>
<ul>
<li><a href="#allow-nil"><code>:allow_nil</code></a></li>
<li><a href="#allow-blank"><code>:allow_blank</code></a></li>
<li><a href="#message"><code>:message</code></a></li>
<li><a href="#on"><code>:on</code></a></li>
</ul>
</li>
<li><a href="#%E5%9A%B4%E6%A0%BC%E9%A9%97%E8%AD%89">嚴格驗證</a></li>
<li>
<a href="#%E6%A2%9D%E4%BB%B6%E5%BC%8F%E9%A9%97%E8%AD%89">條件式驗證</a>
<ul>
<li><a href="#if-%E8%88%87-unless%EF%BC%9A%E4%BD%BF%E7%94%A8-symbol"><code>:if</code> 與 <code>:unless</code>:使用 Symbol</a></li>
<li><a href="#if-%E8%88%87-unless%EF%BC%9A%E4%BD%BF%E7%94%A8-string"><code>:if</code> 與 <code>:unless</code>:使用 String</a></li>
<li><a href="#if-%E8%88%87-unless%EF%BC%9A%E4%BD%BF%E7%94%A8-proc"><code>:if</code> 與 <code>:unless</code>:使用 <code>Proc</code></a></li>
<li><a href="#%E7%B5%84%E5%90%88%E6%A2%9D%E4%BB%B6%E5%BC%8F%E9%A9%97%E8%AD%89">組合條件式驗證</a></li>
<li><a href="#%E7%B5%90%E5%90%88%E9%A9%97%E8%AD%89%E6%A2%9D%E4%BB%B6">結合驗證條件</a></li>
</ul>
</li>
<li>
<a href="#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%AE%9A%E9%A9%97%E8%AD%89">使用自定驗證</a>
<ul>
<li><a href="#%E8%87%AA%E5%AE%9A-validators">自定 Validators</a></li>
<li><a href="#%E8%87%AA%E5%AE%9A%E6%96%B9%E6%B3%95">自定方法</a></li>
</ul>
</li>
<li>
<a href="#%E8%99%95%E7%90%86%E9%A9%97%E8%AD%89%E9%8C%AF%E8%AA%A4">處理驗證錯誤</a>
<ul>
<li><a href="#%E8%99%95%E7%90%86%E9%A9%97%E8%AD%89%E9%8C%AF%E8%AA%A4-errors"><code>errors</code></a></li>
<li><a href="#errors"><code>errors[]</code></a></li>
<li><a href="#errors-add"><code>errors.add</code></a></li>
<li><a href="#errors-base"><code>errors[:base]</code></a></li>
<li><a href="#errors-clear"><code>errors.clear</code></a></li>
<li><a href="#errors-size"><code>errors.size</code></a></li>
</ul>
</li>
<li><a href="#%E5%9C%A8-view-%E9%A1%AF%E7%A4%BA%E9%A9%97%E8%AD%89%E5%A4%B1%E6%95%97%E8%A8%8A%E6%81%AF">在 View 顯示驗證失敗訊息</a></li>
</ol>
</div>
</div>
</div>
<div id="container">
<div class="wrapper">
<div id="mainCol">
<h3 id="驗證綜覽">1 驗證綜覽</h3><p>以下是一個簡單的例子:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
</pre>
</div>
<p>如您所見,透過驗證,我們知道 <code>Person</code> 必須要有 <code>name</code> 屬性才算有效。上例第二個建立的 <code>Person</code>,因為缺少了 <code>name</code>,則不會存至資料庫。</p><p>在深入了解之前,先談談驗證在應用程式裡所扮演的角色。</p><h4 id="為什麼要驗證?">1.1 為什麼要驗證?</h4><p>驗證用來確保只有有效的資料才能存入資料庫。譬如每個使用者需要填寫有效的 E-mail 與郵寄地址。在 Model 層級驗證資料是最好的,只有通過驗證的資料方可存入資料庫。因為在 Model 層面驗證,不需要考慮資料庫的種類、無法在用戶端(瀏覽器)跳過驗證、且更容易測試與維護。Rails 使得資料驗證用起來非常簡單,提供了各種內建輔助方法,來滿足常見的需求,也可以新建自定的驗證方法。</p><p>在存入資料庫前有好幾種驗證方法,包含了原生的資料庫約束(constraint)、用戶端驗證、Controller 層級驗證。以下是各種方法的優缺點:</p>
<ul>
<li>資料庫約束和 stored procedure 驗證機制只適用單一資料庫,不好測試,也更難維護。但若是其它應用程式也使用您的資料庫,加上資料庫層級的約束可能比較好。除此之外,資料庫層級的驗證可以安全地處理某些問題(像是在使用頻繁的資料表裡檢查唯一性),這倘若不在資料庫層級做,其它層級做起來可能很困難。</li>
<li>用戶端驗證很有用,但單獨使用時可靠性不高。若是用 JavaScript 實作,關掉 JavaScript 便可跳過驗證。但結合其它種驗證方式,用戶端驗證可提供使用者即時的反饋。</li>
<li>Controller 層級驗證聽起來很誘人,但用起來很笨重,也很難測試與維護。不管怎麼說,盡量保持 Controller 輕巧短小,長遠下來看,應用程式會更好維護。</li>
</ul>
<p>根據不同場合選擇不同驗證方式。Rails 團隊的觀點是 Model 層級的驗證,最符合多數應用場景。</p><h4 id="驗證何時發生?">1.2 驗證何時發生?</h4><p>Active Record 物件有兩種:一種對應到資料庫的列、另一種沒有。當新建一個新的物件時,比如使用 <code>new</code> 方法,物件此時並不屬於資料庫。一旦對物件呼叫 <code>save</code>,則物件會存入對應的資料表裡。Active Record 使用 <code>new_record?</code> 這個實體方法來決定物件是否已存在資料庫。看看下面這個簡單的 Active Record 類別:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
end
</pre>
</div>
<p>可以在 <code>rails console</code> 下試試這是怎麼工作的:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
$ bin/rails console
>> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
>> p.new_record?
=> true
>> p.save
=> true
>> p.new_record?
=> false
</pre>
</div>
<p>新建與儲存新紀錄(record),會對資料庫做 SQL 的 <code>INSERT</code> 操作。更新已存在的記錄則會做 <code>UPDATE</code>。驗證通常在這些 SQL 執行之前就發生了。如果驗證失敗,則物件會被標示為無效的,Active Record 便不會執行 <code>INSERT</code> 或是 <code>UPDATE</code>。這避免了存入無效的物件到資料庫。您可以指定在物件建立時、儲存時、更新時,各個階段要做何種資料驗證。</p><div class="warning"><p>有許多種方法可以改變資料庫裡物件的狀態。某些方法會觸發驗證、某些不會。這表示有可能會不小心將無效的物件存入資料庫。</p></div><p>以下方法會觸發驗證,只會在物件有效時,把物件存入資料庫:</p>
<ul>
<li><code>create</code></li>
<li><code>create!</code></li>
<li><code>save</code></li>
<li><code>save!</code></li>
<li><code>update</code></li>
<li><code>update!</code></li>
</ul>
<p>這些方法對應的 BANG 版本(比如 <code>save!</code>),會對無效的記錄拋出異常。非 BANG 方法則不會,<code>save</code> 與 <code>update</code> 僅回傳 <code>false</code>,<code>create</code> 僅回傳物件本身。</p><h4 id="略過驗證">1.3 略過驗證</h4><p>以下這些方法會略過驗證,將物件存入資料庫時不會考慮資料的有效性。應謹慎使用。</p>
<ul>
<li><code>decrement!</code></li>
<li><code>decrement_counter</code></li>
<li><code>increment!</code></li>
<li><code>increment_counter</code></li>
<li><code>toggle!</code></li>
<li><code>touch</code></li>
<li><code>update_all</code></li>
<li><code>update_attribute</code></li>
<li><code>update_column</code></li>
<li><code>update_columns</code></li>
<li><code>update_counters</code></li>
</ul>
<p>注意 <code>save</code> 也能夠略過驗證,傳入 <code>validate: false</code> 作為參數即可。這個技巧要小心使用。</p>
<ul>
<li><code>save(validate: false)</code></li>
</ul>
<h4 id="valid-questionmark-與-invalid-questionmark">1.4 <code>valid?</code> 與 <code>invalid?</code>
</h4><p>檢查物件是否有效,Rails 使用的是 <code>valid?</code> 方法。可以直接呼叫此方法來觸發驗證。物件若沒有錯誤會回傳 <code>true</code>,反之回傳 <code>false</code>。前面已經見過了:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
</pre>
</div>
<p>Active Record 做完驗證後,所有找到的錯誤都可透過 <code>errors.messages</code> 這個實體方法來存取,會回傳錯誤集合。就定義來說,物件做完驗證後,錯誤集合為空才是有效的。</p><p>注意到用 <code>new</code> 實體化出來的物件,即便有錯誤也不會說,因為 <code>new</code> 不會觸發任何驗證。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true
end
>> p = Person.new
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {}
>> p.valid?
# => false
>> p.errors.messages
# => {name:["can't be blank"]}
>> p = Person.create
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {name:["can't be blank"]}
>> p.save
# => false
>> p.save!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
>> Person.create!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
</pre>
</div>
<p><code>invalid?</code> 是 <code>valid?</code> 的反相。物件找到任何錯誤回傳 <code>true</code>,反之回傳 <code>false</code>。</p><h4 id="驗證綜覽-errors">1.5 <code>errors[]</code>
</h4><p>要檢查物件的特定屬性是否有效,可以使用 <code>errors[:attribute]</code>,會以陣列形式返回該屬性的所有錯誤,沒有錯誤則返回空陣列。</p><p>這個方法只有在驗證後呼叫才有用,因為它只是檢查 <code>errors</code> 集合,而不會觸發驗證。<code>errors[:attribute]</code> 與 <code>ActiveRecord::Base#invalid?</code> 方法不同,因為它不是檢查整個物件的有效性,只是檢查物件單一屬性是否有錯誤。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true
end
>> Person.new.errors[:name].any? # => false
>> Person.create.errors[:name].any? # => true
</pre>
</div>
<p>在 <a href="#">7 處理驗證錯誤</a>一節會更深入講解驗證錯誤。現在讓我們看看 Rails 內建的驗證輔助方法有那些。</p><h3 id="驗證輔助方法">2 驗證輔助方法</h3><p>Active Record 預先定義了許多驗證用的輔助方法,供您直接在類別定義中使用。這些輔助方法提供了常見的驗證規則。每當驗證失敗時,驗證訊息會新增到物件的 <code>errors</code> 集合,這個訊息與出錯的屬性是相關聯的。</p><p>每個輔助方法皆接受任意數量的屬性名稱,所以一行程式碼,便可給多個屬性加入同樣的驗證。</p><p>所有的輔助方法皆接受 <code>:on</code> 與 <code>:message</code> 選項,分別用來指定何時做驗證、出錯時的錯誤訊息。每個驗證輔助方法都有預設的錯誤訊息。這些訊息在沒有指定 <code>:message</code> 選項時很有用。讓我們看看每一個可用的輔助方法。</p><h4 id="acceptance">2.1 <code>acceptance</code>
</h4><p>這個方法在表單送出時,檢查 UI 的 checkbox 是否有打勾。這對於使用者需要接受服務條款、隱私權政策等相關的場景下很有用。這個驗證僅針對網頁應用程式,且不需要存入資料庫(如果沒有為 <code>acceptance</code> 開一個欄位,輔助方法自己會使用一個虛擬屬性)。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :terms_of_service, acceptance: true
end
</pre>
</div>
<p>這個輔助方法預設的錯誤訊息是 <em>"must be accepted"</em>。</p><p>這個方法接受一個 <code>:accept</code> 選項,用來決定什麼值代表“接受”。預設是 “1”,改成別的也很簡單。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :terms_of_service, acceptance: { accept: 'yes' }
end
</pre>
</div>
<h4 id="validates-associated">2.2 <code>validates_associated</code>
</h4><p>當 Model 與其它 Model 有關聯,且與之關聯的 Model 也需要驗證時,用這個方法來處理。在儲存物件時,會對相關聯的物件呼叫 <code>valid?</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Library < ActiveRecord::Base
has_many :books
validates_associated :books
end
</pre>
</div>
<p>所有的關聯類型皆適用此方法。</p><div class="warning"><p>不要在關聯的兩邊都使用 <code>validates_associated</code>。它們會互相呼叫陷入無窮迴圈。</p></div><p><code>validates_associated</code> 預設錯誤訊息是 <em>"is invalid"</em>。注意到每個關聯的物件會有自己的 <code>errors</code> 集合。錯誤不會集中到呼叫該方法的 Model。</p><h4 id="confirmation">2.3 <code>confirmation</code>
</h4><p>當有兩個 text field 內容需要完全相同時,使用這個方法。比如可能想要確認 E-mail 或密碼兩次輸入是否相同。這個驗證會新建一個虛擬屬性,名字是該欄位(field)的名稱,後面加上 <code>_confirmation</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :email, confirmation: true
end
</pre>
</div>
<p>在 View 模版(template)裡,可以這麼用:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>
</pre>
</div>
<p>只有 <code>email_confirmation</code> 不為 <code>nil</code> 時,才會做驗證。需要確認的話,記得要給 <code>email_confirmation</code> 屬性加上存在性(presence)驗證(稍後介紹 <code>presence</code>):</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :email, confirmation: true
validates :email_confirmation, presence: true
end
</pre>
</div>
<p><code>confirmation</code> 預設錯誤訊息是 <em>"doesn't match confirmation"</em>。</p><h4 id="exclusion">2.4 <code>exclusion</code>
</h4><p>這個方法驗證屬性是否“不屬於”某個給定的集合。集合可以是任何 <code>Enumerable</code> 的物件。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Account < ActiveRecord::Base
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
end
</pre>
</div>
<p><code>exclusion</code> 有 <code>:in</code> 選項,接受一組數值,決定屬性“不可接受”的值。<code>:in</code> 別名為 <code>:within</code>。上例使用了 <code>:message</code> 選項來示範如何在錯誤訊息裡印出屬性的值。</p><p><code>exclusion</code> 預設錯誤訊息是 <em>"is reserved"</em>。</p><h4 id="format">2.5 <code>format</code>
</h4><p>這個方法驗證屬性的值是否匹配一個透過 <code>:with</code> 給定的正規表達式。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Product < ActiveRecord::Base
validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
message: "only allows letters" }
end
</pre>
</div>
<p>也可以使用 <code>:without</code> 來指定沒有匹配的屬性。</p><p><code>format</code> 預設錯誤訊息是 <em>"is invalid"</em>。</p><h4 id="inclusion">2.6 <code>inclusion</code>
</h4><p>這個方法驗證屬性是否“屬於”某個給定的集合。集合可以是任何 <code>Enumerable</code> 的物件。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Coffee < ActiveRecord::Base
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }
end
</pre>
</div>
<p><code>inclusion</code> 有 <code>:in</code> 選項,接受一組數值,決定屬性“可接受”的值。<code>:in</code> 的別名為 <code>:within</code>。上例使用了 <code>:message</code> 選項來示範如何在錯誤訊息裡印出屬性的值。</p><p><code>inclusion</code> 預設錯誤訊息是 <em>"is not included in the list"</em>。</p><h4 id="length">2.7 <code>length</code>
</h4><p>這個方法驗證屬性值的長度。有多種選項來限制長度(如下所示):</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
</pre>
</div>
<p>長度限制選項有:</p>
<ul>
<li>
<code>:minimum</code> - 屬性值的長度的最小值。</li>
<li>
<code>:maximum</code> - 屬性值的長度的最大值。</li>
<li>
<code>:in</code> (or <code>:within</code>) - 屬性值的長度所屬的區間。這個選項的值必須是一個範圍。</li>
<li>
<code>:is</code> - T屬性值的長度必須等於。</li>
</ul>
<p>預設錯誤訊息取決於用的是那種長度驗證方法。可以使用 <code>:wrong_length</code>、<code>too_long</code>、<code>too_short</code> 選項,以及 <code>%{count}</code> 來客製化訊息。使用 <code>:message</code> 也是可以的。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :bio, length: { maximum: 1000,
too_long: "%{count} characters is the maximum allowed" }
end
</pre>
</div>
<p>這個方法計算長度的預設單位是字元。但可以用 <code>:tokenizer</code> 選項來修改,比如取一個字為最小單位:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Essay < ActiveRecord::Base
validates :content, length: {
minimum: 300,
maximum: 400,
tokenizer: lambda { |str| str.scan(/\w+/) },
too_short: "must have at least %{count} words",
too_long: "must have at most %{count} words"
}
end
</pre>
</div>
<p>注意到預設的錯誤訊息是複數。(例如,"is too short (minimum
is %{count} characters)")。故當 <code>:minimum</code> 為 1 時,要提供一個自訂的訊息,或者是使用 <code>presence: true</code> 取代。當 <code>:in</code> 或 <code>:within</code> 下限小於 1 時,應該要提供一個自訂的訊息,或者是在驗證 <code>length</code> 之前,先驗證 <code>presence</code>。</p><h4 id="numericality">2.8 <code>numericality</code>
</h4><p>這個方法驗證屬性是不是純數字。預設會匹配帶有正負號(可選)的整數或浮點數。只允許整數可以透過將 <code>:only_integer</code> 為 <code>true</code>。</p><p><code>:only_integer</code> 為 <code>true</code>,會使用下面的正規表達式來檢查屬性的值:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
/\A[+-]?\d+\Z/
</pre>
</div>
<p>否則會嘗試使用 <code>Float</code> 將值轉為數字。</p><div class="warning"><p>注意上面的正規表達式允許最後有新行字元。</p></div><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Player < ActiveRecord::Base
validates :points, numericality: true
validates :games_played, numericality: { only_integer: true }
end
</pre>
</div>
<p>除了 <code>only_integer</code> 之外,這個方法也接受下列選項,用來限制允許的數值:</p>
<ul>
<li>
<code>:greater_than</code> - 屬性的值必須大於指定的值。預設錯誤訊息是 <em>"must be greater than %{count}"</em>。</li>
<li>
<code>:greater_than_or_equal_to</code> - 屬性的值必須大於等於指定的值。預設錯誤訊息是 <em>"must be greater than or equal to %{count}"</em>。</li>
<li>
<code>:equal_to</code> - 屬性的值必須等於指定的值。預設錯誤訊息是 <em>"must be equal to %{count}"</em>。</li>
<li>
<code>:less_than</code> - 屬性的值必須小於指定的值。預設錯誤訊息是 <em>"must be less than %{count}"</em>。</li>
<li>
<code>:less_than_or_equal_to</code> - 屬性的值必須小於等於指定的值。預設錯誤訊息是 <em>"must be less than or equal to %{count}"</em>。</li>
<li>
<code>:odd</code> - 若 <code>:odd</code> 設為 <code>true</code>,則屬性的值必須是奇數。預設錯誤訊息是 <em>"must be odd"</em>。</li>
<li>
<code>:even</code> - 若 <code>:even</code> 設為 <code>true</code>,則屬性的值必須是偶數。預設錯誤訊息是 <em>"must be even"</em>。</li>
</ul>
<p><code>numericality</code> 預設錯誤訊息是 <em>"is not a number"</em>。</p><h4 id="presence">2.9 <code>presence</code>
</h4><p>這個方法驗證指定的屬性是否“存在”。使用 <code>blank?</code> 來檢查數值是否為 <code>nil</code> 或空字串(僅有空白的字串也是空字串)。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, :login, :email, presence: true
end
</pre>
</div>
<p>想確保關聯物件是否存在,需要檢查關聯物件本身,而不是檢查對應的外鍵。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LineItem < ActiveRecord::Base
belongs_to :order
validates :order, presence: true
end
</pre>
</div>
<p>而在 <code>Order</code> 這一邊,要用 <code>inverse_of</code> 來檢查關聯的物件是否存在。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
</pre>
</div>
<p>如透過 <code>has_one</code> 或 <code>has_many</code> 關係來驗證關聯的物件是否存在,則會對該物件呼叫 <code>blank?</code> 與 <code>marked_for_destruction?</code>,來確定存在性。</p><p>由於 <code>false.blank?</code> 為 <code>true</code>,如果想驗證布林欄位的存在性,應該要使用下列的驗證方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
validates :boolean_field_name, presence: true
validates :boolean_field_name, inclusion: { in: [true, false] }
validates :boolean_field_name, exclusion: { in: [nil] }
</pre>
</div>
<p>預設錯誤訊息是 <em>"can't be blank"</em>。</p><h4 id="absence">2.10 <code>absence</code>
</h4><p>這個方法驗證是否“不存在”。使用 <code>present?</code> 來檢查數值是否為非 <code>nil</code> 或非空字串(僅有空白的字串也是空字串)。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, :login, :email, absence: true
end
</pre>
</div>
<p>想確保關聯物件是否“不存在”,需要檢查關聯物件本身,而不是檢查對應的外鍵。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LineItem < ActiveRecord::Base
belongs_to :order
validates :order, absence: true
end
</pre>
</div>
<p>而在 <code>Order</code> 這一邊,要用 <code>inverse_of</code> 來檢查關聯的物件是否不存在。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
</pre>
</div>
<p>如透過 <code>has_one</code> 或 <code>has_many</code> 關係來驗證關聯的物件是否存在,則會對該物件呼叫 <code>present?</code> 與 <code>marked_for_destruction?</code>,來確定不存在性。</p><p>由於 <code>false.present?</code> 為 <code>false</code>,如果想驗證布林欄位的存在性,應該要使用 <code>validates :field_name, exclusion: { in: [true, false] }</code>。</p><p>預設錯誤訊息是 <em>"must be blank"</em>。</p><h4 id="uniqueness">2.11 <code>uniqueness</code>
</h4><p>這個方法在物件儲存前,驗證屬性值是否是唯一的。此方法只是在應用層面檢查,不對資料庫做約束。同時有兩個資料庫連接,便有可能建立出兩個相同的紀錄。要避免則是需要在資料庫加上 unique 索引,請參考 <a href="http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html">MySQL 手冊</a>來了解多欄索引該怎麼做。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Account < ActiveRecord::Base
validates :email, uniqueness: true
end
</pre>
</div>
<p>這個驗證透過對 Model 的資料表執行一條 SQL 查詢語句,搜尋是否已經有同樣數值的紀錄存在。</p><p><code>:scope</code> 選項可以用另一個屬性來限制唯一性:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Holiday < ActiveRecord::Base
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
</pre>
</div>
<p>另有 <code>:case_sensitive</code> 選項可以用來定義是否要分大小寫。此選項預設開啟。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, uniqueness: { case_sensitive: false }
end
</pre>
</div>
<div class="warning"><p>注意某些資料庫預設搜尋是不分大小寫的。</p></div><p>預設錯誤訊息是 <em>"has already been taken"</em>。</p><h4 id="validates-with">2.12 <code>validates_with</code>
</h4><p>這個方法將記錄傳入,另開一類別來驗證。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
record.errors[:base] << "This person is evil"
end
end
end
class Person < ActiveRecord::Base
validates_with GoodnessValidator
end
</pre>
</div>
<div class="note"><p>注意錯誤會加到 <code>record.errors[:base]</code>。這個錯誤與整個物件有關,不單屬於某個屬性。</p></div><p><code>validates_with</code> 方法接受一個類別,或一組類別。<code>validates_with</code> 沒有預設錯誤訊息。你必須要手動新增錯誤到記錄的 <code>errors</code> 集合。</p><p>實作 <code>validate</code> 方法時,參數必須要有 <code>record</code>,來表示要被驗證的那條記錄。</p><p>與所有的驗證類似,<code>validates_with</code> 接受 <code>:if</code>、<code>:unless</code>,以及 <code>:on</code> 選項。如果傳入其它的選項,預設會被放入 <code>options</code> Hash(參考下例):</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any? { |field| record.send(field) == "Evil" }
record.errors[:base] << "This person is evil"
end
end
end
class Person < ActiveRecord::Base
validates_with GoodnessValidator, fields: [:first_name, :last_name]
end
</pre>
</div>
<p>注意自己寫的這個驗證類別(上例為 <code>GoodnessValidator</code>),在應用程式生命週期內<strong>只會實體化一次</strong>,而不是每次驗證時就實體化一次。所以使用實體變數時要很小心。</p><p>如果驗證類別足夠複雜的話,需要用到實體變數,可以用純 Ruby 物件(Plain Old Ruby Object, PORO) 來取代:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validate do |person|
GoodnessValidator.new(person).validate
end
end
class GoodnessValidator
def initialize(person)
@person = person
end
def validate
if some_complex_condition_involving_ivars_and_private_methods?
@person.errors[:base] << "This person is evil"
end
end
# ...
end
</pre>
</div>
<p><code>validates_with</code> 沒有預設錯誤訊息。</p><h4 id="validates-each">2.13 <code>validates_each</code>
</h4><p>這個方法採用區塊(block)來驗證屬性。沒有預先定義的驗證功能。可以在程式碼區塊裡寫要驗證的行為,<code>validates_each</code> 指定的每個屬性,會傳入區塊做驗證。比如下例檢查名與姓是否以小寫字母開頭:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates_each :name, :surname do |record, attr, value|
record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
end
end
</pre>
</div>
<p>這個區塊接受記錄、屬性名稱、屬性值。在區塊裡可以寫任何驗證行為。驗證失敗時應給 Model 新增錯誤訊息,才能把記錄標記成非法的。</p><p><code>validates_each</code> 沒有預設錯誤訊息。</p><h3 id="常見驗證選項">3 常見驗證選項</h3><p>以下是常見的驗證選項:</p><h4 id="allow-nil">3.1 <code>:allow_nil</code>
</h4><p><code>:allow_nil</code> 選項當驗證的值為 <code>nil</code> 時略過驗證。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Coffee < ActiveRecord::Base
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }, allow_nil: true
end
</pre>
</div>
<h4 id="allow-blank">3.2 <code>:allow_blank</code>
</h4><p><code>:allow_nil</code> 選項當驗證的值為 <code>blank?</code> 時,即 <code>blank?</code> 回傳 <code>true</code> 時,略過驗證。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Topic < ActiveRecord::Base
validates :title, length: { is: 5 }, allow_blank: true
end
Topic.create(title: "").valid? # => true
Topic.create(title: nil).valid? # => true
</pre>
</div>
<h4 id="message">3.3 <code>:message</code>
</h4><p>如上已經介紹過,<code>message</code> 選項可在驗證失敗時,加上錯誤訊息至 <code>errors</code> 集合。沒給入此選項時,Active Record 會使用預設的錯誤訊息。</p><h4 id="on">3.4 <code>:on</code>
</h4><p><code>:on</code> 選項可指定驗證發生時機。所有驗證方法預設在 <code>save</code> 時會觸發驗證(也就是新建與更新時)。也可以指定只在新建時做驗證 <code>on: :create</code>,或是只在更新時做驗證 <code>on: :update</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
# it will be possible to update email with a duplicated value
validates :email, uniqueness: true, on: :create
# it will be possible to create the record with a non-numerical age
validates :age, numericality: true, on: :update
# the default (validates on both create and update)
validates :name, presence: true
end
</pre>
</div>
<h3 id="嚴格驗證">4 嚴格驗證</h3><p>如傳入了 <code>strict: true</code>,當物件為無效時,會拋出 <code>ActiveModel::StrictValidationFailed</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: { strict: true }
end
Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
</pre>
</div>
<p>可自定要拋出的異常:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
Person.new.valid? # => TokenGenerationException: Token can't be blank
</pre>
</div>
<h3 id="條件式驗證">5 條件式驗證</h3><p>某些時候只有在物件滿足了給定條件時,再進行驗證比較合理。可以透過 <code>:if</code> 與 <code>:unless</code> 選項來辦到此事。它們接受符號、字串、<code>Proc</code> 或 <code>Array</code>。<code>:if</code> 可以指定驗證發生時機,而 <code>:unless</code> 則是指定驗證略過時機。</p><h4 id="if-與-unless:使用-symbol">5.1 <code>:if</code> 與 <code>:unless</code>:使用 Symbol</h4><p><code>:if</code> 與 <code>:uinless</code> 接受符號,這個符號代表了驗證執行之前所需呼叫的方法。這是最常見的用途。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Order < ActiveRecord::Base
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
</pre>
</div>
<h4 id="if-與-unless:使用-string">5.2 <code>:if</code> 與 <code>:unless</code>:使用 String</h4><p><code>:if</code> 與 <code>:unless</code> 也接受字串,字串需要是有效的 Ruby 程式碼,會使用 <code>eval</code> 來對字串求值。極短的條件式可以使用字串:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :surname, presence: true, if: "name.nil?"
end
</pre>
</div>
<h4 id="if-與-unless:使用-proc">5.3 <code>:if</code> 與 <code>:unless</code>:使用 <code>Proc</code>
</h4><p>最後,<code>:if</code> 與 <code>:unless</code> 也可以接受 <code>Proc</code> 物件。使用 <code>Proc</code> 可以寫把一行的條件式寫在區塊裡,而不用另外寫在方法裡。一行的條件式最適合用 <code>Proc</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Account < ActiveRecord::Base
validates :password, confirmation: true,
unless: Proc.new { |a| a.password.blank? }
end
</pre>
</div>
<h4 id="組合條件式驗證">5.4 組合條件式驗證</h4><p>有時候多個驗證需要共用一個條件式,可以透過 <code>with_options</code> 來實作:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class User < ActiveRecord::Base
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
end
</pre>
</div>
<p>所有在 <code>with_options</code> 區塊內的驗證都會傳入 <code>if: :is_admin?</code> 驗證。</p><h4 id="結合驗證條件">5.5 結合驗證條件</h4><p>另一方面來看,當驗證發生於否取決於多條條件時,可以使用 <code>Array</code>。此外,<code>:if</code> 與 <code>:unless</code> 也可以混用。以下是一個綜合的例子:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Computer < ActiveRecord::Base
validates :mouse, presence: true,
if: ["market.retail?", :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
</pre>
</div>
<p>這條驗證只在滿足了所有 <code>:if</code> 的條件,以及 <code>:unless</code> 條件求值結果為 <code>true</code> 時才執行。</p><h3 id="使用自定驗證">6 使用自定驗證</h3><p>當內建的驗證不夠用時,可以自己定義 validator 或驗證方法。</p><h4 id="自定-validators">6.1 自定 Validators</h4><p>自定 Validator 是擴展 <code>ActiveModel::Validator</code> 的類別,且必須實作 <code>validate</code> 方法,此方法接受 <code>record</code> 作為參數,驗證行為寫在這個方法裡。寫好 Validator,使用時則是用 <code>validates_with</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
</pre>
</div>
<p>加入自定 Validator 來驗證每一個屬性的最簡單方法是使用 <code>ActiveModel::EachValidator</code>。在這個例子裡,自定的 Validator 類別必須實作一個 <code>validate_each</code> 方法,接受三個參數,<code>record</code>、<code>attribute</code> 以及 <code>value</code>,分別對應到要驗證的紀錄、屬性、屬性值。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
class Person < ActiveRecord::Base
validates :email, presence: true, email: true
end
</pre>
</div>
<p>如上例所示,也可以在自定的 Validator 裡結合標準的驗證方法。</p><h4 id="自定方法">6.2 自定方法</h4><p>也可以寫方法來驗證 Model 的狀態,並在 Model 狀態無效的情況下將錯誤加入 <code>errors</code> 集合。必須使用 <code>validate</code> 這個類別方法來註冊。</p><p>這個類別方法接受多個符號,執行的順序按照註冊的順序。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Invoice < ActiveRecord::Base
validate :expiration_date_cannot_be_in_the_past,
:discount_cannot_be_greater_than_total_value
def expiration_date_cannot_be_in_the_past
if expiration_date.present? && expiration_date < Date.today
errors.add(:expiration_date, "can't be in the past")
end
end
def discount_cannot_be_greater_than_total_value
if discount > total_value
errors.add(:discount, "can't be greater than total value")
end
end
end
</pre>
</div>
<p>預設情況下,每當你呼叫 <code>valid?</code> 時,都會執行這些自定的驗證方法。也可以透過 <code>:on</code> 來決定何時觸發自定驗證方法,可指定 <code>:create</code> 或 <code>:update</code>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Invoice < ActiveRecord::Base
validate :active_customer, on: :create
def active_customer
errors.add(:customer_id, "is not active") unless customer.active?
end
end
</pre>
</div>
<h3 id="處理驗證錯誤">7 處理驗證錯誤</h3><p>除了前面介紹過的 <code>valid?</code> 與 <code>invalid?</code> 之外,Rails 提供了許多方法來處理 <code>errors</code> 集合、查詢物件的有效性。</p><p>以下是最常使用的方法。請參考 <code>ActiveModel::Errors</code> 的文件來了解所有可用的方法。</p><h4 id="處理驗證錯誤-errors">7.1 <code>errors</code>
</h4><p>此方法回傳 <code>ActiveModel::Errors</code> 類別的實體,包含了所有的錯誤。屬性名稱為鍵,值為由錯誤訊息字串組成的陣列,</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true, length: { minimum: 3 }
end
person = Person.new
person.valid? # => false
person.errors.messages
# => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
person = Person.new(name: "John Doe")
person.valid? # => true
person.errors.messages # => {}
</pre>
</div>
<h4 id="errors">7.2 <code>errors[]</code>
</h4><p><code>errors[]</code> 用來檢查特定屬性的錯誤訊息。會回傳給定屬性的錯誤訊息字串陣列,每個字串都是一個錯誤訊息。如果該屬性沒有錯誤,則返回空陣列。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true, length: { minimum: 3 }
end
person = Person.new(name: "John Doe")
person.valid? # => true
person.errors[:name] # => []
person = Person.new(name: "JD")
person.valid? # => false
person.errors[:name] # => ["is too short (minimum is 3 characters)"]
person = Person.new
person.valid? # => false
person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
</pre>
</div>
<h4 id="errors-add">7.3 <code>errors.add</code>
</h4><p><code>errors.add</code> 方法讓你手動加上特定屬性的錯誤訊息。可以使用 <code>errors.full_messages</code> 或是 <code>errors.to_a</code> 方法來檢視最終將呈現給使用者的錯誤訊息。這些特定的錯誤訊息前面會附上屬性名稱(大寫形式)。<code>errors.add</code> 接受的參數為:要加上錯誤訊息的屬性、錯誤訊息內容。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
def a_method_used_for_validation_purposes
errors.add(:name, "cannot contain the characters !@#%*()_-+=")
end
end
person = Person.create(name: "!@#")
person.errors[:name]
# => ["cannot contain the characters !@#%*()_-+="]
person.errors.full_messages
# => ["Name cannot contain the characters !@#%*()_-+="]
</pre>
</div>
<p>另一種方式是使用 <code>[]=</code> Setter。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
def a_method_used_for_validation_purposes
errors[:name] = "cannot contain the characters !@#%*()_-+="
end
end
person = Person.create(name: "!@#")
person.errors[:name]
# => ["cannot contain the characters !@#%*()_-+="]
person.errors.to_a
# => ["Name cannot contain the characters !@#%*()_-+="]
</pre>
</div>
<h4 id="errors-base">7.4 <code>errors[:base]</code>
</h4><p>可以針對整個物件本身新增錯誤訊息,而不是針對某個特定的屬性。不論是那個值所導致的錯誤,想要把物件標記為無效的時候,可以使用這個方法。由於 <code>errors[:base]</code> 是個陣列,可以加入字串進去,字串會被當成錯誤訊息使用。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
def a_method_used_for_validation_purposes
errors[:base] << "This person is invalid because ..."
end
end
</pre>
</div>
<h4 id="errors-clear">7.5 <code>errors.clear</code>
</h4><p><code>errors.clear</code> 方法可以清除 <code>errors</code> 集合裡的所有錯誤。當然了,對無效物件呼叫 <code>errors.clear</code> 不會使其有效,只是清除了錯誤訊息。下次再呼叫 <code>valid?</code>,或是其它會呼叫 <code>save</code> 的方法時,驗證再次觸發,失敗的錯誤訊息仍會將錯誤填入 <code>errors</code> 集合。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
validates :name, presence: true, length: { minimum: 3 }
end
person = Person.new
person.valid? # => false