new wave of database programming with ruby 1.9 on rails 2.1

Post on 28-May-2015

4.751 Views

Category:

Technology

3 Downloads

Preview:

Click to see full reader

DESCRIPTION

LT Given at RubyKaigi 2008, describing the history of DB programming style, AR, named_scope and named_scope in Ruby 1.9 syntax.

TRANSCRIPT

New Wave of Database Programming with

Ruby 1.9 on Rails 2.1

松田 明 @ RubyKaigi 2008 LT

1

begin

2

自己紹介:名前 => 松田 明

:職業 => フリーランスの Ruby/Rails プログラマ

:仕事 => Rails

:趣味 => Rails

:仕事で書いているもの => 業務アプリが多い

3

業務アプリと言えば、DB。

4

人類のDBプログラミングの進化の歴史を振り返る

5

WEB + DBプログラミングにおけるパラダイムの遷移

古代 → 近代 → 現代

2回ぐらいの大きなパラダイムの転換

6

history

古代 → 近代 → 現代

7

古代言語e.g.

P H P8

:logic => 生SQLを文字列で 組み立てて発行

:view => 結果セットをぐるぐるループして HTML文字列を生成。

9

サンプルコード

10

省略。11

古代言語の衰退に伴って 絶滅。

12

history

古代 → 近代 → 現代

13

大きなパラダイムの転換

14

DBフレームワークの登場

• DBアクセス手段の共通化

• 設定の一元管理/コネクションのハンドリング

• RDBMS間の方言を吸収

• いわゆる O/Rマッパー

15

O/Rマッパーがもたらしたもの

RDBMSと

オブジェクト指向の

幸福な出会い

16

人間らしいDBプログラミング

17

サンプルコード

18

よくあるO/Rマッパーを使ったちょっとした業務アプリ

public interface ProductDAO { public Product find_by_id(int id); public Product find_by_name(String name); public List<Product> find_by_maker(Maker maker); public List<Product> find_by_category(Category category); public List<Product> find_by_maker_and_category(Maker maker, Category category);}

19

よくあるO/Rマッパーを使ったちょっとした業務アプリ

public interface ProductDAO { public Product find_by_id(int id); public Product find_by_name(String name); public List<Product> find_by_maker(Maker maker); public List<Product> find_by_category(Category category); public List<Product> find_by_maker_and_category(Maker maker, Category category);}

Remote InterfaceImpl

EntityBeanmock Home Interface

Serializable

CORBA

RMISetter

Getter

Compile

Deploy

DBUnit

19

よくあるO/Rマッパーを使ったちょっとした業務アプリ

public interface ProductDAO { public Product find_by_id(int id); public Product find_by_name(String name); public List<Product> find_by_maker(Maker maker); public List<Product> find_by_category(Category category); public List<Product> find_by_maker_and_category(Maker maker, Category category);}

Remote InterfaceImpl

EntityBeanDAO

Annotation

Dependency Injection

mock

DTODXO

Home Interface

Lazy LoadingOpen Session in View

Serializable

CORBA

RMISetter

Getter

Compile

Deploy

DBUnit

Bytecode Enhancement

19

よくあるO/Rマッパーを使ったちょっとした業務アプリ

public interface ProductDAO { public Product find_by_id(int id); public Product find_by_name(String name); public List<Product> find_by_maker(Maker maker); public List<Product> find_by_category(Category category); public List<Product> find_by_maker_and_category(Maker maker, Category category);}

Remote InterfaceImpl

EntityBeanDAO

Annotation

Dependency Injection

mock

DTODXO

Home Interface

Lazy LoadingOpen Session in View

Serializable

CORBA

RMISetter

Getter

Compile

Deploy

流れるようなインターフェース(笑)

DBUnit

Bytecode Enhancement

19

よくあるO/Rマッパーを使ったちょっとした業務アプリ

public interface ProductDAO { public Product find_by_id(int id); public Product find_by_name(String name); public List<Product> find_by_maker(Maker maker); public List<Product> find_by_category(Category category); public List<Product> find_by_maker_and_category(Maker maker, Category category);}

Remote InterfaceImpl

EntityBeanDAO

Annotation

Dependency Injection

mock

DTODXO

Home Interface

Lazy LoadingOpen Session in View

Serializable

CORBA

RMISetter

Getter

Compile

Deploy

流れるようなインターフェース(笑)

DBUnit

Bytecode Enhancement

XML

XML

XML

XMLXML

XML

XMLXML

XMLXML

XMLXML

XML

XMLXMLXML

XML

XML

XML

XML

19

カオス(笑)

20

人々がまだJavaを喋っていた時代の懐かしいお話

21

history

古代 → 近代 → 現代

22

次のパラダイム転換のきっかけ

23

我らが ActiveRecordの登場

24

•CoC をフル活用。「XML hell」と決別。

• modelクラスのアクセッサメソッドや単純な検索メソッドは DBのスキーマ定義から動的に生成。

•REST思想と見事な統合を遂げる。model == リソース。

• 柔軟なプラグイン機構

特徴

25

• 言語内DSLを生かした極めて可読性の高いバリデーション

• マイグレーションも Rubyスクリプトで。

• YAML形式でさくさくテストデータを記述。テストは専用DBで。

• フィルタのようなイメージで条件を重ね合わせていくwith_scope という機能=> 「Activerecord を詳しく」by 舞波氏 参照

機能

26

現代人のニーズとセンスに完璧にマッチした、変化に強い、エレガントなフレームワーク

27

DB層フレームワークの決定版

28

scaffoldで作る CRUDアプリ• 一覧

@models = Model.find(:all)

• 詳細@model = Model.find(params[:id])a

• 登録@model = Model.new(params[:model])@model.save

