activerecord-embedding 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +84 -0
- data/Rakefile +9 -0
- data/activerecord-embedding.gemspec +26 -0
- data/lib/active_record/embedding.rb +155 -0
- data/lib/active_record/embedding/version.rb +6 -0
- data/lib/activerecord-embedding.rb +4 -0
- data/test/models/invoice.rb +10 -0
- data/test/models/item.rb +6 -0
- data/test/schema/schema.rb +14 -0
- data/test/test_main.rb +73 -0
- metadata +122 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# activerecord-embedding
|
2
|
+
|
3
|
+
Adds MongoDB style `embeds_many` to `ActiveRecord`.
|
4
|
+
|
5
|
+
## Example
|
6
|
+
|
7
|
+
```
|
8
|
+
class Invoice < ActiveRecord::Base
|
9
|
+
include ActiveRecord::Embedding
|
10
|
+
|
11
|
+
embeds_many :items
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
Now you can do all the magic `ActiveRecord` does not support natively.
|
16
|
+
|
17
|
+
```
|
18
|
+
@invoice = Invoice.new
|
19
|
+
@invoice.attributes = {items: [{description: "Some fancy ORM", value: 10.00},
|
20
|
+
{description: "When ActiveRecord does what you want", value: "priceless"}]}
|
21
|
+
```
|
22
|
+
|
23
|
+
You can also change your items and ActiveRecord will mark them for destruction and destroy them later.
|
24
|
+
|
25
|
+
```
|
26
|
+
# Imagine an Invoice with two items...
|
27
|
+
@invoice.find(1)
|
28
|
+
Items.count # => 2
|
29
|
+
@invoice.items.length # => 2
|
30
|
+
|
31
|
+
# When we change the items to zero...
|
32
|
+
@invoice.attributes = {items: []}
|
33
|
+
@invoice.items.length # => 0
|
34
|
+
Items.count # => 2 (because not saved yet)
|
35
|
+
|
36
|
+
# It will write the changes after we save them
|
37
|
+
@invoice.save!
|
38
|
+
@invoice.items.length # => 0
|
39
|
+
Items.count # => 0
|
40
|
+
```
|
41
|
+
|
42
|
+
Hopefully someday there will be native support for this in ActiveRecord.
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
Add the gem to your `Gemfile`
|
47
|
+
|
48
|
+
```
|
49
|
+
gem "activerecord-embedding"
|
50
|
+
```
|
51
|
+
|
52
|
+
Then use in your models.
|
53
|
+
```
|
54
|
+
class Invoice < ActiveRecord::Base
|
55
|
+
include ActiveRecord::Embedding
|
56
|
+
|
57
|
+
embeds_many :items
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
Remember to `include ActiveRecord::Embedding`!
|
62
|
+
|
63
|
+
## Development
|
64
|
+
|
65
|
+
There are some pretty evil hacks in the source. Feel free to fix them and send
|
66
|
+
me a pull request. Please comment your code and write tests. You can run the
|
67
|
+
test suite with `rake`. Please do not modify the `version.rb` file.
|
68
|
+
|
69
|
+
## Release
|
70
|
+
|
71
|
+
(Because my short time memory lasts for less than 23 ignoseconds, I need to write this down)
|
72
|
+
|
73
|
+
```
|
74
|
+
# Remember to run the tests and to bump the version
|
75
|
+
gem build activerecord-embedding.gemspec
|
76
|
+
gem push activerecord-embedding-$version.gem
|
77
|
+
```
|
78
|
+
|
79
|
+
## Credits
|
80
|
+
|
81
|
+
I must admit that this code is mostly based on a gist of netzpirat. Michael,
|
82
|
+
you did a great job! I just improved your code, added a few really really ugly
|
83
|
+
hacks to work around some strange ActiveRecord behavior and wrote some tests.
|
84
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/active_record/embedding/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "activerecord-embedding"
|
6
|
+
s.version = ActiveRecord::Embedding::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Markus Fenske"]
|
9
|
+
s.email = ["iblue@gmx.net"]
|
10
|
+
s.homepage = "http://github.com/iblue/activerecord-embedding"
|
11
|
+
s.summary = "Adds MongoMapper embeds_many style behavior to ActiveRecord"
|
12
|
+
s.description = "Adds MongoMapper embeds_many style behavior to ActiveRecord"
|
13
|
+
|
14
|
+
s.required_rubygems_version = ">= 1.3.6"
|
15
|
+
|
16
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
17
|
+
s.add_development_dependency "debugger"
|
18
|
+
s.add_development_dependency "sqlite3"
|
19
|
+
|
20
|
+
s.add_dependency "activerecord", "~> 3.0"
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
24
|
+
s.require_path = 'lib'
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# The code is based on this original source by Michael Kessler (netzpirat):
|
2
|
+
# Original source: https://gist.github.com/892426
|
3
|
+
module ActiveRecord
|
4
|
+
# Allows embedding of ActiveRecord models.
|
5
|
+
#
|
6
|
+
# Embedding other ActiveRecord models is a composition of the two
|
7
|
+
# and leads to the following behaviour:
|
8
|
+
#
|
9
|
+
# - Nested attributes are accepted on the parent without the _attributes suffix
|
10
|
+
# - Mass assignment security allows the embedded attributes
|
11
|
+
# - Embedded models are destroyed with the parent when not appearing in an update again
|
12
|
+
# - Embedded documents appears in the JSON output
|
13
|
+
# - Embedded documents that are deleted are not visible to the parent anymore, but
|
14
|
+
# will be deleted *after* save has been caled
|
15
|
+
#
|
16
|
+
# You have to manually include this module
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# class Invoice
|
20
|
+
# include ActiveRecord::Embedding
|
21
|
+
#
|
22
|
+
# embeds_many :items
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @author Michael Kessler
|
26
|
+
# modified by Markus Fenske <iblue@gmx.net>
|
27
|
+
#
|
28
|
+
module Embedding
|
29
|
+
extend ActiveSupport::Concern
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
mattr_accessor :embeddings
|
33
|
+
self.embeddings = []
|
34
|
+
|
35
|
+
# Embeds many ActiveRecord model
|
36
|
+
#
|
37
|
+
# @param models [Symbol] the name of the embedded models
|
38
|
+
# @param options [Hash] the embedding options
|
39
|
+
#
|
40
|
+
def embeds_many(models, options = { })
|
41
|
+
has_many models, options.merge(:dependent => :destroy, :autosave => true)
|
42
|
+
embed_attribute(models)
|
43
|
+
attr_accessible "#{models}_attributes".to_sym
|
44
|
+
|
45
|
+
# What is marked for destruction does not evist anymore from
|
46
|
+
# our point of view. FIXME: Really evil hack.
|
47
|
+
alias_method "_super_#{models}".to_sym, models
|
48
|
+
define_method models do
|
49
|
+
# This is an evil hack. Because activerecord uses the items method itself to
|
50
|
+
# find out which items are deleted, we need to act differently if called by
|
51
|
+
# ActiveRecord. So we look at the paths in the Backtrace. If there is
|
52
|
+
# activerecord-3 anywhere there, this is called by AR. This will work until
|
53
|
+
# AR 4.0...
|
54
|
+
if caller(0).select{|x| x =~ /activerecord-3/}.any?
|
55
|
+
return send("_super_#{models}".to_sym)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Otherwise, when we are called by someone else, we will not return the items
|
59
|
+
# marked for destruction.
|
60
|
+
send("_super_#{models}".to_sym).reject(&:marked_for_destruction?)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Embeds many ActiveRecord models which have been referenced
|
65
|
+
# with has_many.
|
66
|
+
#
|
67
|
+
# @param models [Symbol] the name of the embedded models
|
68
|
+
#
|
69
|
+
def embeds(models)
|
70
|
+
embed_attribute(models)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Makes the child model accessible by accepting nested attributes and
|
76
|
+
# makes the attributes accessible when mass assignment security is enabled.
|
77
|
+
#
|
78
|
+
# @param name [Symbol] the name of the embedded model
|
79
|
+
#
|
80
|
+
def embed_attribute(name)
|
81
|
+
accepts_nested_attributes_for name, :allow_destroy => true
|
82
|
+
attr_accessible "#{ name }_attributes".to_sym if _accessible_attributes?
|
83
|
+
self.embeddings << name
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Sets the attributes
|
88
|
+
#
|
89
|
+
# @param new_attributes [Hash] the new attributes
|
90
|
+
#
|
91
|
+
def attributes=(attrs)
|
92
|
+
return unless attrs.is_a?(Hash)
|
93
|
+
|
94
|
+
# Create a copy early so we do not overwrite the argument
|
95
|
+
new_attributes = attrs.dup
|
96
|
+
|
97
|
+
mark_for_destruction(new_attributes)
|
98
|
+
|
99
|
+
self.class.embeddings.each do |embed|
|
100
|
+
if new_attributes[embed]
|
101
|
+
new_attributes["#{embed}_attributes"] = new_attributes[embed]
|
102
|
+
new_attributes.delete(embed)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
super(new_attributes)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Update attributes and destroys missing embeds
|
110
|
+
# from the database.
|
111
|
+
#
|
112
|
+
# @params attributes [Hash] the attributes to update
|
113
|
+
#
|
114
|
+
def update_attributes(attributes)
|
115
|
+
super(mark_for_destruction(attributes))
|
116
|
+
end
|
117
|
+
|
118
|
+
# Update attributes and destroys missing embeds
|
119
|
+
# from the database.
|
120
|
+
#
|
121
|
+
# @params attributes [Hash] the attributes to update
|
122
|
+
#
|
123
|
+
def update_attributes!(attributes)
|
124
|
+
super(mark_for_destruction(attributes))
|
125
|
+
end
|
126
|
+
|
127
|
+
# Add the embedded document in JSON serialization
|
128
|
+
#
|
129
|
+
# @param options [Hash] the rendering options
|
130
|
+
#
|
131
|
+
def as_json(options = { })
|
132
|
+
super({ :include => self.class.embeddings }.merge(options || { }))
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# Marks missing models as deleted. Writes the changes to the database,
|
138
|
+
# after save has been called.
|
139
|
+
#
|
140
|
+
# @param attributes [Hash] the attributes
|
141
|
+
#
|
142
|
+
def mark_for_destruction(attributes)
|
143
|
+
self.class.embeddings.each do |embed|
|
144
|
+
if attributes[embed]
|
145
|
+
updates = attributes[embed].map { |model| model[:id] }.compact
|
146
|
+
destroy = updates.empty? ? send("_super_#{embed}".to_sym).select(:id) : send("_super_#{embed}".to_sym).select(:id).where('id NOT IN (?)', updates)
|
147
|
+
destroy.each { |model| attributes[embed] << { :id => model.id, :_destroy => '1' } }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
attributes
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
data/test/models/item.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
ActiveRecord::Schema.define do
|
3
|
+
create_table "invoices", :force => true do |t|
|
4
|
+
t.string "recipient_email"
|
5
|
+
end
|
6
|
+
|
7
|
+
create_table "items", :force => true do |t|
|
8
|
+
t.integer "invoice_id"
|
9
|
+
t.integer "amount"
|
10
|
+
t.string "description"
|
11
|
+
t.float "value"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
data/test/test_main.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require 'debugger'
|
6
|
+
|
7
|
+
require 'test/unit'
|
8
|
+
require 'active_record'
|
9
|
+
require 'active_record/base'
|
10
|
+
require 'logger'
|
11
|
+
|
12
|
+
require 'activerecord-embedding'
|
13
|
+
|
14
|
+
require 'models/invoice'
|
15
|
+
require 'models/item'
|
16
|
+
|
17
|
+
ActiveRecord::Base.establish_connection(
|
18
|
+
:adapter => "sqlite3",
|
19
|
+
:database => "/tmp/test.db"
|
20
|
+
)
|
21
|
+
load "schema/schema.rb"
|
22
|
+
|
23
|
+
|
24
|
+
# Make sure we see whats happening
|
25
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
26
|
+
|
27
|
+
class MainTest < Test::Unit::TestCase
|
28
|
+
def test_embedding
|
29
|
+
# Build a new invoice, do not save anything, exists in memory only
|
30
|
+
puts "--- Should not issue a query"
|
31
|
+
@invoice = Invoice.new
|
32
|
+
@invoice.attributes = {items: [{amount: 1, description: "Item 1", value: 10.00}, {amount: 2, description: "Item 2", value: 8.00}]}
|
33
|
+
assert_equal 0, Invoice.count
|
34
|
+
assert_equal 0, Item.count
|
35
|
+
assert_equal 26.00, @invoice.total
|
36
|
+
puts " end"
|
37
|
+
|
38
|
+
# Create everything, make sure it gets saved to the database
|
39
|
+
puts "--- Should INSERT 1 invoice and 2 items"
|
40
|
+
@invoice.save!
|
41
|
+
assert_equal 1, Invoice.count
|
42
|
+
assert_equal 2, Item.count
|
43
|
+
assert_equal ["Item 1", "Item 2"], Item.all.map(&:description)
|
44
|
+
assert_equal 26.00, @invoice.total
|
45
|
+
puts " end"
|
46
|
+
|
47
|
+
# Change attributes, make sure it exists in memory only and does not get
|
48
|
+
# saved to the database
|
49
|
+
puts "--- Should not issue a query"
|
50
|
+
@invoice.attributes = {items: [{amount: 1, description: "Item 3", value: 10.00}]}
|
51
|
+
assert_equal 1, Invoice.count
|
52
|
+
assert_equal 2, Item.count
|
53
|
+
assert_equal ["Item 1", "Item 2"], Item.all.map(&:description)
|
54
|
+
assert_equal 10.00, @invoice.total # But the total value in memory should change
|
55
|
+
puts " end"
|
56
|
+
|
57
|
+
# Now save changes to database.
|
58
|
+
puts "--- Should DELETE 2 items, INSERT 1 item"
|
59
|
+
@invoice.save!
|
60
|
+
assert_equal 1, Invoice.count
|
61
|
+
assert_equal 1, Item.count
|
62
|
+
assert_equal ["Item 3"], Item.all.map(&:description)
|
63
|
+
assert_equal 10.00, @invoice.total # Total value in memory should stay the same
|
64
|
+
puts " end"
|
65
|
+
|
66
|
+
# Delete everything, make sure it's gone.
|
67
|
+
puts "--- Should DELETE 1 item, DELETE 1 invoice"
|
68
|
+
@invoice.destroy
|
69
|
+
assert_equal 0, Invoice.count
|
70
|
+
assert_equal 0, Item.count
|
71
|
+
puts " end"
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-embedding
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Markus Fenske
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: debugger
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: sqlite3
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activerecord
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.0'
|
78
|
+
description: Adds MongoMapper embeds_many style behavior to ActiveRecord
|
79
|
+
email:
|
80
|
+
- iblue@gmx.net
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- Gemfile
|
87
|
+
- README.md
|
88
|
+
- Rakefile
|
89
|
+
- activerecord-embedding.gemspec
|
90
|
+
- lib/active_record/embedding.rb
|
91
|
+
- lib/active_record/embedding/version.rb
|
92
|
+
- lib/activerecord-embedding.rb
|
93
|
+
- test/models/invoice.rb
|
94
|
+
- test/models/item.rb
|
95
|
+
- test/schema/schema.rb
|
96
|
+
- test/test_main.rb
|
97
|
+
homepage: http://github.com/iblue/activerecord-embedding
|
98
|
+
licenses: []
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ! '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 1.3.6
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 1.8.24
|
118
|
+
signing_key:
|
119
|
+
specification_version: 3
|
120
|
+
summary: Adds MongoMapper embeds_many style behavior to ActiveRecord
|
121
|
+
test_files: []
|
122
|
+
has_rdoc:
|