-
Notifications
You must be signed in to change notification settings - Fork 3
/
chapter6.html
1705 lines (1448 loc) · 160 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.5.1"/>
<link rel="stylesheet" type="text/css" href="//railstutorial-china.org/assets/css/main.css"/>
<link rel="stylesheet" type="text/css" href="book.css"/>
<script type="text/javascript" src="//railstutorial-china.org/assets/js/global.js"></script>
</head>
<body class="book-page">
<nav class="navbar">
<div class="container">
<div class="clearfix">
<a class="navbar-brand hidden-sm-up" href="//railstutorial-china.org/" title="Ruby on Rails 教程">Ruby on Rails 教程</a>
<button class="navbar-toggler hidden-sm-up pull-xs-right" type="button" data-toggle="collapse" data-target="#main-nav">☰</button>
</div>
<a class="navbar-brand hidden-xs-down" href="//railstutorial-china.org/" title="Ruby on Rails 教程">Ruby on Rails 教程</a>
<div class="collapse navbar-toggleable-xs pull-sm-right" id="main-nav">
<ul class="nav navbar-nav">
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/" title="首页">首页</a></li>
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/blog/" title="博客">博客</a></li>
<li class="nav-item active"><a class="nav-link" href="//railstutorial-china.org/book/" title="阅读">阅读</a></li>
<li class="nav-item"><a class="nav-link" href="//railstutorial-china.org/#ebook" title="电子书">电子书</a></li>
</ul>
</div>
</div>
</nav>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<div class="book-versions">
选择版本:
<a class="btn btn-primary" href="//railstutorial-china.org/book/" title="Ruby on Rails 教程(原书第 4 版,针对 Rails 5)">Rails 5</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails42/" title="Ruby on Rails 教程(原书第 3 版,针对 Rails 4.2)">Rails 4.2</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails4/" title="Ruby on Rails 教程(原书第 3 版,针对 Rails 4.0)">Rails 4.0</a>
<a class="btn btn-secondary" href="//railstutorial-china.org/rails3/" title="Ruby on Rails 教程(原书第 2 版,针对 Rails 3.2)">Rails 3.2</a>
</div>
<div class="alert alert-warning">
<p>在线版的内容可能落后于电子书,如果想及时获得更新,请<a href="//railstutorial-china.org/#ebook" title="购买电子书">购买电子书</a>。</p>
</div>
<article class="article">
<section data-type="chapter" id="modeling-users">
<h1><span class="title-label">第 6 章</span> 用户建模</h1>
<p><a class="xref-link" href="chapter5.html#filling-in-the-layout">第 5 章</a>末尾创建了一个临时的用户注册页面(<a class="xref-link" href="chapter5.html#user-signup">5.4 节</a>)。本书接下来的六章会逐步在这个页面中添加功能。本章我们将迈出关键的一步,创建网站中用户的数据模型(data model),并实现存储数据的方式。<a class="xref-link" href="chapter7.html#sign-up">第 7 章</a>会实现用户注册功能,并创建用户资料页面。用户能注册后,我们将实现登录和退出功能(<a class="xref-link" href="chapter8.html#basic-login">第 8 章</a>和<a class="xref-link" href="chapter9.html#advanced-login">第 9 章</a>)。<a class="xref-link" href="chapter10.html#updating-showing-and-deleting-users">第 10 章</a>(<a class="xref-link" href="chapter10.html#requiring-logged-in-users">10.2.1 节</a>)会介绍如何保护页面,禁止无权限的用户访问。最后,在<a class="xref-link" href="chapter11.html#account-activation">第 11 章</a>和<a class="xref-link" href="chapter12.html#password-reset">第 12 章</a>实现账户激活(从而确认电子邮件地址有效)和密码重设功能。<a class="xref-link" href="#modeling-users">第 6 章</a>到<a class="xref-link" href="chapter12.html#password-reset">第 12 章</a>的内容结合在一起,为 Rails 应用开发一个功能完整的登录和身份验证系统。你或许知道已经有很多开发好的 Rails 身份验证方案,<a class="xref-link" href="#aside-roll-your-own">旁注 6.1</a>会告诉你为什么至少在初学阶段,最好自己动手实现。</p>
<div data-type="sidebar" id="aside-roll-your-own" class="sidebar">
<h5><span class="title-label">旁注 6.1</span>:自己开发身份验证系统</h5>
<p>基本上所有 Web 应用都需要某种登录和身份验证系统。为此,大多数 Web 框架都提供了多种实现方式,Rails 也不例外。为 Rails 开发的身份验证和权限系统有 <a href="http://github.com/thoughtbot/clearance" class="external-link">Clearance</a>、<a href="http://github.com/binarylogic/authlogic" class="external-link">Authlogic</a>、<a href="http://github.com/plataformatec/devise" class="external-link">Devise</a> 和 <a href="http://railscasts.com/episodes/192-authorization-with-cancan" class="external-link">CanCan</a>。除此之外,还有一些不是 Rails 专用的方案,而是基于 <a href="http://en.wikipedia.org/wiki/OpenID" class="external-link">OpenID</a> 或 <a href="http://en.wikipedia.org/wiki/Oauth" class="external-link">OAuth</a> 实现。所以你肯定会问,为什么我们要重复制造轮子,为什么不直接使用现成的方案,而要自己开发呢?</p>
<p>首先,实践已经证明,大多数网站的身份验证系统都要对第三方产品做一些定制和修改,这往往比重新开发一个工作量还大。再者,现成的方案就像一个“黑盒”,你无法了解其中到底有些什么功能,而自己开发的话能更好地理解实现的过程。而且,Rails 最近的更新(参见 <a class="xref-link" 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> <code>User</code> 模型</h1>
<p>接下来的三章要实现网站的“注册”页面(构思图如<a class="xref-link" 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 class="xref-link" href="chapter1.html#mvc">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" class="external-link">关系数据库</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 class="xref-link" href="chapter1.html#deploying">1.5 节</a>)。这就引出了一个更深层的话题——在不同的环境中,即便使用不同类型的数据库,我们也无需关心 Rails 是如何存储数据的。</p>
<p>和之前一样,如果使用 Git 做版本控制,现在应该新建一个主题分支,用于建模用户:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>git checkout <span class="nt">-b</span> modeling-users
</code></pre></div>
</div>
<section data-type="sect2" id="database-migrations">
<h2><span class="title-label">6.1.1</span> 数据库迁移</h2>
<p>回顾一下 <a class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>的内容,我们在自己创建的 <code>User</code> 类中为用户对象定义了 <code>name</code> 和 <code>email</code> 两个属性。那是个很有用的例子,但没有实现持久化存储最关键的要求:在 Rails 控制台中创建的用户对象,退出控制台后就会消失。本节的目的是为用户创建一个模型,让用户数据不会这么轻易消失。</p>
<p>与 <a class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>中定义的 <code>User</code> 类一样,我们先为 <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 class="xref-link" href="#adding-a-secure-password">6.3 节</a>会添加一个属性,用于存储密码。)在<a class="xref-link" href="chapter4.html#listing-example-user">代码清单 4.17</a> 中,我们使用 Ruby 的 <code>attr_accessor</code> 方法创建了这两个属性:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span>
<span class="nb">attr_accessor</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</code></pre></div>
</div>
<p>不过,在 Rails 中不用这样定义属性。前面提到过,Rails 默认使用关系数据库存储数据,数据库中的表由数据行(row)组成,每一行都有相应的列(column),对应于数据属性。例如,为了存储用户的名字和电子邮件地址,我们要创建 <code>users</code> 表,表中有两个列,<code>name</code> 和 <code>email</code>,这样每一行就表示一个用户,如<a class="xref-link" href="#fig-users-table">图 6.2</a> 所示,对应的数据模型如<a class="xref-link" href="#fig-user-model-sketch">图 6.3</a> 所示。(<a class="xref-link" href="#fig-user-model-sketch">图 6.3</a> 只是梗概,完整的数据模型如<a class="xref-link" 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>:<code>User</code> 数据模型梗概</div></div>
<p>你可能还记得,在<a class="xref-link" href="chapter5.html#listing-generate-users-controller">代码清单 5.38</a> 中,我们使用下面的命令生成了 <code>Users</code> 控制器和 <code>new</code> 动作:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails generate controller Users new
</code></pre></div>
</div>
<p>创建模型有个类似的命令——<code>generate model</code>。我们可以使用这个命令生成 <code>User</code> 模型,以及 <code>name</code> 和 <code>email</code> 属性,如<a class="xref-link" 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>:生成 <code>User</code> 模型</h5>
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails generate model User name:string email:string
invoke active_record
create db/migrate/20160523010738_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
</code></pre></div>
</div>
<p>(注意,控制器名是复数,模型名是单数:控制器是 <code>Users</code>,而模型是 <code>User</code>。)我们指定了可选的参数 <code>name:string</code> 和 <code>email:string</code>,告诉 Rails 我们需要的两个属性是什么,以及各自的类型(两个都是字符串)。你可以把这两个参数与<a class="xref-link" href="chapter3.html#listing-generating-pages">代码清单 3.6</a> 和<a class="xref-link" href="chapter5.html#listing-generate-users-controller">代码清单 5.38</a> 中的动作名对比一下,看看有什么不同。</p>
<p>执行上述 <code>generate</code> 命令之后,会生成一个迁移文件。迁移是一种递进修改数据库结构的方式,可以根据需求修改数据模型。执行上述 <code>generate</code> 命令后会自动为 <code>User</code> 模型创建迁移,这个迁移的作用是创建一个 <code>users</code> 表,以及 <code>name</code> 和 <code>email</code> 两个列,如<a class="xref-link" href="#listing-users-migration">代码清单 6.2</a> 所示。(我们会在 <a class="xref-link" 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>User</code> 模型的迁移文件(创建 <code>users</code> 表)</h5>
<div class="source-file">db/migrate/[timestamp]_create_users.rb</div>
<div class="highlight language-ruby"><pre><code><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="p">[</span><span class="mf">5.0</span><span class="p">]</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="p">.</span><span class="nf">string</span> <span class="ss">:name</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:email</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>注意,迁移文件名前面有个时间戳(timestamp),指明创建的时间。早期,迁移文件名的前缀是递增的数字,在团队协作中,如果多个程序员生成了序号相同的迁移文件就可能会发生冲突。除非两个迁移文件在同一秒钟生成这种小概率事件发生了,否则使用时间戳基本可以避免冲突。</p>
<p>迁移文件中有一个名为 <code>change</code> 的方法,定义要对数据库做什么操作。在<a class="xref-link" 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</code> 是个特殊的方法,它会自动创建 <code>created_at</code> 和 <code>updated_at</code> 两个列,分别记录创建用户的时间戳和更新用户的时间戳。(<a class="xref-link" href="#creating-user-objects">6.1.3 节</a>有使用这两个列的例子。)这个迁移文件表示的完整数据模型如<a class="xref-link" href="#fig-user-model-initial">图 6.4</a> 所示。(注意,<a class="xref-link" 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 class="xref-link" href="#listing-users-migration">代码清单 6.2</a> 生成的 <code>User</code> 数据模型</div></div>
<p>我们可以使用如下的 <code>db:migrate</code> 命令执行这个迁移(这叫“向上迁移”):</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails db:migrate
</code></pre></div>
</div>
<p>(你可能还记得,我们在 <a class="xref-link" href="chapter2.html#demo-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/" class="external-link">SQLite 数据库浏览器</a>打开 <code>db/development.sqlite3</code> 文件,如<a class="xref-link" href="#fig-sqlite-database-browser">图 6.5</a> 所示。(如果使用云端 IDE,要先把数据库文件下载到本地磁盘中,如<a class="xref-link" href="#fig-sqlite-download">图 6.6</a> 所示。)与<a class="xref-link" href="#fig-user-model-initial">图 6.4</a> 中的模型对比之后,你可能会发现有一个列在迁移中没有出现——<code>id</code> 列。<a class="xref-link" href="chapter2.html#demo-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>
<div id="fig-sqlite-download" class="figure"><img src="images/chapter6/sqlite_download.png" alt="sqlite download" /><div class="figcaption"><span class="title-label">图 6.6</span>:从云端 IDE 中下载文件</div></div>
<h5 id="exercises-database-migrations" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>Rails 使用 <code>db/</code> 目录中的 <code>schema.rb</code> 文件记录数据库的结构[称作模式(schema),因此才用这个文件名]。打开你应用中的 <code>db/schema.rb</code> 文件,与<a class="xref-link" href="#listing-users-migration">代码清单 6.2</a> 中的迁移代码比较一下。</p>
</li>
<li>
<p>大多数迁移,包括本书中的所有迁移,都是可逆的,也就是说可以使用一个简单的命令“向下迁移”,撤销之前的操作。这个命令是 <code>db:rollback</code>:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><code><span class="nv">$ </span>rails db:rollback
</code></pre></div>
</div>
<p>执行上述命令后,查看 <code>db/schema.rb</code> 文件,确认成功回滚了。</p>
<p>(还有一个撤销迁移的方法,参见<a class="xref-link" 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" class="external-link">Rails 指南</a>。</p>
</li>
<li>
<p>执行 <code>rails db:migrate</code> 命令,重新执行迁移。确认 <code>db/schema.rb</code> 文件的内容确实还原了。</p>
</li>
</ol>
</section>
<section data-type="sect2" id="the-model-file">
<h2><span class="title-label">6.1.2</span> 模型文件</h2>
<p>我们看到,执行<a class="xref-link" href="#listing-generate-user-model">代码清单 6.1</a> 中的命令后会生成一个迁移文件(<a class="xref-link" href="#listing-users-migration">代码清单 6.2</a>),也看到了执行迁移后得到的结果(<a class="xref-link" href="#fig-sqlite-database-browser">图 6.5</a>):修改 <code>development.sqlite3</code> 文件,新建 <code>users</code> 表,并创建 <code>id</code>、<code>name</code>、<code>email</code>、<code>created_at</code> 和 <code>updated_at</code> 等列。<a class="xref-link" href="#listing-generate-user-model">代码清单 6.1</a> 还生成了一个模型文件,本节剩下的内容专门讲解这个文件。</p>
<p>我们先看 <code>User</code> 模型的代码,在 <code>app/models/</code> 目录中的 <code>user.rb</code> 文件里。这个文件的内容非常简单,如<a class="xref-link" 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>:刚创建的 <code>User</code> 模型</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p><a class="xref-link" href="chapter4.html#class-inheritance">4.4.2 节</a>介绍过,<code>class User < ApplicationRecord</code> 的意思是 <code>User</code> 类继承自 <code>ApplicationRecord</code> 类(而它继承自 <code>ActiveRecord::Base</code> 类,参见<a class="xref-link" href="chapter2.html#fig-demo-model-inheritance">图 2.18</a>),所以 <code>User</code> 模型自动获得了 <code>ActiveRecord::Base</code> 的所有功能。当然了,只知道这种继承关系没什么用,我们并不知道 <code>ActiveRecord::Base</code> 做了什么。下面看几个实例。</p>
<h5 id="exercises-the-model-file" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>根据 <a class="xref-link" href="chapter4.html#a-controller-class">4.4.4 节</a>所讲的知识,在 Rails 控制台中确认 <code>User.new</code> 属于 <code>User</code> 类,而它继承自 <code>ApplicationRecord</code> 类。</p>
</li>
<li>
<p>确认 <code>ApplicationRecord</code> 继承自 <code>ActiveRecord::Base</code>。</p>
</li>
</ol>
</section>
<section data-type="sect2" id="creating-user-objects">
<h2><span class="title-label">6.1.3</span> 创建用户对象</h2>
<p>和<a class="xref-link" href="chapter4.html#rails-flavored-ruby">第 4 章</a>一样,探索数据模型使用的工具是 Rails 控制台。因为我们(还)不想修改数据库中的数据,所以要在沙盒(sandbox)模式中启动控制台:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><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>
</code></pre></div>
</div>
<p>如提示消息所说,“Any modifications you make will be rolled back on exit”,在沙盒模式下使用控制台,退出当前会话后,对数据库做的所有改动都会回归到原来的状态(即撤销)。</p>
<p>在 <a class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>的控制台会话中,我们要引入<a class="xref-link" href="chapter4.html#listing-example-user">代码清单 4.17</a> 中的代码才能使用 <code>User.new</code> 创建用户对象。对模型来说,情况有所不同。你可能还记得 <a class="xref-link" href="chapter4.html#a-controller-class">4.4.4 节</a>说过,Rails 控制台会自动加载 Rails 环境,这其中就包括模型。也就是说,现在无需加载任何代码就可以直接创建用户对象:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> User.new</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">name: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">email: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">created_at: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="kp">nil</span><span class="kt">></span>
</code></pre></div>
</div>
<p>上述代码显示了用户对象在控制台中的默认表述。</p>
<p>如果不为 <code>User.new</code> 指定参数,对象的所有属性值都是 <code>nil</code>。在 <a class="xref-link" 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><code><span class="go">>> user = User.new(name: "Michael Hartl", email: "[email protected]")</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="kp">nil</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="kp">nil</span><span class="kt">></span>
</code></pre></div>
</div>
<p>我们看到 <code>name</code> 和 <code>email</code> 属性的值都已经按预期设定了。</p>
<p>数据的有效性(validity)对理解 Active Record 模型对象很重要,我们会在 <a class="xref-link" href="#user-validations">6.2 节</a>深入探讨。不过注意,现在这个 <code>user</code> 对象是有效的,我们可以在这个对象上调用 <code>valid?</code> 方法确认:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> user.valid?</span>
<span class="go">true</span>
</code></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><code><span class="go">>> user.save</span>
<span class="go"> (0.1ms) SAVEPOINT active_record_1</span>
<span class="go"> SQL (0.8ms) INSERT INTO "users" ("name", "email", "created_at",</span>
<span class="go"> "updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"],</span>
<span class="go"> ["email", "[email protected]"], ["created_at", 2016-05-23 19:05:58 UTC],</span>
<span class="go"> ["updated_at", 2016-05-23 19:05:58 UTC]]</span>
<span class="go"> (0.1ms) RELEASE SAVEPOINT active_record_1</span>
<span class="p">=></span> <span class="kp">true</span>
</code></pre></div>
</div>
<p>如果保存成功,<code>save</code> 方法返回 <code>true</code>,否则返回 <code>false</code>。(现在所有保存操作都会成功,因为还没有数据验证;<a class="xref-link" 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><code><span class="go">>> user.save</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="kt">></span>
</code></pre></div>
</div>
<p>我们看到,<code>id</code> 的值变成了 <code>1</code>,那两个自动创建的时间戳属性也变成了当前时间和日期。<sup>[<a id="fn-ref-7" href="#fn-7">7</a>]</sup>现在这两个时间戳是一样的,<a class="xref-link" href="#updating-user-objects">6.1.5 节</a>会看到二者不同的情况。</p>
<p>与 <a class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>定义的 <code>User</code> 类一样,<code>User</code> 模型的实例也可以使用点号获取属性:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> user.name</span>
<span class="p">=></span> <span class="s2">"Michael Hartl"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">email</span>
<span class="o">=></span> <span class="s2">"[email protected]"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">updated_at</span>
<span class="o">=></span> <span class="no">Mon</span><span class="p">,</span> <span class="mi">23</span> <span class="no">May</span> <span class="mi">2016</span> <span class="mi">19</span><span class="p">:</span><span class="mo">05</span><span class="p">:</span><span class="mi">58</span> <span class="no">UTC</span> <span class="o">+</span><span class="mo">00</span><span class="p">:</span><span class="mo">00</span>
</code></pre></div>
</div>
<p><a class="xref-link" href="chapter7.html#sign-up">第 7 章</a>会介绍,虽然一般习惯把创建和保存分成如上所示的两步完成,不过 Active Record 也允许我们使用 <code>User.create</code> 方法把这两步合成一步:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> User.create(name: "A Nother", email: "[email protected]")</span>
<span class="c">#<User id: 2, name: "A Nother", email: "[email protected]", created_at:</span>
<span class="go">"2016-05-23 19:18:46", updated_at: "2016-05-23 19:18:46"></span>
<span class="go">>> foo = User.create(name: "Foo", email: "[email protected]")</span>
<span class="c">#<User id: 3, name: "Foo", email: "[email protected]", created_at: "2016-05-23</span>
<span class="go">19:19:06", updated_at: "2016-05-23 19:19:06"></span>
</code></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><code><span class="go">>> foo.destroy</span>
<span class="go"> (0.1ms) SAVEPOINT active_record_1</span>
<span class="go"> SQL (0.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]</span>
<span class="go"> (0.1ms) RELEASE SAVEPOINT active_record_1</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Foo"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span> <span class="ss">created_at: </span><span class="s2">"2016-05-23</span>
<span class="s2">19:19:06"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:19:06"</span><span class="kt">></span>
</code></pre></div>
</div>
<p>奇怪的是,<code>destroy</code> 和 <code>create</code> 一样,返回值是对象。我不觉得什么地方会用到 <code>destroy</code> 的返回值。更奇怪的是,销毁的对象还在内存中:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> foo</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">3</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Foo"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span> <span class="ss">created_at: </span><span class="s2">"2016-05-23</span>
<span class="s2">19:19:06"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:19:06"</span><span class="kt">></span>
</code></pre></div>
</div>
<p>那么我们怎么知道对象是否真被销毁了呢?对于已经保存而没有销毁的对象,怎样从数据库中读取呢?要回答这些问题,我们要先学习如何使用 Active Record 查找用户对象。</p>
<h5 id="exercises-creating-user-objects" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>确认 <code>user.name</code> 和 <code>user.email</code> 属于 <code>String</code> 类。</p>
</li>
<li>
<p><code>created_at</code> 和 <code>updated_at</code> 属性的值属于哪个类?</p>
</li>
</ol>
</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><code><span class="go">>> User.find(1)</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="kt">></span>
</code></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><code><span class="go">>> User.find(3)</span>
<span class="go">ActiveRecord::RecordNotFound: Couldn't find User with ID=3</span>
</code></pre></div>
</div>
<p>因为我们在 <a class="xref-link" href="#creating-user-objects">6.1.3 节</a>销毁了第三个用户,所以 Active Record 无法在数据库中找到这个用户,从而抛出一个异常(exception),这说明在查找过程中出现了问题。因为 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><code><span class="go">>> User.find_by(email: "[email protected]")</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="kt">></span>
</code></pre></div>
</div>
<p>我们将使用电子邮件地址做用户名,在学习如何让用户登录网站时会用到这种 <code>find</code> 方法(<a class="xref-link" href="chapter7.html#sign-up">第 7 章</a>)。你可能会担心如果用户数量过多,使用 <code>find_by</code> 的效率不高。事实的确如此,我们会在 <a class="xref-link" href="#uniqueness-validation">6.2.5 节</a>说明这个问题,以及如何使用数据库索引解决。</p>
<p>最后,再介绍几个常用的查找方法。首先是 <code>first</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> User.first</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="kt">></span>
</code></pre></div>
</div>
<p>很明显,<code>first</code> 会返回数据库中的第一个用户。还有 <code>all</code> 方法:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> User.all</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Relation</span> <span class="p">[</span><span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span>
<span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span> <span class="ss">created_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="p">,</span>
<span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="kt">></span><span class="p">,</span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"A Nother"</span><span class="p">,</span>
<span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span> <span class="ss">created_at: </span><span class="s2">"2016-05-23 19:18:46"</span><span class="p">,</span>
<span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:18:46"</span><span class="kt">></span><span class="p">]</span><span class="kt">></span>
</code></pre></div>
</div>
<p>从控制台的输出可以看出,<code>User.all</code> 方法返回一个 <code>ActiveRecord::Relation</code> 实例,其实这是一个数组(<a class="xref-link" href="chapter4.html#arrays-and-ranges">4.3.1 节</a>),
包含数据库中的所有用户。</p>
<h5 id="exercises-finding-user-objects" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>通过用户的名字(<code>name</code>)查找用户。确认也可以使用 <code>find_by_name</code> 方法。(在旧的 Rails 应用中经常能见到这种旧的 <code>find_by</code> 方法。)</p>
</li>
<li>
<p><code>User.all</code> 得到的结果虽然行为类似于数组,但它不是数组,确认它其实属于 <code>User::ActiveRecord_Relation</code> 类。</p>
</li>
<li>
<p>确认可以使用 <code>length</code> 方法(<a class="xref-link" href="chapter4.html#objects-and-message-passing">4.2.3 节</a>)获取 <code>User.all</code> 的长度。在 Ruby 中,我们根据对象的行为而不是所属的类确定能对对象执行什么操作,这叫鸭子类型(duck typing),意思是:“如果看起来像鸭子,叫起来也像鸭子,那么它可能就是鸭子”。</p>
</li>
</ol>
</section>
<section data-type="sect2" id="updating-user-objects">
<h2><span class="title-label">6.1.5</span> 更新用户对象</h2>
<p>创建对象后,一般都会进行更新操作。更新有两种基本方式,其一,可以分别为各个属性赋值,在 <a class="xref-link" href="chapter4.html#a-user-class">4.4.5 节</a>就是这么做的:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> user # 只是为了查看 user 对象的属性是什么</span>
<span class="p">=></span> <span class="kt">#<</span><span class="no">User</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Michael Hartl"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="p">,</span> <span class="ss">updated_at: </span><span class="s2">"2016-05-23 19:05:58"</span><span class="kt">></span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">email</span> <span class="o">=</span> <span class="s2">"[email protected]"</span>
<span class="o">=></span> <span class="s2">"[email protected]"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">save</span>
<span class="o">=></span> <span class="kp">true</span>
</code></pre></div>
</div>
<p>注意,如果想把改动写入数据库,必须执行最后一个方法。我们可以执行 <code>reload</code> 命令来看一下没保存的话是什么情况。<code>reload</code> 方法会使用数据库中的数据重新加载对象:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> user.email</span>
<span class="p">=></span> <span class="s2">"[email protected]"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">email</span> <span class="o">=</span> <span class="s2">"[email protected]"</span>
<span class="o">=></span> <span class="s2">"[email protected]"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">reload</span><span class="p">.</span><span class="nf">email</span>
<span class="o">=></span> <span class="s2">"[email protected]"</span>
</code></pre></div>
</div>
<p>现在我们已经更新了用户数据,如 <a class="xref-link" href="#creating-user-objects">6.1.3 节</a>所说,现在自动创建的那两个时间戳属性不一样了:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> user.created_at</span>
<span class="p">=></span> <span class="s2">"2016-05-23 19:05:58"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">updated_at</span>
<span class="o">=></span> <span class="s2">"2016-05-23 19:08:23"</span>
</code></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><code><span class="go">>> user.update_attributes(name: "The Dude", email: "[email protected]")</span>
<span class="p">=></span> <span class="kp">true</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">name</span>
<span class="o">=></span> <span class="s2">"The Dude"</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">email</span>
<span class="o">=></span> <span class="s2">"[email protected]"</span>
</code></pre></div>
</div>
<p><code>update_attributes</code> 方法接受一个指定对象属性的散列作为参数,如果操作成功,会执行更新和保存两个操作(保存成功时返回 <code>true</code>)。注意,如果任何一个数据验证失败了,例如存储记录时需要密码(<a class="xref-link" 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><code><span class="go">>> user.update_attribute(:name, "El Duderino")</span>
<span class="p">=></span> <span class="kp">true</span>
<span class="o">>></span> <span class="n">user</span><span class="p">.</span><span class="nf">name</span>
<span class="o">=></span> <span class="s2">"El Duderino"</span>
</code></pre></div>
</div>
<h5 id="exercises-updating-user-objects" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>通过赋值更新用户的名字,然后调用 <code>save</code> 方法。</p>
</li>
<li>
<p>调用 <code>update_attributes</code> 方法,更新用户的电子邮件地址。</p>
</li>
<li>
<p>通过赋值更新 <code>created_at</code> 列的值,然后调用 <code>save</code> 方法,以此确认特殊的列也可以直接更新。把这一列的值设为 <code>1.year.ago</code>,这是 Rails 扩展的功能,作用是创建距当前时间一年前的时间戳。</p>
</li>
</ol>
</section>
</section>
<section data-type="sect1" id="user-validations">
<h1><span class="title-label">6.2</span> 验证用户数据</h1>
<p><a class="xref-link" href="#user-model">6.1 节</a>创建的 <code>User</code> 模型现在已经有了可以使用的 <code>name</code> 和 <code>email</code> 属性,不过功能还很简单:任何字符串(包括空字符串)都可以使用。名字和电子邮件地址的格式显然要复杂一些。例如,<code>name</code> 不应该是空的,<code>email</code> 应该符合特定的格式。而且,我们将把电子邮件地址当成用户名用来登录,那么在数据库中就不能重复出现。</p>
<p>总之,<code>name</code> 和 <code>email</code> 不是什么字符串都可以使用的,我们要对它们可以使用的值做个限制。Active Record 通过数据验证(validation)实现这种限制(<a class="xref-link" href="chapter2.html#putting-the-micro-in-microposts">2.3.2 节</a>简单提到过)。本节介绍几种常用的数据验证:存在性、长度、格式和唯一性。<a class="xref-link" href="#user-has-secure-password">6.3.2 节</a>还会介绍另一种常用的数据验证——二次确认。<a class="xref-link" 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 class="xref-link" href="chapter3.html#aside-when-to-test">旁注 3.3</a>说过,TDD 并不适用所有情况,但是模型验证是使用 TDD 的绝佳时机。如果不先编写失败测试,再想办法让它通过,我们很难确定验证是否实现了我们希望实现的功能。</p>
<p>我们采用的方法是,先得到一个有效的模型对象,然后把属性改为无效值,以此确认这个对象是无效的。以防万一,我们先编写一个测试,确认模型对象一开始是有效的。这样,如果验证测试失败了,我们才知道的确事出有因(而不是因为一开始对象是无效的)。</p>
<p><a class="xref-link" href="#listing-generate-user-model">代码清单 6.1</a> 中的命令生成了一个用于测试 <code>User</code> 模型的测试文件,现在这个文件中还没什么内容,如<a class="xref-link" 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>:还没什么内容的 <code>User</code> 模型测试文件</h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><code><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>
</code></pre></div>
</div>
<p>为了测试有效的对象,我们要在特殊的 <code>setup</code> 方法中创建一个有效的用户对象 <code>@user</code>。<a class="xref-link" href="chapter3.html#mostly-static-pages">第 3 章</a>的练习中提到过,<code>setup</code> 方法会在每个测试方法运行前执行。因为 <code>@user</code> 是实例变量,所以自动可在所有测试方法中使用,而且我们可以使用 <code>valid?</code> 方法检查它是否有效。测试如<a class="xref-link" 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><code><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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email: </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="p">.</span><span class="nf">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p><a class="xref-link" href="#listing-valid-user-test">代码清单 6.5</a> 使用简单的 <code>assert</code> 方法,如果 <code>@user.valid?</code> 返回 <code>true</code>,测试就能通过;返回 <code>false</code>,测试则会失败。</p>
<p>因为 <code>User</code> 模型现在还没有任何验证,所有这个测试可以通过:</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><code><span class="nv">$ </span>rails <span class="nb">test</span>:models
</code></pre></div>
</div>
<p>这里,我们使用 <code>rails test:models</code> 命令,只运行模型测试(与 <a class="xref-link" href="chapter5.html#layout-link-tests">5.3.4 节</a>的 <code>rails test:integration</code> 对比一下)。</p>
<h5 id="exercises-a-validity-test" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>在控制台中确认新建的用户现在是有效的。</p>
</li>
<li>
<p>确认 <a class="xref-link" href="#creating-user-objects">6.1.3 节</a>创建的用户也是有效的。</p>
</li>
</ol>
</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 class="xref-link" href="chapter7.html#signup-error-messages">7.3.3 节</a>会介绍如何把这个限制应用到创建用户的注册表单中。</p>
<p>我们要先在<a class="xref-link" href="#listing-valid-user-test">代码清单 6.5</a> 的基础上再编写一个测试,检查 <code>name</code> 属性是否存在。如<a class="xref-link" 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><code><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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email: </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="p">.</span><span class="nf">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="p">.</span><span class="nf">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="p">.</span><span class="nf">valid?</span></span>
<span class="hll"> <span class="k">end</span></span>
<span class="k">end</span>
</code></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><code><span class="nv">$ </span>rails <span class="nb">test</span>:models
</code></pre></div>
</div>
<p>我们在<a class="xref-link" href="chapter2.html#a-toy-app">第 2 章</a>的练习中见过,<code>name</code> 属性的存在性验证使用 <code>validates</code> 方法,而且其参数为 <code>presence: true</code>,如<a class="xref-link" href="#listing-validates-presence-of-name">代码清单 6.9</a> 所示。<code>presence: true</code> 是只有一个元素的可选散列参数;<a class="xref-link" href="chapter4.html#css-revisited">4.3.4 节</a>说过,如果方法的最后一个参数是散列,可以省略花括号。(<a class="xref-link" 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><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</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="kp">true</span></span>
<span class="k">end</span>
</code></pre></div>
</div>
<p><a class="xref-link" href="#listing-validates-presence-of-name">代码清单 6.9</a> 中的代码看起来可能有点儿神奇,其实 <code>validates</code> 就是个方法。加入括号后,可以写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</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="kp">true</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>打开控制台,看一下在 <code>User</code> 模型中加入验证后有什么效果:<sup>[<a id="fn-ref-10" href="#fn-10">10</a>]</sup></p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">$ rails console --sandbox</span>
<span class="go">>> user = User.new(name: "", email: "[email protected]")</span>
<span class="go">>> user.valid?</span>
<span class="p">=></span> <span class="kp">false</span>
</code></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><code><span class="go">>> user.errors.full_messages</span>
<span class="p">=></span> <span class="p">[</span><span class="s2">"Name can't be blank"</span><span class="p">]</span>
</code></pre></div>
</div>
<p>(错误消息暗示,Rails 使用 <a class="xref-link" href="chapter4.html#modifying-built-in-classes">4.4.3 节</a>介绍的 <code>blank?</code> 方法验证存在性。)</p>
<p>因为用户无效,如果尝试把它保存到数据库中,操作会失败:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> user.save</span>
<span class="p">=></span> <span class="kp">false</span>
</code></pre></div>
</div>
<p>加入验证后,<a class="xref-link" 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><code><span class="nv">$ </span>rails <span class="nb">test</span>:models
</code></pre></div>
</div>
<p>按照<a class="xref-link" href="#listing-name-presence-test">代码清单 6.7</a> 的方式,再编写一个检查 <code>email</code> 属性存在性的测试就简单了,如<a class="xref-link" href="#listing-email-presence-test">代码清单 6.11</a> 所示。让这个测试通过的应用代码如<a class="xref-link" 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><code><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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email: </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="p">.</span><span class="nf">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="p">.</span><span class="nf">name</span> <span class="o">=</span> <span class="s2">""</span>
<span class="n">assert_not</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">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="p">.</span><span class="nf">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="p">.</span><span class="nf">valid?</span></span>
<span class="hll"> <span class="k">end</span></span>
<span class="k">end</span>
</code></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><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence: </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="kp">true</span></span>
<span class="k">end</span>
</code></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><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<h5 id="exercises-validating-presence" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>新建一个用户,赋值给变量 <code>u</code>,确认一开始这个用户对象是无效的。看一下完整的错误消息是什么。</p>
</li>
<li>
<p>确认 <code>u.errors.messages</code> 是一个散列。怎么获取电子邮件地址相关的错误呢?</p>
</li>
</ol>
</section>
<section data-type="sect2" id="length-validation">
<h2><span class="title-label">6.2.3</span> 长度验证</h2>
<p>我们已经对 <code>User</code> 模型可接受的数据做了一些限制,现在必须为用户提供一个名字,不过我们应该做进一步限制,因为用户的名字会在演示应用中显示,所以最好限制它的长度。有了前一节的基础,这一步就简单了。</p>
<p>没有科学的方法确定最大长度应该是多少,我们就使用 50 作为长度的上限吧。因此,我们要验证 51 个字符超长了。此外,用户的电子邮件地址可能会超过字符串的最大长度限制,这个最大值在很多数据库中都是 255——这种情况虽然很少发生,但也有发生的可能。因为下一节的格式验证无法实现这种限制,所以我们要在这一节实现。测试如<a class="xref-link" 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><code><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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">test</span> <span class="s2">"name should not be too long"</span> <span class="k">do</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">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="p">.</span><span class="nf">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="p">.</span><span class="nf">email</span> <span class="o">=</span> <span class="s2">"a"</span> <span class="o">*</span> <span class="mi">244</span> <span class="o">+</span> <span class="s2">"@example.com"</span>
<span class="n">assert_not</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>为了方便,我们使用字符串连乘生成了一个有 51 个字符的字符串。在控制台中可以看到连乘是什么:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> "a" * 51</span>
<span class="p">=></span> <span class="s2">"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"</span>
<span class="o">>></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="nf">length</span>
<span class="o">=></span> <span class="mi">51</span>
</code></pre></div>
</div>
<p>在电子邮件地址长度的测试中,我们创建了一个比要求多一个字符的地址:</p>
<div data-type="listing">
<div class="highlight language-irb"><pre><code><span class="go">>> "a" * 244 + "@example.com"</span>
<span class="p">=></span> <span class="s2">"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
<span class="s2">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
<span class="s2">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
<span class="s2">[email protected]"</span>
<span class="o">>></span> <span class="p">(</span><span class="s2">"a"</span> <span class="o">*</span> <span class="mi">244</span> <span class="o">+</span> <span class="s2">"@example.com"</span><span class="p">).</span><span class="nf">length</span>
<span class="o">=></span> <span class="mi">256</span>
</code></pre></div>
</div>
<p>现在,<a class="xref-link" 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><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<p>为了让测试通过,我们要使用验证参数限制长度,即 <code>length</code>,以及限制上线的 <code>maximum</code> 参数,如<a class="xref-link" 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><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</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="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">50</span> <span class="p">}</span></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="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">255</span> <span class="p">}</span></span>
<span class="k">end</span>
</code></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><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<p>测试组件再次通过,接下来我们要实现一个更有挑战的验证——电子邮件地址的格式。</p>
<h5 id="exercises-length-validation" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>使用非常长的名字和电子邮件地址创建一个用户,确认它是无效的。</p>
</li>
<li>
<p>长度验证生成的错误消息是什么?</p>
</li>
</ol>
</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><code><span class="go">>> %w[foo bar baz]</span>
<span class="p">=></span> <span class="p">[</span><span class="s2">"foo"</span><span class="p">,</span> <span class="s2">"bar"</span><span class="p">,</span> <span class="s2">"baz"</span><span class="p">]</span>
<span class="o">>></span> <span class="n">addresses</span> <span class="o">=</span> <span class="sx">%w[[email protected] [email protected] [email protected]]</span>
<span class="o">=></span> <span class="p">[</span><span class="s2">"[email protected]"</span><span class="p">,</span> <span class="s2">"[email protected]"</span><span class="p">,</span> <span class="s2">"[email protected]"</span><span class="p">]</span>
<span class="o">>></span> <span class="n">addresses</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">address</span><span class="o">|</span>
<span class="sc">?></span> <span class="nb">puts</span> <span class="n">address</span>
<span class="o">>></span> <span class="k">end</span>
<span class="no">USER</span><span class="vi">@foo</span><span class="o">.</span><span class="no">COM</span>
<span class="no">THE_US</span><span class="o">-</span><span class="no">ER</span><span class="vi">@foo</span><span class="p">.</span><span class="nf">bar</span><span class="p">.</span><span class="nf">org</span>
<span class="n">first</span><span class="p">.</span><span class="nf">last</span><span class="vi">@foo</span><span class="p">.</span><span class="nf">jp</span>
</code></pre></div>
</div>
<p>在上面的控制台会话中,我们使用 <code>each</code> 方法(<a class="xref-link" href="chapter4.html#blocks">4.3.2 节</a>)遍历 <code>addresses</code> 数组中的元素。掌握这种用法之后,我们就可以编写一些基本的电子邮件地址格式验证测试了。</p>
<p>电子邮件地址格式验证有点棘手,而且容易出错,所以我们会先编写检查有效电子邮件地址的测试,这些测试应该能通过,以此捕获验证可能出现的错误。也就是说,添加验证后,不仅要拒绝无效的电子邮件地址,例如 <em>user@example,com</em>,还得接受有效的电子邮件地址,例如 <em>[email protected]</em>。(显然目前会接受所有电子邮件地址,因为只要不为空值都能通过验证。)检查有效电子邮件地址的测试如<a class="xref-link" 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><code><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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">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="p">.</span><span class="nf">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="p">.</span><span class="nf">email</span> <span class="o">=</span> <span class="n">valid_address</span>
<span class="n">assert</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">valid?</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">valid_address</span><span class="p">.</span><span class="nf">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>
</code></pre></div>
</div>
<p>注意,我们为 <code>assert</code> 方法指定了可选的第二个参数,用于定制错误消息,识别是哪个地址导致测试失败的:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">assert</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">valid?</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">valid_address</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2"> should be valid"</span>
</code></pre></div>
</div>
<p>这行代码在字符串插值中使用了 <a class="xref-link" 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 class="xref-link" href="#listing-email-format-valid-tests">代码清单 6.18</a> 一样,<a class="xref-link" 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><code><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="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Example User"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">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="p">.</span><span class="nf">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="p">.</span><span class="nf">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="p">.</span><span class="nf">valid?</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">invalid_address</span><span class="p">.</span><span class="nf">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>
</code></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><code><span class="nv">$ </span>rails <span class="nb">test</span>
</code></pre></div>
</div>
<p>电子邮件地址格式验证使用 <code>format</code> 参数,用法如下:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code><span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">format: </span><span class="p">{</span> <span class="ss">with: </span><span class="sr">/<regular expression>/</span> <span class="p">}</span>
</code></pre></div>
</div>
<p>它使用指定的正则表达式(regular expression,简称 regex)验证属性。正则表达式很强大,使用模式匹配字符串,但往往晦涩难懂。我们要编写一个正则表达式,匹配有效的电子邮件地址,但不匹配无效的地址。</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><code><span class="no">VALID_EMAIL_REGEX</span> <span class="o">=</span> <span class="sr">/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i</span>
</code></pre></div>
</div>
<p>为了便于理解,我把 <code>VALID_EMAIL_REGEX</code> 拆分成几块来讲,如<a class="xref-link" href="#table-valid-email-regex">表 6.1</a> 所示。<sup>[<a id="fn-ref-12" href="#fn-12">12</a>]</sup></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 class="xref-link" href="#table-valid-email-regex">表 6.1</a> 中虽然能学到很多,但若想真正理解正则表达式,我觉得交互式正则表达式匹配工具,例如 <a href="http://www.rubular.com/" class="external-link">Rubular</a>(<a class="xref-link" href="#fig-rubular">图 6.7</a>)<sup>[<a id="fn-ref-13" href="#fn-13">13</a>]</sup>,是必不可少的。Rubular 的界面很友好,便于编写所需的正则表达式,而且还有一个便捷的语法速查表。我建议你使用 Rubular 来理解<a class="xref-link" href="#table-valid-email-regex">表 6.1</a>中的正则表达式——读得次数再多也不比不上在 Rubular 中实操几次。(注意:如果你在 Rubular 中输入<a class="xref-link" 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.7</span>:强大的 Rubular 正则表达式编辑器</div></div>
<p>在 <code>email</code> 属性的格式验证中使用这个正则表达式后得到的代码如<a class="xref-link" 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><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </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="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">255</span> <span class="p">},</span>
<span class="hll"> <span class="ss">format: </span><span class="p">{</span> <span class="ss">with: </span><span class="no">VALID_EMAIL_REGEX</span> <span class="p">}</span></span>
<span class="k">end</span>
</code></pre></div>
</div>
<p>其中,<code>VALID_EMAIL_REGEX</code> 是一个常量(constant)。在 Ruby 中常量的首字母为大写。下面这段代码:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><code> <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="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">255</span> <span class="p">},</span>
<span class="ss">format: </span><span class="p">{</span> <span class="ss">with: </span><span class="no">VALID_EMAIL_REGEX</span> <span class="p">}</span>
</code></pre></div>
</div>
<p>确保只有匹配正则表达式的电子邮件地址才是有效的。这个正则表达式有一个缺陷:能匹配 <code>[email protected]</code> 这种有连续点号的地址。修正这个瑕疵需要一个更复杂的正则表达式,留作<a class="xref-link" href="#exercises-format-validation">练习</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><code><span class="nv">$ </span>rails <span class="nb">test</span>:models
</code></pre></div>
</div>
<p>那么,现在就只剩一个限制要实现了:确保电子邮件地址的唯一性。</p>
<h5 id="exercises-format-validation" class="discrete">练习</h5>
<ol class="arabic">
<li>
<p>把<a class="xref-link" href="#listing-email-format-valid-tests">代码清单 6.18</a> 中的有效地址和<a class="xref-link" href="#listing-email-format-validation-tests">代码清单 6.19</a> 中的无效地址复制粘贴到 Rubular 的测试字符串文本框中,确认<a class="xref-link" href="#listing-validates-format-of-email">代码清单 6.21</a> 中的正则表达式能匹配全部有效地址,而且不能匹配任何无效地址。</p>
</li>
<li>
<p>前面说过,<a class="xref-link" href="#listing-validates-format-of-email">代码清单 6.21</a> 中的电子邮件地址正则表达式能匹配有连续点号的无效地址,例如 <em>[email protected]</em>。把这个地址添加到<a class="xref-link" href="#listing-email-format-validation-tests">代码清单 6.19</a> 中的无效地址列表中,让测试失败,然后使用<a class="xref-link" href="#listing-better-email-regex">代码清单 6.23</a> 中较复杂的正则表达式让测试通过。</p>
</li>
<li>
<p>把 <em>[email protected]</em> 添加到 Rubular 中的测试字符串文本框中,确认<a class="xref-link" href="#listing-better-email-regex">代码清单 6.23</a> 中的正则表达式能匹配全部有效地址,而且不能匹配任何无效地址。</p>
</li>
</ol>
<div id="listing-better-email-regex" data-type="listing">
<h5><span class="title-label">代码清单 6.23</span>:不允许电子邮件地址中有多个点号的正则表达式 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </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\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="kp">true</span><span class="p">,</span> <span class="ss">length: </span><span class="p">{</span> <span class="ss">maximum: </span><span class="mi">255</span> <span class="p">},</span>
<span class="ss">format: </span><span class="p">{</span> <span class="ss">with: </span><span class="no">VALID_EMAIL_REGEX</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div>
</div>
</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-14" href="#fn-14">14</a>]</sup>对重复电子邮件地址的测试如<a class="xref-link" href="#listing-validates-uniqueness-of-email-test">代码清单 6.24</a> 所示。</p>
<div id="listing-validates-uniqueness-of-email-test" data-type="listing">
<h5><span class="title-label">代码清单 6.24</span>:拒绝重复电子邮件地址的测试 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><code><span class="nb">require</span> <span class="s1">'test_helper'</span>