• 更新@model = Model.find(params[:id])@model.update_attributes(params[:model]

• 削除@model.destroy

29

コマンド一発で自動生成

30

誰でも書ける ActiveRecord

31

みんなの ActiveRecord

32

ご清聴ありが(ry

33

「15分で作るブログ」みたいなアプリ

ならね。

34

user_values = [] user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? if project user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } else # members of the user's projects user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } end @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?

if project # project specific filters @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } unless @project.active_children.empty? @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } end @project.all_custom_fields.select(&:is_filter?).each do |field| case field.field_format when "text" options = { :type => :text, :order => 20 } when "list" options = { :type => :list_optional, :values => field.possible_values, :order => 20} when "date" options = { :type => :date, :order => 20 } when "bool" options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } else options = { :type => :string, :order => 20 } end @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) end # remove category filter if no category defined @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? end

でも実際の業務アプリになると…

35

やっぱりカオス・・・

36

実は DBアクセスは Railsの弱点

• 結局、SQL文字列を組み立てる処理を常に意識

•CoCではどうにもならない泥臭いところ

• 複雑になればなるほど肥大化

37

• 「SQLの文字列を組み立てる」

という昔ながらのダサい処理

•Ruby的オブジェクト指向

というハイセンスで華やかな世界観

この問題の根本原因

38

• 「SQLの文字列を組み立てる」

という昔ながらのダサい処理

•Ruby的オブジェクト指向

というハイセンスで華やかな世界観

この問題の根本原因

大きなギャップ

38

RDBMS操作 == SQL文を組み立てて発行することという先入観から脱却できていないから。

why?

39

RDBMS操作 != SQL文を組み立てて発行すること

actually,

40

DB操作の本質は集合演算。

41

そこで、

42

history

古代 → 近代 → 現代 → 2008年~

43

named_scope

44

named_scope とは?

• もともとは 昨秋ごろに彗星のように登場した has_finderという名前の野良 Railsプラグイン

• 今年の3月ごろ、Rails 2.1系 から正式に

Rails本体に取り込まれた

45

named_scopeの機能• 問い合わせを固まりではなく断片でとらえる

「scope」

• 「scope」に名前を付けて、DSL的にあらかじめ定義

• 複雑な条件クエリの組み立ては、定義済みの「scope」を組み合わせて動的に作ることで表現できる

46

example• modelnamed_scope :of_the_month, :conditions => {:reported_at => Date.today.beginning_of_month..Date.today.end_of_month}

• 呼び出し側@reports = Report.of_the_month

• 発行されるSQLSELECT * FROM "reports" WHERE ("reports"."reported_at" BETWEEN '2008-06-01' AND '2008-06-30')

47

『DAO』と何が違うの?

48

example(2)• modelnamed_scope :of_the_month, :conditions => {:reported_at => Date.today.beginning_of_month..Date.today.end_of_month} named_scope :written_by, Proc.new {|u| {:conditions => {:user_id => u}}}

• 呼び出し側@reports = Report.of_the_month.written_by(@current_user)

• 発行されるSQLSELECT * FROM "reports" WHERE (("reports"."user_id" = 1) AND ("reports"."reported_at" BETWEEN '2008-06-01' AND '2008-06-30'))

49

カスケードして呼び出してもSQLは1回

50

可読性高すぎ。

51

イメージ図

52

イメージ図of_the_month

52

イメージ図of_the_month written_by

52

イメージ図of_the_month written_by

これを select。

52

まさに集合演算。

53

集合がDSLで!

54

named_scopeがもたらしたものRDBMS

オブジェクト指向と

集合演算の

幸福な出会い55

これを実現している技術

• scopeの実体は Procのインスタンス

• チェインされた Procたちをまとめて評価 -> 実行

• Rubyの豊かな表現力を生かした、Rubyならではの実装

56

単体テストも楽々(RSpecの場合)• scopeそのものの条件テスト

describe 'written_by' do it 'Userのインスタンスを受け取って user_id で検索を実行すること' do Report.written_by(@user).proxy_options.should == {:conditions => {:user_id => 1}} end

• ロジック側から然るべきscopeが正しく呼ばれていることのテスト it ‘named_scope :written_by を「ログインユーザ」を引数にして呼び出していること’ do @written_by_scope.should_receive(:call).with(Report, users(:ito)).and_return(Report) do_get end

57

Rails 2.2に向けてますます機能拡張中!

• !付きメソッド

• 検索以外でも使えるように

• 詳細は

Rails勉強会@東京ブースにて配布中のペーパー参照。

58

Life Changing!

59

まとめ

60

RubyプログラマはRubyでものを考える

61

named_scopeは、泥臭い DB操作をキレイな Ruby語に翻訳するデバイス

62

でも、キレイな Ruby語とか言うわりには、

• デフォルト引数を与えたい場合、こう書きたいけど書けないnamed_scope :recent, Proc.new {|time || 2.weeks.ago| {:conditions => ['reported_at > ?', time]} }

• ので、こんなふうにでも書いて逃げるしか。named_scope :recent, Proc.new {|*args| {:conditions => ['reported_at > ?', (args.first || 2.weeks.ago)]} }

63

これはちょっとイケてないかも?

64

そこで、Ruby 1.9。

65

Ruby 1.9なら

•新しい lambda記法 -> ができた

•procにデフォルト引数がとれるようになった

67

Ruby 1.9 なら• さっきのこれが、

named_scope :recent, Proc.new {|*args| {:conditions => ['reported_at > ?', (args.first || 2.weeks.ago)]} }

• こうなる。named_scope :recent, ->(time = 2.weeks.ago) { {:conditions => ["reported_at > ?", time]} }

68

Ruby 1.9 is for you, Railers!

69

end

70

続きは WEBで!http://blog.dio.jp

71

top related