piggyback 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ = 0.2.4 / 2011-03-24
2
+
3
+ * Changed join type from INNER to OUTER
4
+
1
5
  = 0.2.3 / 2011-03-21
2
6
 
3
7
  * Updated documentation.
data/README.markdown CHANGED
@@ -18,25 +18,23 @@ This is best illustrated in an example. Consider these models:
18
18
 
19
19
  ActiveRecord supports piggybacking simply by joining the associated table and selecting columns from it:
20
20
 
21
- post = Post.joins(:author).select('posts.*, author.name AS author_name').first
21
+ post = Post.select('posts.*, author.name AS author_name') \
22
+ .joins("JOIN authors ON posts.author_id = authors.id") \
23
+ .first
22
24
 
23
- post.title
24
- # => "Why piggybacking in ActiveRecord is flawed"
25
-
26
- post.author_name
27
- # => "Alec Smart"
25
+ post.title # => "Why piggybacking in ActiveRecord is flawed"
26
+ post.author_name # => "Alec Smart"
28
27
 
29
28
  As you can see, the `name` attribute from `Author` is treated as if it were an attribute of `Post`. ActiveRecord dynamically determines a model's attributes from the result set returned by the database. Every column in the result set becomes an attribute of the instantiated ActiveRecord objects. Whether the columns originate from the model's own or from a foreign table doesn't make a difference.
30
29
 
31
30
  Or so it seems. Actually there is a drawback which becomes obvious when we select non-string columns:
32
31
 
33
- post = Post.joins(:author).select('posts.*, author.birthday AS author_birthday, author.rating AS author_rating').first
34
-
35
- post.author_birthday
36
- # => "2011-03-01"
32
+ post = Post.select('posts.*, author.birthday AS author_birthday, author.rating AS author_rating') \
33
+ .joins("JOIN authors ON posts.author_id = authors.id") \
34
+ .first
37
35
 
38
- post.author_rating
39
- # => "4.5"
36
+ post.author_birthday # => "2011-03-01"
37
+ post.author_rating # => "4.5"
40
38
 
41
39
  Any attributes originating from the `authors` table are treated as strings instead of being automatically type-casted as we would expect. The database returns result sets as plain text and ActiveRecord needs to obtain type information separately from the table schema in order to do its type-casting magic. Unfortunately, a model only knows about the columns types in its own table, so type-casting doesn't work with columns selected from foreign tables.
42
40
 
@@ -65,7 +63,6 @@ You simply declare which association you want to piggyback and how the attribute
65
63
 
66
64
  class Post < ActiveRecord::Base
67
65
  belongs_to :author
68
-
69
66
  piggybacks :author, :birthday => :author_birthday, :rating => :author_rating
70
67
  end
71
68
 
@@ -73,19 +70,16 @@ Now you can do the following:
73
70
 
74
71
  post = Post.piggyback(:author).first
75
72
 
76
- post.author_birthday
77
- # => Tue, 01 Mar 2011
78
-
79
- post.author_rating
80
- # => 4.5
73
+ post.author_birthday # => Tue, 01 Mar 2011
74
+ post.author_rating # => 4.5
81
75
 
82
76
  The type-casting works with any type of attribute, even with serialized ones.
83
77
 
84
- As you can see, the `piggibacks` statement replaces the `joins` and `select` parts of the query. Using it is optional but makes life easier since you don't have to write the SQL for `select` by hand.
78
+ As you can see, the `piggibacks` statement replaces the `joins` and `select` parts of the query. Using it is optional but makes life easier. In certain situations however, you may want to select columns by hand or use another kind of join for related table. By default `piggibacks` uses `OUTER JOIN` in order to include both records that have an associated record and ones that don't.
85
79
 
86
80
  Of course, `piggibacks` plays nice with Arel and you can add additional `joins`, `select` and other statements as you like, for example:
87
81
 
