mudnaes-dbstruct 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +56 -0
- data/dbstruct.gemspec +21 -0
- data/lib/dbstruct.rb +204 -0
- data/spec/dbstruct_spec.rb +143 -0
- metadata +82 -0
data/README
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
This is a very simple framework to be able to access database rows as
|
2
|
+
objects without forcing you to inherit from a Model class. Instead
|
3
|
+
you create the class-hierarchy you want and use mixin to add
|
4
|
+
persistance functionality to the object.
|
5
|
+
|
6
|
+
It a lot simpler than an real Object-Relation-Mapper since it's ignores relations.
|
7
|
+
|
8
|
+
The benefit of this framework is that you can write code like this:
|
9
|
+
|
10
|
+
class AnObject
|
11
|
+
include DBStruct
|
12
|
+
@non_persisted_field
|
13
|
+
attr_accessor :non_persisted_field
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
@non_persisted_field = args[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
def return_value_from_field
|
20
|
+
return @non_persisted_field
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Create a simple migration:
|
25
|
+
|
26
|
+
class CreateDb < Sequel::Migration
|
27
|
+
def up
|
28
|
+
create_table :person do
|
29
|
+
primary_key :id
|
30
|
+
text :name
|
31
|
+
float :amount
|
32
|
+
integer :age
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
An then start coding. To create new field you only have to add it to the migration.
|
38
|
+
|
39
|
+
DB = Sequel.sqlite '', :logger => [Logger.new($stdout)]
|
40
|
+
CreateDb.apply(DB,:up)
|
41
|
+
AnObject.bind_table(DB,:person)
|
42
|
+
|
43
|
+
r = AnObject.template
|
44
|
+
r.name = 'Donald'
|
45
|
+
r.amount = 10.1
|
46
|
+
r.age = 77
|
47
|
+
|
48
|
+
r.insert(DB)
|
49
|
+
|
50
|
+
r.age = 98
|
51
|
+
r.update(DB)
|
52
|
+
r.delete(DB)
|
53
|
+
|
54
|
+
This framework relies heavely on the work of others (Sequel and OpenStruct)
|
55
|
+
|
56
|
+
Morten Udnæs.
|
data/dbstruct.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "dbstruct"
|
3
|
+
s.version = "0.1.1"
|
4
|
+
s.date = "2008-12-03"
|
5
|
+
s.summary = "Easy to use library for mapping rows between objects and database"
|
6
|
+
s.email = "mudnaes@gmail.com"
|
7
|
+
s.homepage = "http://github.com/mudnaes/dbstruct/tree/master"
|
8
|
+
s.description = "This is a very simple framework to be able to access database rows as objects without forcing you to inherit from a Model class. Instead you create the class-hierarchy you want and use mixin to add persistance functionality to the object."
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ["Morten Udnæs"]
|
11
|
+
s.files = ["README",
|
12
|
+
"dbstruct.gemspec",
|
13
|
+
"lib/dbstruct.rb"]
|
14
|
+
s.test_files = ["spec/dbstruct_spec.rb"]
|
15
|
+
s.rdoc_options = ["--main", "README.txt"]
|
16
|
+
s.extra_rdoc_files = ["README"]
|
17
|
+
s.add_dependency("diff-lcs", ["> 0.0.0"])
|
18
|
+
s.add_dependency("mime-types", ["> 0.0.0"])
|
19
|
+
s.add_dependency("open4", ["> 0.0.0"])
|
20
|
+
end
|
21
|
+
|
data/lib/dbstruct.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module DBStruct
|
5
|
+
# The purpose of this module is to have a struct inside an ordinary object
|
6
|
+
# to contain feilds for persistence. The fields are accessed the same way
|
7
|
+
# as the other fields inside the object..
|
8
|
+
#
|
9
|
+
|
10
|
+
|
11
|
+
# Define dynamic accessors to struct fields
|
12
|
+
def method_missing(mid, *args)
|
13
|
+
fieldname = mid.id2name
|
14
|
+
|
15
|
+
if mid.id2name.match("=")
|
16
|
+
fieldname.chop!
|
17
|
+
if args.length != 1
|
18
|
+
raise ArgumentError, "Setter must have only one argument. Number of arguments found was (#{len} for 1)", caller(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Make sure fields aren't added in error (typos)
|
22
|
+
if !@_dbdata.marshal_dump.key?(fieldname.to_sym) & @_frozen
|
23
|
+
raise ArgumentError, "Struct fields are frozen, reopen before using <field>= . Field was (#{fieldname} for 1)", caller(1)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if @_dbdata == nil
|
28
|
+
super(mid, *args)
|
29
|
+
|
30
|
+
elsif @_dbdata.marshal_dump.key?(fieldname.to_sym)
|
31
|
+
@_dbdata.send(mid.id2name,args[0])
|
32
|
+
|
33
|
+
# Route method missing to struct object
|
34
|
+
elsif !@_frozen & mid.id2name.match("=")
|
35
|
+
@_dbdata.method_missing(mid, *args)
|
36
|
+
|
37
|
+
# Forward methods missing not related to struct
|
38
|
+
else
|
39
|
+
super(mid, *args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Used for creating an object based on an hash (primarily for loading records directly from DB
|
44
|
+
def load(response)
|
45
|
+
if response != nil
|
46
|
+
@_dbdata = OpenStruct.new(response)
|
47
|
+
@_frozen = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Export hash from struct
|
52
|
+
def save(*args)
|
53
|
+
@_dbdata.marshal_dump(*args)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(db, pkid)
|
57
|
+
@_dbdata.pkid = db[self.class.get_table].filter(:pkid =>pkid)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def insert(db)
|
62
|
+
@_dbdata.pkid = db[self.class.get_table].insert(save)
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete(db,search_criteria)
|
66
|
+
db[self.class.get_table].filter({:pkid =>pkid}).delete
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def update(db)
|
71
|
+
db[self.class.get_table].filter({:pkid => pkid}).update(save)
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# Define which fields are needed. Sent in as an array of symbols or strings
|
76
|
+
def setup_fields(fields)
|
77
|
+
h = {}
|
78
|
+
fields.each do |field|
|
79
|
+
if field.kind_of? Symbol
|
80
|
+
h[field.to_s] = nil
|
81
|
+
else
|
82
|
+
h[field] = nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
@_dbdata = OpenStruct.new(fields)
|
86
|
+
@_frozen = true
|
87
|
+
end
|
88
|
+
|
89
|
+
# Avoid anyone referencing @_dbdata directly
|
90
|
+
def empty?
|
91
|
+
@_dbdata == nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# When overiding method_missing this one should also be...
|
95
|
+
def respond_to(*args)
|
96
|
+
super(*args)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Freeze struct so new fields aren't added by mistake
|
100
|
+
def freeze_fields
|
101
|
+
@_frozen = true
|
102
|
+
end
|
103
|
+
|
104
|
+
# Unfreeze struct so new fields can be added manually. Remember to freeze afterwards
|
105
|
+
def unfreeze_fields
|
106
|
+
@_frozen = false
|
107
|
+
end
|
108
|
+
|
109
|
+
#Class methods for included dbstructs
|
110
|
+
def self.included receiver
|
111
|
+
receiver.extend DBStructClassMethods
|
112
|
+
end
|
113
|
+
|
114
|
+
module DBStructClassMethods
|
115
|
+
# Used to create objects based on database rows
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
def create(rows)
|
120
|
+
list = []
|
121
|
+
if rows == nil
|
122
|
+
return list
|
123
|
+
end
|
124
|
+
rows.each do |row|
|
125
|
+
the_instance = self.new
|
126
|
+
the_instance.load(row)
|
127
|
+
list << the_instance
|
128
|
+
end
|
129
|
+
list
|
130
|
+
end
|
131
|
+
|
132
|
+
#Hack enable referencing modularized class-variable from instance
|
133
|
+
def get_table
|
134
|
+
return @@_table
|
135
|
+
end
|
136
|
+
|
137
|
+
#Binds class to given table in the database, Sequel specific
|
138
|
+
def bind_table(db,table)
|
139
|
+
tablefields = []
|
140
|
+
@@_table = table
|
141
|
+
|
142
|
+
if db.schema(table).empty?
|
143
|
+
raise ArgumentError, "Table [#{table}] was not found. Unable to bind object", caller(1)
|
144
|
+
end
|
145
|
+
|
146
|
+
db.schema(table).each do |fields|
|
147
|
+
tablefields << fields[0]
|
148
|
+
end
|
149
|
+
bind(tablefields)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Use this to bind a class to a table, so new objects created with template will match fields in database
|
153
|
+
def bind(fields)
|
154
|
+
|
155
|
+
if fields.empty?
|
156
|
+
raise ArgumentError, "Trying to bind object to empty list of fields..."
|
157
|
+
end
|
158
|
+
|
159
|
+
if (@@_template rescue nil) != nil
|
160
|
+
raise ArgumentError, "Class already bound to table: #{@@_template.to_s}. Use method rebind if new binding is needed", caller(1)
|
161
|
+
end
|
162
|
+
@@_template = {}
|
163
|
+
fields.each { |field| @@_template[field] = nil}
|
164
|
+
return
|
165
|
+
end
|
166
|
+
|
167
|
+
# Method can rebind fields if database changes dynamicly.... I'm not sure you should need this one....
|
168
|
+
def rebind!(fields)
|
169
|
+
@@_template = {}
|
170
|
+
fields.each { |field| @@_template[field] = nil}
|
171
|
+
return
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
def find(db,search_criteria)
|
176
|
+
db[@@_table].filter(search_criteria)
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
def delete!(db,search_criteria)
|
181
|
+
db[@@_table].filter(search_criteria).delete
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
def update!(db,search_criteria, update_criteria)
|
186
|
+
db[@@_table ].filter(search_criteria).update(update_criteria)
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
|
191
|
+
def template(*args)
|
192
|
+
the_instance = self.new(*args)
|
193
|
+
the_instance.load(@@_template)
|
194
|
+
the_instance
|
195
|
+
end
|
196
|
+
|
197
|
+
def find(db,search_criteria)
|
198
|
+
create(db[@@_table].where(search_criteria))
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sequel'
|
3
|
+
require '../lib/dbstruct'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
class AnObject
|
7
|
+
include DBStruct
|
8
|
+
@non_persisted_field
|
9
|
+
attr_accessor :non_persisted_field
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
@non_persisted_field = args[0]
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def return_value_from_field
|
17
|
+
return @non_persisted_field
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class CreateDb < Sequel::Migration
|
22
|
+
def up
|
23
|
+
create_table :person do
|
24
|
+
primary_key :pkid
|
25
|
+
text :name
|
26
|
+
float :amount
|
27
|
+
integer :age
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class DBStruct_spec
|
34
|
+
DB = Sequel.sqlite '', :logger => [Logger.new($stdout)]
|
35
|
+
CreateDb.apply(DB,:up)
|
36
|
+
AnObject.bind_table(DB,:person)
|
37
|
+
|
38
|
+
def self.create_test_object
|
39
|
+
r = AnObject.template
|
40
|
+
r.name = 'Donald'
|
41
|
+
r.amount = 10.1
|
42
|
+
r.age = 77
|
43
|
+
return r
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
context "Create empty object and access fields" do
|
48
|
+
setup do
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
specify "All fields should have accesors based on table names" do
|
53
|
+
r = DBStruct_spec.create_test_object
|
54
|
+
|
55
|
+
r.name.should == 'Donald'
|
56
|
+
r.amount.should == 10.1
|
57
|
+
r.age.should == 77
|
58
|
+
end
|
59
|
+
|
60
|
+
specify "should raise error when accessing field that doesn't exist in database or object" do
|
61
|
+
r = AnObject.template
|
62
|
+
lambda {r.field_does_nt_exist}.should raise_error
|
63
|
+
end
|
64
|
+
|
65
|
+
specify "should be able to accesses transient (non persisted) values" do
|
66
|
+
r = AnObject.template
|
67
|
+
r.non_persisted_field = 'BOO'
|
68
|
+
r.non_persisted_field.should == 'BOO'
|
69
|
+
end
|
70
|
+
|
71
|
+
specify "should be able to pass variables to object constructor (initialize)" do
|
72
|
+
r = AnObject.template('BOO')
|
73
|
+
r.non_persisted_field.should == 'BOO'
|
74
|
+
end
|
75
|
+
|
76
|
+
specify "should be able to call ordinary method on persisted object" do
|
77
|
+
r = AnObject.template("BOO")
|
78
|
+
r.return_value_from_field.should == "BOO"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
context "Use instance to perform insert, read, update and delete" do
|
84
|
+
setup do
|
85
|
+
end
|
86
|
+
|
87
|
+
specify "should be able to insert and read object" do
|
88
|
+
i = DBStruct_spec.create_test_object
|
89
|
+
|
90
|
+
lambda {i.insert(DB)}.should_not raise_error
|
91
|
+
from_db = AnObject.find(DB,{:age => 77}).first
|
92
|
+
from_db.name == i.name.should
|
93
|
+
from_db.amount == i.amount.should
|
94
|
+
from_db.age == i.age.should
|
95
|
+
|
96
|
+
i.amount = 12.1
|
97
|
+
i.update(DB)
|
98
|
+
from_db = AnObject.find(DB,{:age => 77}).first
|
99
|
+
from_db.name == i.name.should
|
100
|
+
from_db.amount == i.amount.should
|
101
|
+
from_db.age == i.age.should
|
102
|
+
|
103
|
+
lambda {AnObject.delete!(DB,{:age => 77})}.should_not raise_error
|
104
|
+
#i.delete(DB)
|
105
|
+
deleted = AnObject.find(DB,{:age => 77})
|
106
|
+
deleted.should == []
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "Using class to perform statements that changes multiple rows" do
|
111
|
+
specify "should update multiple rows" do
|
112
|
+
i1 = DBStruct_spec.create_test_object
|
113
|
+
i1.insert(DB)
|
114
|
+
i2 = DBStruct_spec.create_test_object
|
115
|
+
i2.insert(DB)
|
116
|
+
lambda {AnObject.update!(DB, {:name => 'Donald'}, :name => 'Dolly')}.should_not raise_error
|
117
|
+
|
118
|
+
from_db = AnObject.find(DB,{:name => 'Dolly'}).first
|
119
|
+
from_db.name.should == 'Dolly'
|
120
|
+
end
|
121
|
+
|
122
|
+
specify "should delete multiple rows" do
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "Use indirect approach to save and store objects" do
|
128
|
+
setup do
|
129
|
+
end
|
130
|
+
|
131
|
+
specify "should be able to insert and read object indirectly" do
|
132
|
+
r = DBStruct_spec.create_test_object
|
133
|
+
r.age = 88
|
134
|
+
DB[:person].insert(r.save)
|
135
|
+
|
136
|
+
row = DB[:person].filter({:age => 88})
|
137
|
+
from_db = AnObject.create(row).first
|
138
|
+
from_db.name.should == r.name
|
139
|
+
from_db.age.should == r.age
|
140
|
+
from_db.amount == r.amount
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mudnaes-dbstruct
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Morten Udn\xC3\xA6s"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-12-03 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: diff-lcs
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.0.0
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: mime-types
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">"
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 0.0.0
|
32
|
+
version:
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: open4
|
35
|
+
version_requirement:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.0.0
|
41
|
+
version:
|
42
|
+
description: This is a very simple framework to be able to access database rows as objects without forcing you to inherit from a Model class. Instead you create the class-hierarchy you want and use mixin to add persistance functionality to the object.
|
43
|
+
email: mudnaes@gmail.com
|
44
|
+
executables: []
|
45
|
+
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
extra_rdoc_files:
|
49
|
+
- README
|
50
|
+
files:
|
51
|
+
- README
|
52
|
+
- dbstruct.gemspec
|
53
|
+
- lib/dbstruct.rb
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/mudnaes/dbstruct/tree/master
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --main
|
59
|
+
- README.txt
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.2.0
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: Easy to use library for mapping rows between objects and database
|
81
|
+
test_files:
|
82
|
+
- spec/dbstruct_spec.rb
|