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 +4 -0
- data/README.markdown +15 -23
- data/Rakefile +1 -1
- data/lib/piggyback.rb +32 -26
- data/piggyback.gemspec +2 -2
- data/spec/piggyback_spec.rb +30 -4
- metadata +2 -2
data/CHANGELOG
CHANGED
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.
|
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
|
-
# => "
|
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.
|
34
|
-
|
35
|
-
|
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.
|
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
|
-
# =>
|
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
|
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).
|
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.
|
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 :
|
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
|
18
|
-
@
|
17
|
+
def table
|
18
|
+
@table ||= @reflection.active_record.arel_table
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
22
|
-
@
|
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].
|
113
|
+
piggyback_associations[assoc_name].mappings.keys
|
98
114
|
end
|
99
115
|
|
100
|
-
def piggyback(*
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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.
|
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-
|
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"]
|
data/spec/piggyback_spec.rb
CHANGED
@@ -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
|
-
|
33
|
-
Essential.create(:token => "
|
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.
|
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-
|
13
|
+
date: 2011-03-24 00:00:00 +01:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|