88
- Post.select('posts.title, posts.body').piggibacks(:author).joins(:comments).where(:published => true)
82
+ Post.select('posts.title, posts.body').piggibacks(:author).where(:published => true)
89
83
 
90
84
  __Please note:__ If you want to restrict the columns selected from the master table as in the example above, you have to do so _before_ the `piggibacks` statement. Otherwise it will insert the select-all wildcard `SELECT posts.*` rendering your column selection useless.
91
85
 
@@ -100,11 +94,9 @@ If you want to use an SQL-expression for selecting an attribute, Piggyback can a
100
94
 
101
95
  class Post < ActiveRecord::Base
102
96
  belongs_to :author
103
-
104
97
  piggybacks :author, :author_name => "authors.first_name || ' ' || authors.last_name"
105
98
  end
106
99
 
107
- post.author_name
108
- # => "Donald Duck"
100
+ post.author_name # => "Donald Duck"
109
101
 
110
102
  In fact, every value you pass in as a string will be treated as raw SQL in the SELECT clause.
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'rake'
2
2
  require 'echoe'
3
3
 
4
- Echoe.new('piggyback', '0.2.3') do |p|
4
+ Echoe.new('piggyback', '0.2.4') do |p|
5
5
  p.description = "Piggyback attributes from associated models with ActiveRecord"
6
6
  p.url = "http://github.com/juni0r/piggyback"
7
7
  p.author = "Andreas Korth"
data/lib/piggyback.rb CHANGED
@@ -3,7 +3,7 @@ module Piggyback
3
3
 
4
4
  class Association #:nodoc:
5
5
 
6
- attr_reader :reflection, :mappings
6
+ attr_reader :mappings
7
7
 
8
8
  def initialize(reflection, mappings = {})
9
9
  @reflection = reflection
@@ -14,12 +14,32 @@ module Piggyback
14
14
  @mappings.merge!(mappings)
15
15
  end
16
16
 
17
- def map(&block)
18
- @mappings.map(&block)
17
+ def table
18
+ @table ||= @reflection.active_record.arel_table
19
19
  end
20
20
 
21
- def attributes
22
- @mappings.keys
21
+ def foreign_table
22
+ @foreign_table ||= @reflection.klass.arel_table
23
+ end
24
+
25
+ def select
26
+ @mappings.map do |attr_name, mapped_name|
27
+ if attr_name == mapped_name
28
+ foreign_table[attr_name]
29
+ elsif mapped_name.is_a? Arel::Nodes::SqlLiteral
30
+ mapped_name.as(attr_name)
31
+ else
32
+ foreign_table[mapped_name].as(attr_name)
33
+ end
34
+ end
35
+ end
36
+
37
+ def joins
38
+ if @reflection.belongs_to?
39
+ table.join(foreign_table, Arel::Nodes::OuterJoin).on(foreign_table.primary_key.eq(table[@reflection.primary_key_name]))
40
+ else
41
+ table.join(foreign_table, Arel::Nodes::OuterJoin).on(foreign_table[@reflection.primary_key_name].eq(table.primary_key))
42
+ end.join_sql
23
43
  end
24
44
 
25
45
  def has_attribute?(attr_name)
@@ -29,14 +49,10 @@ module Piggyback
29
49
  def column(attr_name)
30
50
  @reflection.klass.columns_hash[@mappings[attr_name]]
31
51
  end
32
-
52
+
33
53
  def serialized_attribute(attr_name)
34
54
  @reflection.klass.serialized_attributes[@mappings[attr_name]]
35
55
  end
36
-
37
- def quoted_table_name
38
- @reflection.klass.quoted_table_name
39
- end
40
56
  end
41
57
 
42
58
  module ClassMethods
@@ -94,25 +110,15 @@ module Piggyback
94
110
  end
95
111
 
96
112
  def piggyback_attributes(assoc_name)
97
- piggyback_associations[assoc_name].attributes
113
+ piggyback_associations[assoc_name].mappings.keys
98
114
  end
