activerecord-embedding 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|