forked from railstutorial-china/rails42
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchapter6.html
1587 lines (1338 loc) · 150 KB
/
chapter6.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-CN">
<head>
<meta charset="UTF-8"/>
<title>Ruby on Rails 教程 - 第 6 章 用户模型</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="最好的 Ruby on Rails 入门教程"/>
<meta name="keywords" content="ruby, rails, tutorial"/>
<meta name="author" content="Michael Hartl"/>
<meta name="translator" content="安道"/>
<meta name="generator" content="persie 0.0.1.alpha.3"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="assets/style.css"/>
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/js/collapse.min.js"></script>
<script type="text/javascript" src="assets/global.js"></script>
</head>
<body>
<header class="navbar navbar-default navbar-fixed-top navbar-book">
<div class="container">
<div class="navbar-header">
<a href="http://railstutorial-china.org" class="navbar-brand">Ruby on Rails 教程</a>
<button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target=".book-navbar-collapse">
<span class="sr-only">导航</span>
<i class="fa fa-bars"></i>
</button>
<a href="http://railstutorial-china.org/#purchase" id="navbar-purchase-xs" class="btn btn-warning navbar-btn visible-xs collapsed-purchase-btn">购买</a>
</div>
<nav class="collapse navbar-collapse book-navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="http://railstutorial-china.org" title="首页">首页</a></li>
<li class="active"><a href="http://railstutorial-china.org/read/" title="在线阅读">阅读</a></li>
<li><a href="http://railstutorial-china.org/blog/" title="最新消息">博客</a></li>
<li><a href="https://selfstore.io/products/189/topics" title="论坛">论坛</a></li>
<li class="hidden-xs"><div><a href="http://railstutorial-china.org/#purchase" id="navbar-purchase" class="btn btn-warning navbar-btn" title="购买电子书">购买</a></div></li>
</ul>
</nav>
</div>
</header>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<article class="article">
<section data-type="chapter" id="modeling-users">
<h1><span class="title-label">第 6 章</span> 用户模型</h1>
<p><a href="chapter5.html#filling-in-the-layout">第 5 章</a>末尾创建了一个临时的用户注册页面(<a href="chapter5.html#user-signup-a-first-step">5.4 节</a>)。本书接下来的五章会逐步在这个页面中添加功能。本章我们要迈出关键的一步,创建网站中用户的数据模型,并实现存储数据的方式。<a href="chapter7.html#sign-up">第 7 章</a>会实现用户注册功能,并创建用户资料页面。用户能注册后,我们要实现登录和退出功能(<a href="chapter8.html#log-in-log-out">第 8 章</a>)。<a href="chapter9.html#updating-showing-and-deleting-users">第 9 章</a>(<a href="chapter9.html#requiring-logged-in-users">9.2.1 节</a>)会介绍如何保护页面,禁止无权限的用户访问。最后,在<a href="chapter10.html#account-activation-and-password-reset">第 10 章</a>实现账户激活(从而确认电子邮件地址有效)和密码重设功能。第 6 章到第 10 章的内容结合在一起,为 Rails 应用开发一个功能完整的登录和认证系统。或许你知道已经有很多开发好的 Rails 认证方案,<a href="aside.html#roll-your-own">旁注 6.1</a>解释了为什么,至少在初学阶段,最好自己动手实现。</p>
<div data-type="sidebar" id="roll-your-own" class="sidebar">
<h5>旁注 6.1:自己开发认证系统</h5>
<p>基本上所有 Web 应用都需要某种登录和认证系统。为此,大多数 Web 框架都提供了多种实现方式,Rails 也不例外。为 Rails 开发的认证和权限系统有 <a href="http://github.com/thoughtbot/clearance">Clearance</a>、<a href="http://github.com/binarylogic/authlogic">Authlogic</a>、<a href="http://github.com/plataformatec/devise">Devise</a> 和 <a href="http://railscasts.com/episodes/192-authorization-with-cancan">CanCan</a>。除此之外,还有一些不是 Rails 专用的方案,基于 <a href="http://en.wikipedia.org/wiki/OpenID">OpenID</a> 和 <a href="http://en.wikipedia.org/wiki/Oauth">OAuth</a> 实现。所以你肯定会问,为什么我们要重复制造轮子,为什么不直接使用现成的方案,而要自己开发呢?</p>
<p>首先,实践已经证明,大多数网站的认证系统都要对第三方代码库做一些定制和修改,这往往比重新开发一个工作量还大。再者,现成的方案就像一个“黑盒”,你无法了解其中到底有些什么功能,而自己开发的话能更好地理解实现的过程。而且,Rails 最近的更新(参见 <a href="#adding-a-secure-password">6.3 节</a>),让开发认证系统变得很简单。最后,如果以后要用第三方系统的话,因为自己开发过,所以能更好地理解实现过程,便于定制功能。</p>
</div>
<section data-type="sect1" id="user-model">
<h1><span class="title-label">6.1.</span> 用户模型</h1>
<p>接下来的三章要实现网站的“注册”页面(构思图如<a href="#fig-signup-mockup-preview">图 6.1</a> 所示),在此之前我们先要解决存储问题,因为现在还没地方存储用户信息。所以,实现用户注册功能的第一步是,创建一个数据结构,获取并存储用户的信息。</p>
<div id="fig-signup-mockup-preview" class="figure"><img src="images/chapter6/signup_mockup_bootstrap.png" alt="signup mockup bootstrap" /><div class="figcaption"><span class="title-label">图 6.1:</span>用户注册页面的构思图</div></div>
<p>在 Rails 中,数据模型的默认数据结构叫“模型”(model,MVC 中的 M,参见 <a href="chapter1.html#model-view-controller">1.3.3 节</a>)。Rails 为解决数据持久化提供的默认解决方案是,使用数据库存储需要长期使用的数据。和数据库交互默认使用的是 Active Record。<sup>[<a id="fn-ref-1" href="#fn-1">1</a>]</sup>Active Record 提供了一系列方法,无需使用<a href="http://en.wikipedia.org/wiki/Relational_database">关系数据库</a>所用的“结构化查询语言”(Structured Query Language,简称 SQL),<sup>[<a id="fn-ref-2" href="#fn-2">2</a>]</sup>就能创建、保存和查询数据对象。Rails 还支持“迁移”(migration)功能,允许我们使用纯 Ruby 代码定义数据结构,而不用学习 SQL “数据定义语言”(Data Definition Language,简称 DDL)。最终的结果是,Active Record 把你和数据存储层完全隔开了。本书开发的应用在本地使用 SQLite,部署后使用 PostgreSQL(由 Heroku 提供,参见 <a href="chapter1.html#deploying">1.5 节</a>)。这就引出了一个更深层的话题——在不同的环境中,即便使用不同类型的数据库,我们也无需关心 Rails 是如何存储数据的。</p>
<p>和之前一样,如果使用 Git 做版本控制,现在应该新建一个主题分支:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>git checkout master
<span class="nv">$ </span>git checkout -b modeling-users
</pre></div>
</div>
<section data-type="sect2" id="database-migrations">
<h2><span class="title-label">6.1.1.</span> 数据库迁移</h2>
<p>回顾一下 <a href="chapter4.html#a-user-class">4.4.5 节</a>的内容,在我们自己创建的 <code>User</code> 类中为用户对象定义了 <code>name</code> 和 <code>email</code> 两个属性。那是个很有用的例子,但没有实现持久性最关键的要求:在 Rails 控制台中创建的用户对象,退出控制台后就会消失。本节的目的是为用户创建一个模型,让用户数据不会这么轻易消失。</p>
<p>和 <a href="chapter4.html#a-user-class">4.4.5 节</a>中定义的 <code>User</code> 类一样,我们先为用户模型创建两个属性,分别是 <code>name</code> 和 <code>email</code>。我们会把 <code>email</code> 属性用作唯一的用户名。<sup>[<a id="fn-ref-3" href="#fn-3">3</a>]</sup>(<a href="#adding-a-secure-password">6.3 节</a>会添加一个属性,存储密码)在<a href="chapter4.html#listing-example-user">代码清单 4.13</a> 中,我们使用 Ruby 的 <code>attr_accessor</code> 方法创建了这两个属性:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span>
<span class="kp">attr_accessor</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>不过,在 Rails 中不用这样定义属性。前面提到过,Rails 默认使用关系数据库存储数据,数据库中的表由数据行组成,每一行都有相应的列,对应数据属性。例如,为了存储用户的名字和电子邮件地址,我们要创建 <code>users</code> 表,表中有两个列,<code>name</code> 和 <code>email</code>,这样每一行就表示一个用户,如<a href="#fig-users-table">图 6.2</a> 所示,对应的数据模型如<a href="#fig-user-model-sketch">图 6.3</a> 所示。(图 6.3 只是梗概,完整的数据模型如<a href="#fig-user-model-initial">图 6.4</a> 所示。)把列命名为 <code>name</code> 和 <code>email</code> 后,Active Record 会自动把它们识别为用户对象的属性。</p>
<div id="fig-users-table" class="figure"><img src="images/chapter6/users_table.png" alt="users table" /><div class="figcaption"><span class="title-label">图 6.2:</span><code>users</code> 表中的示例数据</div></div>
<div id="fig-user-model-sketch" class="figure"><img src="images/chapter6/user_model_sketch.png" alt="user model sketch" /><div class="figcaption"><span class="title-label">图 6.3:</span>用户数据模型梗概</div></div>
<p>你可能还记得,在<a href="chapter5.html#listing-generate-users-controller">代码清单 5.28</a> 中,我们使用下面的命令生成了用户控制器和 <code>new</code> 动作:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate controller Users new
</pre></div>
</div>
<p>创建模型有个类似的命令——<code>generate model</code>。我们可以使用这个命令生成用户模型,以及 <code>name</code> 和 <code>email</code> 属性,如<a href="#listing-generate-user-model">代码清单 6.1</a> 所示。</p>
<div id="listing-generate-user-model" data-type="listing">
<h5><span class="title-label">代码清单 6.1:</span>生成用户模型</h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate model User name:string email:string
invoke active_record
create db/migrate/20140724010738_create_users.rb
create app/models/user.rb
invoke test_unit
create <span class="nb">test</span>/models/user_test.rb
create <span class="nb">test</span>/fixtures/users.yml
</pre></div>
</div>
<p>(注意,控制器名是复数,模型名是单数:控制器是 <code>Users</code>,而模型是 <code>User</code>。)我们指定了可选的参数 <code>name:string</code> 和 <code>email:string</code>,告诉 Rails 我们需要的两个属性是什么,以及各自的类型(两个都是字符串)。你可以把这两个参数与<a href="chapter3.html#listing-generating-pages">代码清单 3.4</a> 和<a href="chapter5.html#listing-generate-users-controller">代码清单 5.28</a> 中的动作名对比一下,看看有什么不同。</p>
<p>执行上述 <code>generate</code> 命令之后,会生成一个迁移文件。迁移是一种递进修改数据库结构的方式,可以根据需求修改数据模型。执行 <code>generate</code> 命令后会自动为用户模型创建迁移,这个迁移的作用是创建一个 <code>users</code> 表以及 <code>name</code> 和 <code>email</code> 两个列,如<a href="#listing-users-migration">代码清单 6.2</a> 所示。(我们会在 <a href="#uniqueness-validation">6.2.5 节</a>介绍如何手动创建迁移文件。)</p>
<div id="listing-users-migration" data-type="listing">
<h5><span class="title-label">代码清单 6.2:</span>用户模型的迁移文件(创建 <code>users</code> 表)</h5>
<div class="source-file">db/migrate/[timestamp]_create_users.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">CreateUsers</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:users</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="o">.</span><span class="n">string</span> <span class="ss">:name</span>
<span class="n">t</span><span class="o">.</span><span class="n">string</span> <span class="ss">:email</span>
<span class="n">t</span><span class="o">.</span><span class="n">timestamps</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意,迁移文件名前面有个时间戳,指明创建的时间。早期,迁移文件名的前缀是递增的数字,在团队协作中,如果多个程序员生成了序号相同的迁移文件就可能会发生冲突。除非两个迁移文件在同一秒钟生成这种小概率事件发生了,否则使用时间戳基本可以避免冲突的发生。</p>
<p>迁移文件中有一个名为 <code>change</code> 的方法,定义要对数据库做什么操作。在<a href="#listing-users-migration">代码清单 6.2</a> 中,<code>change</code> 方法使用 Rails 提供的 <code>create_table</code> 方法在数据库中新建一个表,用来存储用户。<code>create_table</code> 方法可以接受一个块,块中有一个块变量 <code>t</code>(“table”)。在块中,<code>create_table</code> 方法通过 <code>t</code> 对象创建 <code>name</code> 和 <code>email</code> 两个列,均为 <code>string</code> 类型。<sup>[<a id="fn-ref-4" href="#fn-4">4</a>]</sup>表名是复数形式(<code>users</code>),不过模型名是单数形式(<code>User</code>),这是 Rails 在用词上的一个约定:模型表示单个用户,而数据库表中存储了很多用户。块中最后一行 <code>t.timestamps null: false</code> 是个特殊的方法,它会自动创建两个列,<code>created_at</code> 和 <code>updated_at</code>,这两个列分别记录创建用户的时间戳和更新用户数据的时间戳。(<a href="#creating-user-objects">6.1.3 节</a>有使用这两个列的例子。)这个迁移文件表示的完整数据模型如<a href="#fig-user-model-initial">图 6.4</a> 所示。(注意,<a href="#fig-user-model-sketch">图 6.3</a> 中没有列出自动添加的两个时间戳列。)</p>
<div id="fig-user-model-initial" class="figure"><img src="images/chapter6/user_model_initial_3rd_edition.png" alt="user model initial 3rd edition" /><div class="figcaption"><span class="title-label">图 6.4:</span><a href="#listing-users-migration">代码清单 6.2</a> 生成的用户数据模型</div></div>
<p>我们可以使用如下的 <code>rake</code> 命令(<a href="chapter2.html#aside-rake">旁注 2.1</a>)执行这个迁移(叫“向上迁移”):</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate
</pre></div>
</div>
<p>(你可能还记得,我们在 <a href="chapter2.html#the-users-resource">2.2 节</a>用过这个命令。)第一次运行 <code>db:migrate</code> 命令时会创建 <code>db/development.sqlite3</code>,这是 SQLite <sup>[<a id="fn-ref-5" href="#fn-5">5</a>]</sup>数据库文件。若要查看数据库结构,可以使用 <a href="http://sqlitebrowser.org/">SQLite 数据库浏览器</a>打开 <code>db/development.sqlite3</code> 文件,如<a href="#fig-sqlite-database-browser">图 6.5</a> 所示。(如果想从云端 IDE 把这个文件下载到本地电脑,可以在 <code>db/development.sqlite3</code> 上按右键,然后选择“Download”。)和<a href="#fig-user-model-initial">图 6.4</a> 中的模型对比之后,你可能会发现有一个列在迁移中没有出现——<code>id</code> 列。<a href="chapter2.html#the-users-resource">2.2 节</a>提到过,这个列是自动生成的,Rails 用这个列作为行的唯一标识符。</p>
<div id="fig-sqlite-database-browser" class="figure"><img src="images/chapter6/sqlite_database_browser_3rd_edition.png" alt="sqlite database browser 3rd edition" /><div class="figcaption"><span class="title-label">图 6.5:</span>在 SQLite 数据库浏览器中查看刚创建的 <code>users</code> 表</div></div>
<p>大多数迁移,包括本书中的所有迁移,都是可逆的,也就是说可以使用一个简单的 Rake 命令“向下迁移”,撤销之前的操作,这个命令是 <code>db:rollback</code>:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:rollback
</pre></div>
</div>
<p>(还有一个撤销迁移的方法,参见<a href="chapter3.html#aside-undoing-things">旁注 3.1</a>。)这个命令会调用 <code>drop_table</code> 方法,把 <code>users</code> 表从数据库中删除。之所以可以这么做,是因为 <code>change</code> 方法知道 <code>create_table</code> 的逆操作是 <code>drop_table</code>,所以回滚时会直接调用 <code>drop_table</code> 方法。对于一些无法自动逆转的操作,例如删除列,就不能依赖 <code>change</code> 方法了,我们要分别定义 <code>up</code> 和 <code>down</code> 方法。关于迁移的更多信息请查看 <a href="http://guides.rubyonrails.org/migrations.html">Rails 指南</a>。</p>
<p>如果你执行了上面的回滚操作,在继续阅读之前请再迁移回来:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate
</pre></div>
</div>
</section>
<section data-type="sect2" id="the-model-file">
<h2><span class="title-label">6.1.2.</span> 模型文件</h2>
<p>我们看到,执行<a href="#listing-generate-user-model">代码清单 6.1</a> 中的命令后会生成一个迁移文件(<a href="#listing-users-migration">代码清单 6.2</a>),也看到了执行迁移后得到的结果(<a href="#fig-sqlite-database-browser">图 6.5</a>):修改 <code>db/development.sqlite3</code> 文件,新建 <code>users</code> 表,并创建 <code>id</code>、<code>name</code>、<code>email</code>、<code>created_at</code> 和 <code>updated_at</code> 这几个列。<a href="#listing-generate-user-model">代码清单 6.1</a> 同时还生成了一个模型文件,本节剩下的内容专门解说这个文件。</p>
<p>我们先看用户模型的代码,在 <code>app/models/</code> 文件夹中的 <code>user.rb</code> 文件里。这个文件的内容非常简单,如<a href="#listing-raw-user-model">代码清单 6.3</a> 所示。</p>
<div id="listing-raw-user-model" data-type="listing">
<h5><span class="title-label">代码清单 6.3:</span>刚创建的用户模型</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="k">end</span>
</pre></div>
</div>
<p><a href="chapter4.html#class-inheritance">4.4.2 节</a>介绍过,<code>class User < ActiveRecord::Base</code> 的意思是 <code>User</code> 类继承自 <code>ActiveRecord::Base</code> 类,所以用户模型自动获得了 <code>ActiveRecord::Base</code> 的所有功能。当然了,只知道这种继承关系没什么用,我们并不知道 <code>ActiveRecord::Base</code> 做了什么。下面看几个实例。</p>
</section>
<section data-type="sect2" id="creating-user-objects">
<h2><span class="title-label">6.1.3.</span> 创建用户对象</h2>
<p>和<a href="chapter4.html#rails-flavored-ruby">第 4 章</a>一样,探索数据模型使用的工具是 Rails 控制台。因为我们还不想修改数据库中的数据,所以要在沙盒模式中启动控制台:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console --sandbox</span>
<span class="go">Loading development environment in sandbox</span>
<span class="go">Any modifications you make will be rolled back on exit</span>
<span class="go">>></span>
</pre></div>
</div>
<p>如提示消息所说,“Any modifications you make will be rolled back on exit”,在沙盒模式下使用控制台,退出当前会话后,对数据库做的所有改动都会回归到原来的状态。</p>
<p>在 <a href="chapter4.html#a-user-class">4.4.5 节</a>的控制台会话中,我们要引入<a href="chapter4.html#listing-example-user">代码清单 4.13</a> 中的代码才能使用 <code>User.new</code> 创建用户对象。对模型来说,情况有所不同。你可能还记得 <a href="chapter4.html#a-controller-class">4.4.4 节</a>说过,Rails 控制台会自动加载 Rails 环境,这其中就包括模型。也就是说,现在无需加载任何代码就可以直接创建用户对象:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">new</span>
<span class="go">=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil></span>
</pre></div>
</div>
<p>上述代码显示了一个用户对象的默认值。</p>
<p>如果不为 <code>User.new</code> 指定参数,对象的所有属性值都是 <code>nil</code>。在 <a href="chapter4.html#a-user-class">4.4.5 节</a>,自己编写的 <code>User</code> 类可以接受一个哈希参数初始化对象的属性。这种方式是受 Active Record 启发的,在 Active Record 中也可以使用相同的方式指定初始值:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="go">=> #<User id: nil, name: "Michael Hartl", email: "[email protected]", created_at: nil, updated_at: nil></span>
</pre></div>
</div>
<p>我们看到 <code>name</code> 和 <code>email</code> 属性的值都已经设定了。</p>
<p>数据的有效性对理解 Active Record 模型对象很重要,我们会在 <a href="#user-validations">6.2 节</a>深入介绍。不过注意,现在这个 <code>user</code> 对象是有效的,我们可以在这个对象上调用 <code>valid?</code> 方法确认:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">valid?</span>
<span class="go">true</span>
</pre></div>
</div>
<p>到目前为止,我们都没有修改数据库:<code>User.new</code> 只在内存中创建一个对象,<code>user.valid?</code> 只是检查对象是否有效。如果想把用户对象保存到数据库中,我们要在 <code>user</code> 变量上调用 <code>save</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">save</span>
<span class="go"> (0.2ms) begin transaction</span>
<span class="go"> User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users".</span>
<span class="go"> "email") = LOWER('[email protected]') LIMIT 1</span>
<span class="go"> SQL (0.5ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at)</span>
<span class="go"> VALUES (?, ?, ?, ?) [["created_at", "2014-09-11 14:32:14.199519"],</span>
<span class="go"> ["email", "[email protected]"], ["name", "Michael Hartl"], ["updated_at",</span>
<span class="go"> "2014-09-11 14:32:14.199519"]]</span>
<span class="go"> (0.9ms) commit transaction</span>
<span class="go">=> true</span>
</pre></div>
</div>
<p>如果保存成功,<code>save</code> 方法返回 <code>true</code>,否则返回 <code>false</code>。(现在所有保存操作都会成功,因为还没有数据验证功能,<a href="#user-validations">6.2 节</a>会看到一些失败的例子。)Rails 还会在控制台中显示 <code>user.save</code> 对应的 SQL 语句(<code>INSERT INTO "users"…</code>),以供参考。本书几乎不会使用原始的 SQL,<sup>[<a id="fn-ref-6" href="#fn-6">6</a>]</sup>所以此后会省略 SQL。不过,从 Active Record 各种操作生成的 SQL 中可以学到很多知识。</p>
<p>你可能注意到了,刚创建时用户对象的 <code>id</code>、<code>created_at</code> 和 <code>updated_at</code> 属性值都是 <code>nil</code>,下面看一下保存之后有没有变化:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span>
<span class="go">=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",</span>
<span class="go">created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46"></span>
</pre></div>
</div>
<p>我们看到,<code>id</code> 的值变成了 <code>1</code>,那两个自动创建的时间戳属性也变成了当前时间。<sup>[<a id="fn-ref-7" href="#fn-7">7</a>]</sup>现在这两个时间戳是一样的,<a href="#updating-user-objects">6.1.5 节</a>会看到二者不同的情况。</p>
<p>和 <a href="chapter4.html#a-user-class">4.4.5 节</a>的 <code>User</code> 类一样,用户模型的实例也可以使用点号获取属性:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">name</span>
<span class="go">=> "Michael Hartl"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">email</span>
<span class="go">=> "[email protected]"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">updated_at</span>
<span class="go">=> Thu, 24 Jul 2014 00:57:46 UTC +00:00</span>
</pre></div>
</div>
<p><a href="chapter7.html#sign-up">第 7 章</a>会介绍,虽然一般习惯把创建和保存分成如上所示的两步完成,不过 Active Record 也允许我们使用 <code>User.create</code> 方法把这两步合成一步:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"A Nother"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="go">#<User id: 2, name: "A Nother", email: "[email protected]", created_at:</span>
<span class="go">"2014-07-24 01:05:24", updated_at: "2014-07-24 01:05:24"></span>
<span class="gp">>> </span><span class="n">foo</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Foo"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="go">#<User id: 3, name: "Foo", email: "[email protected]", created_at: "2014-07-24</span>
<span class="go">01:05:42", updated_at: "2014-07-24 01:05:42"></span>
</pre></div>
</div>
<p>注意,<code>User.create</code> 的返回值不是 <code>true</code> 或 <code>false</code>,而是创建的用户对象,可直接赋值给变量(例如上面第二个命令中的 <code>foo</code> 变量).</p>
<p><code>create</code> 的逆操作是 <code>destroy</code>:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">foo</span><span class="o">.</span><span class="n">destroy</span>
<span class="go">=> #<User id: 3, name: "Foo", email: "[email protected]", created_at: "2014-07-24</span>
<span class="go">01:05:42", updated_at: "2014-07-24 01:05:42"></span>
</pre></div>
</div>
<p>奇怪的是,<code>destroy</code> 和 <code>create</code> 一样,返回值是对象。我不觉得什么地方会用到 <code>destroy</code> 的返回值。更奇怪的是,销毁的对象还在内存中:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">foo</span>
<span class="go">=> #<User id: 3, name: "Foo", email: "[email protected]", created_at: "2014-07-24</span>
<span class="go">01:05:42", updated_at: "2014-07-24 01:05:42"></span>
</pre></div>
</div>
<p>那么我们怎么知道对象是否真被销毁了呢?对于已经保存而没有销毁的对象,怎样从数据库中读取呢?要回答这些问题,我们要先学习如何使用 Active Record 查找用户对象。</p>
</section>
<section data-type="sect2" id="finding-user-objects">
<h2><span class="title-label">6.1.4.</span> 查找用户对象</h2>
<p>Active Record 提供了好几种查找对象的方法。下面我们使用这些方法查找创建的第一个用户,同时也验证一下第三个用户(<code>foo</code>)是否被销毁了。先看一下还存在的用户:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="go">=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",</span>
<span class="go">created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46"></span>
</pre></div>
</div>
<p>我们把用户的 ID 传给 <code>User.find</code> 方法,Active Record 会返回 ID 为 1 的用户对象。</p>
<p>下面来看一下 ID 为 3 的用户是否还在数据库中:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="go">ActiveRecord::RecordNotFound: Couldn't find User with ID=3</span>
</pre></div>
</div>
<p>因为我们在 <a href="#creating-user-objects">6.1.3 节</a>销毁了第三个用户,所以 Active Record 无法在数据库中找到这个用户,抛出了一个异常,这说明在查找过程中出现了问题。因为 ID 不存在,所以 <code>find</code> 方法抛出 <code>ActiveRecord::RecordNotFound</code> 异常。<sup>[<a id="fn-ref-8" href="#fn-8">8</a>]</sup></p>
<p>除了这种查找方法之外,Active Record 还支持通过属性查找用户:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="go">=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",</span>
<span class="go">created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46"></span>
</pre></div>
</div>
<p>我们会使用电子邮件地址做用户名,所以在学习如何让用户登录网站时会用到这种 <code>find</code> 方法(<a href="chapter7.html#sign-up">第 7 章</a>)。你可能会担心如果用户数量过多,使用 <code>find_by</code> 的效率不高。事实的确如此,我们会在 <a href="#uniqueness-validation">6.2.5 节</a>说明这个问题,以及如何使用数据库索引解决。</p>
<p>最后,再介绍几个常用的查找方法。首先是 <code>first</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">first</span>
<span class="go">=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",</span>
<span class="go">created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46"></span>
</pre></div>
</div>
<p>很明显,<code>first</code> 会返回数据库中的第一个用户。还有 <code>all</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="no">User</span><span class="o">.</span><span class="n">all</span>
<span class="go">=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",</span>
<span class="go">email: "[email protected]", created_at: "2014-07-24 00:57:46",</span>
<span class="go">updated_at: "2014-07-24 00:57:46">, #<User id: 2, name: "A Nother",</span>
<span class="go">email: "[email protected]", created_at: "2014-07-24 01:05:24",</span>
<span class="go">updated_at: "2014-07-24 01:05:24">]></span>
</pre></div>
</div>
<p>从控制台的输出可以看出,<code>User.all</code> 方法返回一个 <code>ActiveRecord::Relation</code> 实例,其实这是一个数组(<a href="chapter4.html#arrays-and-ranges">4.3.1 节</a>),
包含数据库中的所有用户。</p>
</section>
<section data-type="sect2" id="updating-user-objects">
<h2><span class="title-label">6.1.5.</span> 更新用户对象</h2>
<p>创建对象后,一般都会进行更新操作。更新有两种基本方式,其一,可以分别为各属性赋值,在 <a href="chapter4.html#a-user-class">4.4.5 节</a>就是这么做的:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span> <span class="c1"># 只是为了查看 user 对象的属性是什么</span>
<span class="go">=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",</span>
<span class="go">created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46"></span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="s2">"[email protected]"</span>
<span class="go">=> "[email protected]"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">save</span>
<span class="go">=> true</span>
</pre></div>
</div>
<p>注意,如果想把改动写入数据库,必须执行最后一个方法。我们可以执行 <code>reload</code> 命令来看一下没保存的话是什么情况。<code>reload</code> 命令会使用数据库中的数据重新加载对象:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">email</span>
<span class="go">=> "[email protected]"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="s2">"[email protected]"</span>
<span class="go">=> "[email protected]"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">reload</span><span class="o">.</span><span class="n">email</span>
<span class="go">=> "[email protected]"</span>
</pre></div>
</div>
<p>现在我们已经更新了用户数据,如在 <a href="#creating-user-objects">6.1.3 节</a>中所说,自动创建的那两个时间戳属性不一样了:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">created_at</span>
<span class="go">=> "2014-07-24 00:57:46"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">updated_at</span>
<span class="go">=> "2014-07-24 01:37:32"</span>
</pre></div>
</div>
<p>更新数据的第二种常用方式是使用 <code>update_attributes</code> 方法:<sup>[<a id="fn-ref-9" href="#fn-9">9</a>]</sup></p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">update_attributes</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"The Dude"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="go">=> true</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">name</span>
<span class="go">=> "The Dude"</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">email</span>
<span class="go">=> "[email protected]"</span>
</pre></div>
</div>
<p><code>update_attributes</code> 方法接受一个指定对象属性的哈希作为参数,如果操作成功,会执行更新和保存两个操作(保存成功时返回值为 <code>true</code>)。注意,如果任何一个数据验证失败了,例如存储记录时需要密码(<a href="#adding-a-secure-password">6.3 节</a>实现),<code>update_attributes</code> 操作就会失败。如果只需要更新单个属性,可以使用 <code>update_attribute</code>,跳过验证:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">update_attribute</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="s2">"The Dude"</span><span class="p">)</span>
<span class="go">=> true</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">name</span>
<span class="go">=> "The Dude"</span>
</pre></div>
</div>
</section>
</section>
<section data-type="sect1" id="user-validations">
<h1><span class="title-label">6.2.</span> 用户数据验证</h1>
<p><a href="#user-model">6.1 节</a>创建的用户模型现在已经有了可以使用的 <code>name</code> 和 <code>email</code> 属性,不过功能还很简单:任何字符串(包括空字符串)都可以使用。名字和电子邮件地址的格式显然要复杂一些。例如,<code>name</code> 不应该是空的,<code>email</code> 应该符合特定的格式。而且,我们要把电子邮件地址当成用户名用来登录,那么在数据库中就不能重复出现。</p>
<p>总之,<code>name</code> 和 <code>email</code> 不是什么字符串都可以使用的,我们要对它们可使用的值做个限制。Active Record 通过数据验证实现这种限制(<a href="chapter2.html#putting-the-micro-in-microposts">2.3.2 节</a>简单提到过)。本节,我们会介绍几种常用的数据验证:存在性、长度、格式和唯一性。<a href="#user-has-secure-password">6.3.2 节</a>还会介绍另一种常用的数据验证——二次确认。<a href="chapter7.html#unsuccessful-signups">7.3 节</a>会看到,如果提交了不合要求的数据,数据验证会显示一些很有用的错误消息。</p>
<section data-type="sect2" id="a-validity-test">
<h2><span class="title-label">6.2.1.</span> 有效性测试</h2>
<p><a href="chapter3.html#aside-when-to-test">旁注 3.3</a>说过,TDD 并不适用所有情况,但是模型验证是使用 TDD 的绝佳时机。如果不先编写失败测试,再想办法让它通过,我们很难确定验证是否实现了我们希望实现的功能。</p>
<p>我们采用的方法是,先得到一个有效的模型对象,然后把属性改为无效值,以此确认这个对象是无效的。以防万一,我们先编写一个测试,确认模型对象一开始是有效的。这样,如果验证测试失败了,我们才知道的确事出有因(而不是因为一开始对象是无效的)。</p>
<p><a href="#listing-generate-user-model">代码清单 6.1</a> 中的命令生成了一个用来测试用户模型的测试文件,现在这个文件中还没什么内容,如<a href="#listing-default-user-test">代码清单 6.4</a> 所示。</p>
<div id="listing-default-user-test" data-type="listing">
<h5><span class="title-label">代码清单 6.4:</span>还没什么内容的用户模型测试文件</h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># test "the truth" do</span>
<span class="c1"># assert true</span>
<span class="c1"># end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>为了测试有效的对象,我们要在特殊的 <code>setup</code> 方法中创建一个有效的用户对象 <code>@user</code>。<a href="chapter3.html#mostly-static-pages-exercises">第 3 章</a>的练习中提到过,<code>setup</code> 方法会在每个测试方法运行前执行。因为 <code>@user</code> 是实例变量,所以自动可在所有测试方法中使用,而且我们可以使用 <code>valid?</code> 方法检查它是否有效。测试如<a href="#listing-valid-user-test">代码清单 6.5</a> 所示。</p>
<div id="listing-valid-user-test" data-type="listing">
<h5><span class="title-label">代码清单 6.5:</span>测试用户对象一开始是有效的 <span class="green">GREEN</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p><a href="#listing-valid-user-test">代码清单 6.5</a> 使用简单的 <code>assert</code> 方法,如果 <code>@user.valid?</code> 返回 <code>true</code>,测试就能通过;返回 <code>false</code>,测试则会失败。</p>
<p>因为用户模型现在还没有任何验证,所有这个测试可以通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.6:</span><strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:models
</pre></div>
</div>
<p>这里,我们使用 <code>rake test:models</code>,只运行模型测试(和 <a href="chapter5.html#layout-link-tests">5.3.4 节</a>的 <code>rake test:integration</code> 对比一下)。</p>
</section>
<section data-type="sect2" id="validating-presence">
<h2><span class="title-label">6.2.2.</span> 存在性验证</h2>
<p>存在性验证算是最基本的验证了,只是检查指定的属性是否存在。本节我们会确保用户存入数据库之前,<code>name</code> 和 <code>email</code> 字段都有值。<a href="chapter7.html#signup-error-messages">7.3.3 节</a>会介绍如何把这个限制应用到创建用户的注册表单中。</p>
<p>我们要先在<a href="#listing-valid-user-test">代码清单 6.5</a> 的基础上再编写一个测试,检查 <code>name</code> 属性是否存在。如<a href="#listing-name-presence-test">代码清单 6.7</a> 所示,我们只需把 <code>@user</code> 的 <code>name</code> 属性设为空字符串(包含几个空格的字符串),然后使用 <code>assert_not</code> 方法确认得到的用户对象是无效的。</p>
<div id="listing-name-presence-test" data-type="listing">
<h5><span class="title-label">代码清单 6.7:</span>测试 <code>name</code> 属性的验证措施 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="hll"> <span class="nb">test</span> <span class="s2">"name should be present"</span> <span class="k">do</span>
</span><span class="hll"> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="s2">" "</span>
</span><span class="hll"> <span class="n">assert_not</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
</span><span class="hll"> <span class="k">end</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>现在,模型测试应该失败:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.8:</span><strong class="red">RED</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:models
</pre></div>
</div>
<p>我们在<a href="chapter2.html#a-toy-app-exercises">第 2 章的练习</a>中见过,<code>name</code> 属性的存在性验证使用 <code>validates</code> 方法,而且其参数为 <code>presence: true</code>,如<a href="#listing-validates-presence-of-name">代码清单 6.9</a> 所示。<code>presence: true</code> 是只有一个元素的可选哈希参数,<a href="chapter4.html#css-revisited">4.3.4 节</a>说过,如果方法的最后一个参数是哈希,可以省略花括号。(<a href="chapter5.html#site-navigation">5.1.1 节</a>说过,Rails 经常使用哈希做参数。)</p>
<div id="listing-validates-presence-of-name" data-type="listing">
<h5><span class="title-label">代码清单 6.9:</span>添加 <code>name</code> 属性存在性验证 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p><a href="#listing-validates-presence-of-name">代码清单 6.9</a> 中的代码看起来可能有点儿神奇,其实 <code>validates</code> 就是个方法。加入括号后,可以写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">validates</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
</div>
<p>打开控制台,看一下在用户模型中加入验证后有什么效果:<sup>[<a id="fn-ref-10" href="#fn-10">10</a>]</sup></p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console --sandbox</span>
<span class="gp">>> </span><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">""</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">valid?</span>
<span class="go">=> false</span>
</pre></div>
</div>
<p>这里我们使用 <code>valid?</code> 方法检查 <code>user</code> 变量的有效性,如果有一个或多个验证失败,返回值为 <code>false</code>,如果所有验证都能通过,返回 <code>true</code>。现在只有一个验证,所以我们知道是哪一个失败,不过看一下失败时生成的 <code>errors</code> 对象还是很有用的:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">full_messages</span>
<span class="go">=> ["Name can't be blank"]</span>
</pre></div>
</div>
<p>(错误消息暗示,Rails 使用 <a href="chapter4.html#modifying-built-in-classes">4.4.3 节</a>介绍的 <code>balnk?</code> 方法验证存在性。)</p>
<p>因为用户无效,如果尝试把它保存到数据库中,操作会失败:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">save</span>
<span class="go">=> false</span>
</pre></div>
</div>
<p>加入验证后,<a href="#listing-name-presence-test">代码清单 6.7</a> 中的测试应该可以通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.10:</span><strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:models
</pre></div>
</div>
<p>按照<a href="#listing-name-presence-test">代码清单 6.7</a> 的方式,再编写一个检查 <code>email</code> 属性存在性的测试就简单了,如<a href="#listing-email-presence-test">代码清单 6.11</a> 所示。让这个测试通过的应用代码如<a href="#listing-validates-presence-of-email">代码清单 6.12</a> 所示。</p>
<div id="listing-email-presence-test" data-type="listing">
<h5><span class="title-label">代码清单 6.11:</span>测试 <code>email</code> 属性的验证措施 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"name should be present"</span> <span class="k">do</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="s2">""</span>
<span class="n">assert_not</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="hll"> <span class="nb">test</span> <span class="s2">"email should be present"</span> <span class="k">do</span>
</span><span class="hll"> <span class="vi">@user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="s2">" "</span>
</span><span class="hll"> <span class="n">assert_not</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
</span><span class="hll"> <span class="k">end</span>
</span><span class="k">end</span>
</pre></div>
</div>
<div id="listing-validates-presence-of-email" data-type="listing">
<h5><span class="title-label">代码清单 6.12:</span>添加 <code>email</code> 属性存在性验证 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>现在,存在性验证都添加了,测试组件应该可以通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.13:</span><strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
<section data-type="sect2" id="length-validation">
<h2><span class="title-label">6.2.3.</span> 长度验证</h2>
<p>我们已经对用户模型可接受的数据做了一些限制,现在必须为用户提供一个名字,不过我们应该做进一步限制,因为用户的名字会在演示应用中显示,所以最好限制它的长度。有了前一节的基础,这一步就简单了。</p>
<p>没有科学的方法确定最大长度是多少,我们就使用 50 作为长度的上限吧,所以要验证 51 个字符超长了。而且,用户的电子邮件地址可能会超过字符串的最大长度限制,这个最大值在很多数据库中都是 255——这种情况虽然很少发生,但也有发生的可能。因为下一节的格式验证无法实现这种限制,所以我们要在这一节实现。测试如<a href="#listing-length-validation-test">代码清单 6.14</a> 所示。</p>
<div id="listing-length-validation-test" data-type="listing">
<h5><span class="title-label">代码清单 6.14:</span>测试 <code>name</code> 属性的长度验证 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"name should not be too long"</span> <span class="k">do</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="s2">"a"</span> <span class="o">*</span> <span class="mi">51</span>
<span class="n">assert_not</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"email should not be too long"</span> <span class="k">do</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="s2">"a"</span> <span class="o">*</span> <span class="mi">256</span>
<span class="n">assert_not</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>为了方便,我们使用字符串连乘生成了一个有 51 个字符的字符串。在控制台中可以看到连乘是什么:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="s2">"a"</span> <span class="o">*</span> <span class="mi">51</span>
<span class="go">=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"</span>
<span class="gp">>> </span><span class="p">(</span><span class="s2">"a"</span> <span class="o">*</span> <span class="mi">51</span><span class="p">)</span><span class="o">.</span><span class="n">length</span>
<span class="go">=> 51</span>
</pre></div>
</div>
<p>现在,<a href="#listing-length-validation-test">代码清单 6.14</a> 中的测试应该失败:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.15:</span><strong class="red">RED</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
<p>为了让测试通过,我们要使用验证参数限制长度,即 <code>length</code>,以及限制上线的 <code>maximum</code> 参数,如<a href="#listing-length-validation">代码清单 6.16</a> 所示。</p>
<div id="listing-length-validation" data-type="listing">
<h5><span class="title-label">代码清单 6.16:</span>为 <code>name</code> 属性添加长度验证 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">50</span> <span class="p">}</span>
</span> <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">255</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p>现在测试应该可以通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.17:</span><strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
<p>测试组件再次通过,接下来我们要实现一个更有挑战的验证——电子邮件地址的格式。</p>
</section>
<section data-type="sect2" id="format-validation">
<h2><span class="title-label">6.2.4.</span> 格式验证</h2>
<p><code>name</code> 属性的验证只需做一些简单的限制就好——任何非空、长度小于 51 个字符的字符串都可以。可是 <code>email</code> 属性需要更复杂的限制,必须是有效地电子邮件地址才行。目前我们只拒绝空电子邮件地址,本节我们要限制电子邮件地址符合常用的形式,类似 <code>[email protected]</code> 这种。</p>
<p>这里我们用到的测试和验证不是十全十美的,只是刚好可以覆盖大多数有效的电子邮件地址,并拒绝大多数无效的电子邮件地址。我们会先测试一组有效的电子邮件地址和一组无效的电子邮件地址。我们要使用 <code>%w[]</code> 创建这两组地址,其中每个地址都是字符串形式,如下面的控制台会话所示:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="gp">>> </span><span class="sx">%w[foo bar baz]</span>
<span class="go">=> ["foo", "bar", "baz"]</span>
<span class="gp">>> </span><span class="n">addresses</span> <span class="o">=</span> <span class="sx">%w[[email protected] [email protected] [email protected]]</span>
<span class="gp">>> </span><span class="n">addresses</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">address</span><span class="o">|</span>
<span class="gp">?> </span> <span class="nb">puts</span> <span class="n">address</span>
<span class="gp">>> </span><span class="k">end</span>
<span class="go">[email protected]</span>
<span class="go">[email protected]</span>
<span class="go">[email protected]</span>
</pre></div>
</div>
<p>在上面这个控制台会话中,我们使用 <code>each</code> 方法(<a href="chapter4.html#blocks">4.3.2 节</a>)遍历 <code>addresses</code> 数组中的元素。掌握这种用法之后,我们就可以编写一些基本的电子邮件地址格式验证测试了。</p>
<p>电子邮件地址格式认证有点棘手,且容易出错,所以我们会先编写检查有效电子邮件地址的测试,这些测试应该能通过,以此捕获验证可能出现的错误。也就是说,添加验证后,不仅要拒绝无效的电子邮件地址,例如 <em>user@example,com</em>,还得接受有效的电子邮件地址,例如 <em>[email protected]</em>。(显然目前会接受所有电子邮件地址,因为只要不为空值都能通过验证。)检查有效电子邮件地址的测试如<a href="#listing-email-format-valid-tests">代码清单 6.18</a> 所示。</p>
<div id="listing-email-format-valid-tests" data-type="listing">
<h5><span class="title-label">代码清单 6.18:</span>测试有效的电子邮件地址格式 <span class="green">GREEN</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"email validation should accept valid addresses"</span> <span class="k">do</span>
<span class="n">valid_addresses</span> <span class="o">=</span> <span class="sx">%w[[email protected] [email protected] [email protected]</span>
<span class="sx"> [email protected] [email protected]]</span>
<span class="n">valid_addresses</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">valid_address</span><span class="o">|</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="n">valid_address</span>
<span class="n">assert</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">valid_address</span><span class="o">.</span><span class="n">inspect</span><span class="si">}</span><span class="s2"> should be valid"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意,我们为 <code>assert</code> 方法指定了可选的第二个参数,定制错误消息,识别是哪个地址导致测试失败的:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">assert</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">valid_address</span><span class="o">.</span><span class="n">inspect</span><span class="si">}</span><span class="s2"> should be valid"</span>
</pre></div>
</div>
<p>这行代码在字符串插值中使用了 <a href="chapter4.html#hashes-and-symbols">4.3.3 节</a> 介绍的 <code>inspect</code> 方法。像这种使用 <code>each</code> 的测试,最好能知道是哪个地址导致失败的,因为不管哪个地址导致测试失败,都无法看到行号,很难查出问题的根源。</p>
<p>接下来,我们要测试一系列无效的电子邮件,确认它们无法通过验证,例如 <em>user@example,com</em>(点号变成了逗号)和 <em>user_at_foo.org</em>(没有“@”符号)。和<a href="#listing-email-format-valid-tests">代码清单 6.18</a> 一样,<a href="#listing-email-format-validation-tests">代码清单 6.19</a> 中也指定了错误消息参数,识别是哪个地址导致测试失败的。</p>
<div id="listing-email-format-validation-tests" data-type="listing">
<h5><span class="title-label">代码清单 6.19:</span>测试电子邮件地址格式验证 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"email validation should reject invalid addresses"</span> <span class="k">do</span>
<span class="n">invalid_addresses</span> <span class="o">=</span> <span class="sx">%w[user@example,com user_at_foo.org user.name@example.</span>
<span class="sx"> foo@bar_baz.com foo@bar+baz.com]</span>
<span class="n">invalid_addresses</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">invalid_address</span><span class="o">|</span>
<span class="vi">@user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="n">invalid_address</span>
<span class="n">assert_not</span> <span class="vi">@user</span><span class="o">.</span><span class="n">valid?</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">invalid_address</span><span class="o">.</span><span class="n">inspect</span><span class="si">}</span><span class="s2"> should be invalid"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>现在,测试应该失败:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.20:</span><strong class="red">RED</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
<p>电子邮件地址格式验证使用 <code>format</code> 参数,用法如下:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="nb">format</span><span class="p">:</span> <span class="p">{</span> <span class="ss">with</span><span class="p">:</span> <span class="sr">/<regular expression>/</span> <span class="p">}</span>
</pre></div>
</div>
<p>使用指定的正则表达式验证属性。正则表达式很强大,但往往很晦涩,用来模式匹配字符串。所以我们要编写一个正则表达式,匹配有效的电子邮件地址,但不匹配无效的地址。</p>
<p>在官方标准中其实有一个正则表达式,可以匹配全部有效的电子邮件地址,但没必要使用这么复杂的正则表达式。<sup>[<a id="fn-ref-11" href="#fn-11">11</a>]</sup>本书使用一个更务实的正则表达式,能很好地满足实际需求,如下所示:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="no">VALID_EMAIL_REGEX</span> <span class="o">=</span> <span class="sr">/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i</span>
</pre></div>
</div>
<p>为了便于理解,我把 <code>VALID_EMAIL_REGEX</code> 拆分成几块来讲,如<a href="#table-valid-email-regex">表 6.1</a> 所示。</p>
<table id="table-valid-email-regex" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 6.1:</span>拆解匹配有效电子邮件地址的正则表达式</caption>
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">表达式</th>
<th class="tableblock halign-left valign-top">含义</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>/\A[\w+\-.]@[a-z\d\-.]\.[a-z]+\z/i</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">完整的正则表达式</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>/</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">正则表达式开始</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>\A</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">匹配字符串的开头</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>[\w+\-.]+</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">一个或多个字母、加号、连字符、或点号</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>@</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">匹配 @ 符号</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>[a-z\d\-.]+</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">一个或多个字母、数字、连字符或点号</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>\.</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">匹配点号</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>[a-z]+</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">一个或多个字母</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>\z</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">匹配字符串结尾</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>/</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">结束正则表达式</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>i</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">不区分大小写</p></td>
</tr>
</tbody>
</table>
<p>从<a href="#table-valid-email-regex">表 6.1</a> 中虽然能学到很多,但若想真正理解正则表达式,我觉得交互式正则表达式匹配程序,例如 <a href="http://www.rubular.com/">Rubular</a>(<a href="#fig-rubular">图 6.6</a>)<sup>[<a id="fn-ref-12" href="#fn-12">12</a>]</sup>,是必不可少的的。Rubular 的界面很友好,便于编写所需的正则表达式,而且还有一个便捷的语法速查表。我建议你使用 Rubular 来理解<a href="#table-valid-email-regex">表 6.1</a>中的正则表达式——读得次数再多也不比不上在 Rubular 中实操几次。(注意:如果你在 Rubular 中输入<a href="#table-valid-email-regex">表 6.1</a> 中的正则表达式,要把 <code>\A</code> 和 <code>\z</code> 去掉,因为 Rubular 无法正确处理字符串的头尾。)</p>
<div id="fig-rubular" class="figure"><img src="images/chapter6/rubular.png" alt="rubular" /><div class="figcaption"><span class="title-label">图 6.6:</span>强大的 Rubular 正则表达式编辑器</div></div>
<p>在 <code>email</code> 属性的格式验证中使用这个表达式后得到的代码如<a href="#listing-validates-format-of-email">代码清单 6.21</a> 所示。</p>
<div id="listing-validates-format-of-email" data-type="listing">
<h5><span class="title-label">代码清单 6.21:</span>使用正则表达式验证电子邮件地址的格式 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">50</span> <span class="p">}</span>
<span class="hll"> <span class="no">VALID_EMAIL_REGEX</span> <span class="o">=</span> <span class="sr">/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i</span>
</span> <span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">255</span> <span class="p">},</span>
<span class="hll"> <span class="nb">format</span><span class="p">:</span> <span class="p">{</span> <span class="ss">with</span><span class="p">:</span> <span class="no">VALID_EMAIL_REGEX</span> <span class="p">}</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>其中,<code>VALID_EMAIL_REGEX</code> 是一个常量,在 Ruby 中常量的首字母为大写形式。这段代码:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="no">VALID_EMAIL_REGEX</span> <span class="o">=</span> <span class="sr">/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i</span>
<span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">255</span> <span class="p">},</span>
<span class="nb">format</span><span class="p">:</span> <span class="p">{</span> <span class="ss">with</span><span class="p">:</span> <span class="no">VALID_EMAIL_REGEX</span> <span class="p">}</span>
</pre></div>
</div>
<p>确保只有匹配正则表达式的电子邮件地址才是有效的。这个正则表达式有一个缺陷:能匹配 <code>[email protected]</code> 这种有连续点号的地址。修正这个瑕疵需要一个更复杂的正则表达式,留作练习由你完成(<a href="#modeling-users-exercises">6.5 节</a>)。</p>
<p>现在测试应该可以通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.22:</span><strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>:models
</pre></div>
</div>
<p>那么就只剩一个限制要实现了:确保电子邮件地址的唯一性。</p>
</section>
<section data-type="sect2" id="uniqueness-validation">
<h2><span class="title-label">6.2.5.</span> 唯一性验证</h2>
<p>确保电子邮件地址的唯一性(这样才能作为用户名),要使用 <code>validates</code> 方法的 <code>:unique</code> 参数。提前说明,实现的过程中有一个很大的陷阱,所以不要轻易跳过本节,要认真阅读。</p>
<p>我们要先编写一些简短的测试。之前的模型测试,只是使用 <code>User.new</code> 在内存中创建一个 Ruby 对象,但是测试唯一性时要把数据存入数据库。<sup>[<a id="fn-ref-13" href="#fn-13">13</a>]</sup>对重复电子邮件地址的测试如<a href="#listing-validates-uniqueness-of-email-test">代码清单 6.23</a> 所示。</p>
<div id="listing-validates-uniqueness-of-email-test" data-type="listing">
<h5><span class="title-label">代码清单 6.23:</span>拒绝重复电子邮件地址的测试 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="hll"> <span class="nb">test</span> <span class="s2">"email addresses should be unique"</span> <span class="k">do</span>
</span><span class="hll"> <span class="n">duplicate_user</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">dup</span>
</span><span class="hll"> <span class="vi">@user</span><span class="o">.</span><span class="n">save</span>
</span><span class="hll"> <span class="n">assert_not</span> <span class="n">duplicate_user</span><span class="o">.</span><span class="n">valid?</span>
</span><span class="hll"> <span class="k">end</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>我们使用 <code>@user.dup</code> 方法创建一个和 <code>@user</code> 的电子邮件地址一样的用户对象,然后保存 <code>@user</code>,因为数据库中的 <code>@user</code> 已经占用了这个电子邮件地址,所有 <code>duplicate_user</code> 对象无效。</p>
<p>在 <code>email</code> 属性的验证中加入 <code>uniqueness: true</code> 可以让<a href="#listing-validates-uniqueness-of-email-test">代码清单 6.23</a> 中的测试通过,如<a href="#listing-validates-uniqueness-of-email">代码清单 6.24</a> 所示。</p>
<div id="listing-validates-uniqueness-of-email" data-type="listing">
<h5><span class="title-label">代码清单 6.24:</span>电子邮件地址唯一性验证 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">50</span> <span class="p">}</span>
<span class="no">VALID_EMAIL_REGEX</span> <span class="o">=</span> <span class="sr">/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i</span>
<span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">255</span> <span class="p">},</span>
<span class="nb">format</span><span class="p">:</span> <span class="p">{</span> <span class="ss">with</span><span class="p">:</span> <span class="no">VALID_EMAIL_REGEX</span> <span class="p">},</span>
<span class="hll"> <span class="ss">uniqueness</span><span class="p">:</span> <span class="kp">true</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>这还不行,一般来说电子邮件地址不区分大小写,也就说 <code>[email protected]</code> 和 <code>[email protected]</code> 或 <code>[email protected]</code> 是同一个地址,所以验证时也要考虑这种情况。<sup>[<a id="fn-ref-14" href="#fn-14">14</a>]</sup>因此,还要测试不区分大小写,如<a href="#listing-validates-uniqueness-of-email-case-insensitive-test">代码清单 6.25</a> 所示。</p>
<div id="listing-validates-uniqueness-of-email-case-insensitive-test" data-type="listing">
<h5><span class="title-label">代码清单 6.25:</span>测试电子邮件地址的唯一性验证不区分大小写 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"email addresses should be unique"</span> <span class="k">do</span>
<span class="n">duplicate_user</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">dup</span>
<span class="hll"> <span class="n">duplicate_user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">email</span><span class="o">.</span><span class="n">upcase</span>
</span> <span class="vi">@user</span><span class="o">.</span><span class="n">save</span>
<span class="n">assert_not</span> <span class="n">duplicate_user</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>上面的代码,在字符串上调用 <code>upcase</code> 方法(<a href="chapter4.html#blocks">4.3.2 节</a>简介过)。这个测试和前面对重复电子邮件的测试作用一样,只是把地址转换成全部大写字母的形式。如果觉得太抽象,那就在控制台中实操一下吧:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><span class="go">$ rails console --sandbox</span>
<span class="gp">>> </span><span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="nb">name</span><span class="p">:</span> <span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email</span><span class="p">:</span> <span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="gp">>> </span><span class="n">user</span><span class="o">.</span><span class="n">email</span><span class="o">.</span><span class="n">upcase</span>
<span class="go">=> "[email protected]"</span>
<span class="gp">>> </span><span class="n">duplicate_user</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">dup</span>
<span class="gp">>> </span><span class="n">duplicate_user</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">email</span><span class="o">.</span><span class="n">upcase</span>
<span class="gp">>> </span><span class="n">duplicate_user</span><span class="o">.</span><span class="n">valid?</span>
<span class="go">=> true</span>
</pre></div>
</div>
<p>当然,现在 <code>duplicate_user.valid?</code> 的返回值是 <code>true</code>,因为唯一性验证还区分大小写。我们希望得到的结果是 <code>false</code>。幸好 <code>:uniqueness</code> 可以指定 <code>:case_sensitive</code> 选项,正好可以解决这个问题,如<a href="#listing-validates-uniqueness-of-email-case-insensitive">代码清单 6.26</a> 所示。</p>
<div id="listing-validates-uniqueness-of-email-case-insensitive" data-type="listing">
<h5><span class="title-label">代码清单 6.26:</span>电子邮件地址唯一性验证,不区分大小写 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">50</span> <span class="p">}</span>
<span class="no">VALID_EMAIL_REGEX</span> <span class="o">=</span> <span class="sr">/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i</span>
<span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span> <span class="ss">length</span><span class="p">:</span> <span class="p">{</span> <span class="ss">maximum</span><span class="p">:</span> <span class="mi">255</span> <span class="p">},</span>
<span class="nb">format</span><span class="p">:</span> <span class="p">{</span> <span class="ss">with</span><span class="p">:</span> <span class="no">VALID_EMAIL_REGEX</span> <span class="p">},</span>
<span class="hll"> <span class="ss">uniqueness</span><span class="p">:</span> <span class="p">{</span> <span class="ss">case_sensitive</span><span class="p">:</span> <span class="kp">false</span> <span class="p">}</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>注意,我们直接把 <code>true</code> 换成了 <code>case_sensitive: false</code>,Rails 会自动指定 <code>:uniqueness</code> 的值为 <code>true</code>。</p>
<p>至此,我们的应用虽还有不足,但基本可以保证电子邮件地址的唯一性了,测试组件应该可以通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 6.27:</span><strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
<p>现在还有一个小问题——Active Record 中的唯一性验证无法保证数据库层也能实现唯一性。我来解释一下:</p>
<ol class="arabic">
<li>
<p>Alice 使用 [email protected] 在演示应用中注册;</p>
</li>
<li>
<p>Alice 不小心按了两次提交按钮,连续发送了两次请求;</p>
</li>
<li>
<p>然后就会发生这种事情:请求 1 在内存中新建了一个用户对象,能通过验证;请求 2 也一样。请求 1 创建的用户存入了数据库,请求 2 创建的用户也存入了数据库。</p>
</li>
<li>
<p>结果是,尽管有唯一性验证,数据库中还是有两条用户记录的电子邮件地址是一样的。</p>
</li>
</ol>
<p>相信我,上面这种难以置信的情况可能发生,只要有一定的访问量,在任何 Rails 网站中都可能发生。幸好解决的办法很容易,只需在数据库层也加上唯一性限制。我们要做的是在数据库中为 <code>email</code> 列建立索引(<a href="#aside-database-indices">旁注 6.2</a>),然后为索引加上唯一性限制。</p>
<div data-type="sidebar" id="aside-database-indices" class="sidebar">
<h5>旁注 6.2:数据库索引</h5>
<p>在数据库中创建列时要考虑是否需要通过这个列查找记录。以<a href="#listing-users-migration">代码清单 6.2</a>中的迁移创建的 <code>email</code> 属性为例,<a href="chapter7.html#sign-up">第 7 章</a>实现登录功能后,我们要根据提交的电子邮件地址查找对应的用户记录。可是在这个简单的数据模型中通过电子邮件地址查找用户只有一种方法——检查数据库中的所有用户记录,比较记录中的 <code>email</code> 属性和指定的电子邮件地址。也就是说,可能要检查每一条记录(毕竟用户可能是数据库中的最后一条记录)。在数据库领域,这叫“全表扫描”。如果网站中有几千个用户,这可不是一件轻松的事。</p>
<p>在 <code>email</code> 列加上索引可以解决这个问题。我们可以把数据库索引看成书籍的索引。如果要在一本书中找出某个字符串(例如 <code>"foobar"</code>)出现的所有位置,需要翻看书中的每一页。但是如果有索引的话,只需在索引中找到 <code>"foobar"</code> 条目,就能看到所有包含 <code>"foobar"</code> 的页码。数据库索引基本上也是这种原理。</p>
</div>
<p>为 <code>email</code> 列建立索引要改变数据模型,在 Rails 中可以通过迁移实现。在 <a href="#database-migrations">6.1.1 节</a>我们看到,生成用户模型时会自动创建一个迁移文件(<a href="#listing-users-migration">代码清单 6.2</a>)。现在我们是要改变已经存在的模型结构,那么使用 <code>migration</code> 命令直接创建迁移文件就可以了:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate migration add_index_to_users_email
</pre></div>
</div>
<p>和用户模型的迁移不一样,实现电子邮件地址唯一性的操作没有事先定义好的模板可用,所以我们要自己动手编写,如<a href="#listing-email-uniqueness-index">代码清单 6.28</a> 所示。<sup>[<a id="fn-ref-15" href="#fn-15">15</a>]</sup></p>
<div id="listing-email-uniqueness-index" data-type="listing">