99
115
 
100
- def piggyback(*assoc_names)
101
- columns = piggyback_associations.values_at(*assoc_names).map do |association|
102
- association.map do |attr_name, mapped_name|
103
- if attr_name == mapped_name
104
- "#{association.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
105
- else
106
- if mapped_name.is_a? Arel::Nodes::SqlLiteral
107
- "#{mapped_name} AS #{attr_name}"
108
- else
109
- "#{association.quoted_table_name}.#{connection.quote_column_name(mapped_name)} AS #{attr_name}"
110
- end
111
- end
112
- end
116
+ def piggyback(*assocs)
117
+ context = scoped
118
+ context = context.select("#{quoted_table_name}.*") unless context.select_values.any?
119
+ piggyback_associations.values_at(*assocs).inject(context) do |ctx, assoc|
120
+ ctx.select(assoc.select).joins(assoc.joins)
113
121
  end
114
- columns.unshift("#{quoted_table_name}.*") unless scoped.select_values.any?
115
- select(columns.flatten.join(', ')).joins(*assoc_names)
116
122
  end
117
123
  end
118
124
  end
data/piggyback.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{piggyback}
5
- s.version = "0.2.3"
5
+ s.version = "0.2.4"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Andreas Korth"]
9
- s.date = %q{2011-03-21}
9
+ s.date = %q{2011-03-24}
10
10
  s.description = %q{Piggyback attributes from associated models with ActiveRecord}
11
11
  s.email = %q{andreas.korth@gmail.com}
12
12
  s.extra_rdoc_files = ["CHANGELOG", "README.markdown", "lib/piggyback.rb"]
@@ -26,21 +26,28 @@ describe Piggyback do
26
26
  end
27
27
  end
28
28
  end
29
-
29
+
30
30
  context "relation" do
31
31
 
32
- let :essential do
33
- Essential.create(:token => "TOKEN").create_detail(
32
+ before do
33
+ Essential.create!(:token => "ESSENTIAL").create_detail(
34
34
  :first_name => "John",
35
35
  :last_name => "Doe",
36
36
  :count => 23,
37
37
  :checked => false,
38
38
  :birthday => "18.12.1974",
39
39
  :baggage => {:key => "value"})
40
-
40
+ end
41
+
42
+ let :essential do
41
43
  Essential.piggyback(:detail).first
42
44
  end
43
45
 
46
+ it "should include records without an association" do
47
+ Essential.create!(:token => "WITHOUT_DETAIL")
48
+ Essential.piggyback(:detail).should have(2).items
49
+ end
50
+
44
51
  it "should not load the association" do
45
52
  essential.should_not_receive(:detail)
46
53
  essential.first_name.should be_present
@@ -73,6 +80,25 @@ describe Piggyback do
73
80
  it "should read computed attributes" do
74
81
  essential.name.should eql("John Doe")
75
82
  end
83
+
84
+ it "should work with belongs_to associations" do
85
+ Detail.piggyback(:essential).first.token.should eql("ESSENTIAL")
86
+ Detail.create!
87
+ Detail.piggyback(:essential).should have(2).items
88
+ end
89
+ end
90
+
91
+ context "scope" do
92
+
93
+ it "should SELECT * if no columns were selected before" do
94
+ Essential.piggyback(:detail).to_sql.should match(/^SELECT "essentials"\.\*,/)
95
+ end
96
+
97
+ it "should not SELECT * if columns were selected before" do
98
+ sql = Essential.select(:id).piggyback(:detail).to_sql
99
+ sql.should match(/^SELECT id,/)
100
+ sql.should_not match(/"essentials"\.\*/)
101
+ end
76
102
  end
77
103
 
78
104
  it "should return the column names for a given association name" do
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: piggyback
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.3
5
+ version: 0.2.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Andreas Korth
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-03-21 00:00:00 +01:00
13
+ date: 2011-03-24 00:00:00 +01:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency