peel 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dev_journal/Day_0.md +109 -0
- data/dev_journal/Day_1.md +181 -0
- data/dev_journal/Day_2.md +59 -0
- data/dev_journal/Day_3.md +138 -0
- data/dev_journal/Day_4.md +195 -0
- data/dev_journal/Day_5..md +366 -0
- data/lib/peel.rb +22 -0
- data/lib/peel/modelable.rb +52 -0
- data/lib/peel/repository.rb +14 -0
- data/lib/peel/version.rb +3 -0
- data/peel.gemspec +31 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3cc4a3fcd5bdf85bd8a05fff0bcb3e2f60b15729
|
4
|
+
data.tar.gz: 047c72e43c0f25a1d75f0bdba97ba4b824f14aef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 321f4a382242123c38982978c9630f421851362c906bb2fc47141e889c7aeb5e3635166fb9adce48dd2d315f3a88711a7a8ff9003ebbe33ddcdc4ef32464be02
|
7
|
+
data.tar.gz: 789903c08e883c70dd5d8e419aca20b8e736233f0bc9134160a85a9c40702c40400851e40d2c0644e562cc85844b10597eaa03e01667318b23579a65f09777bd
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at thomascountz@gmail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
peel (0.1.0)
|
5
|
+
sqlite3
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.3)
|
11
|
+
rake (10.4.2)
|
12
|
+
rspec (3.7.0)
|
13
|
+
rspec-core (~> 3.7.0)
|
14
|
+
rspec-expectations (~> 3.7.0)
|
15
|
+
rspec-mocks (~> 3.7.0)
|
16
|
+
rspec-core (3.7.1)
|
17
|
+
rspec-support (~> 3.7.0)
|
18
|
+
rspec-expectations (3.7.0)
|
19
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
20
|
+
rspec-support (~> 3.7.0)
|
21
|
+
rspec-mocks (3.7.0)
|
22
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
+
rspec-support (~> 3.7.0)
|
24
|
+
rspec-support (3.7.1)
|
25
|
+
sqlite3 (1.3.13)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
bundler (~> 1.16)
|
32
|
+
peel!
|
33
|
+
rake (~> 10.0)
|
34
|
+
rspec (~> 3.0)
|
35
|
+
|
36
|
+
BUNDLED WITH
|
37
|
+
1.16.6
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Thomas Countz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Peel
|
2
|
+
|
3
|
+
Non-production Ruby implementation of the Active Record Pattern, inspired by Gregory Brown's Broken Record project.
|
4
|
+
|
5
|
+
## Development Journal
|
6
|
+
|
7
|
+
A contemporaneous journal of the development of Peel can be found under [/dev_journal](dev_journal)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'peel'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install peel
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Configure Peel to use an _existing_ SQLite databse file, by calling `Peel.configure()` and passing in a block:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# config.rb
|
31
|
+
require 'peel'
|
32
|
+
Peel.configure do |config|
|
33
|
+
config.database_file = "books_app"
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Include `Peel::Modelable` in your class and call `peel_off`, passing in the table name as a symbol or string:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# book.rb
|
41
|
+
require 'config'
|
42
|
+
class Book
|
43
|
+
include Peel::Modelable
|
44
|
+
peel_off(:books)
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
Call `.find()` on your model. A new instance will be returned with getters/setters based on the row set:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
book = Book.find(1)
|
52
|
+
#=> #<Book:0x007f803ea9e938 @author="Metz, Sandi", @id=1, @isbn="0311237841549", @title="Practical Object-Oriented Design in Ruby">
|
53
|
+
|
54
|
+
book.title
|
55
|
+
#=> "Practical Object-Oriented Design in Ruby"
|
56
|
+
```
|
57
|
+
|
58
|
+
## Development
|
59
|
+
|
60
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
61
|
+
|
62
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/peel. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
67
|
+
|
68
|
+
## License
|
69
|
+
|
70
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
71
|
+
|
72
|
+
## Code of Conduct
|
73
|
+
|
74
|
+
Everyone interacting in the Peel project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/peel/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "peel"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Day 0
|
2
|
+
|
3
|
+
## The Project
|
4
|
+
|
5
|
+
As one of the final projects during my apprenticeship at 8th Light, I've been tasked with implementing a feature-light ORM in Ruby, modeled after the Active Record _pattern_ and inspired by Gregory Brown's [Broken Record project](https://practicingruby.com/articles/implementing-the-active-record-pattern-1).
|
6
|
+
|
7
|
+
"Pattern?" Up until this point, I knew about the `ActiveRecord` ORM that ships with Ruby on Rails, but I wasn't aware that this was an implementation of the _active record pattern_...
|
8
|
+
|
9
|
+
## Init Research
|
10
|
+
|
11
|
+
In _Patterns of Enterprise Application Architecture_, Martin Fowler introduces the Active Record pattern as:
|
12
|
+
|
13
|
+
> _...an object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data..._
|
14
|
+
|
15
|
+
This sounds like how one might describe Ruby on Rails' `ActiveRecord`, and that's no mistake. In the Ruby on Rails guides' [_Active Record Basics_](https://guides.rubyonrails.org/active_record_basics.html#the-active-record-pattern), they describe it like so:
|
16
|
+
|
17
|
+
> It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system... In Active Record, objects carry both persistent data and behavior which operates on that data.
|
18
|
+
|
19
|
+
Let's say I have a database table called `users`, which has three columns, `id`, `firstname`, and `lastname`. I could represent this table, and some data in it, like this:
|
20
|
+
|
21
|
+
```
|
22
|
+
| id | firstname | lastname |
|
23
|
+
|----|-----------|----------|
|
24
|
+
| 0 | Simon | Parker |
|
25
|
+
| 1 | Mary | Souza |
|
26
|
+
| 2 | Tristen | Lu-Chen |
|
27
|
+
```
|
28
|
+
|
29
|
+
If I want to get the data for `Mary Souza`, I might write an SQL query like this:
|
30
|
+
|
31
|
+
```sql
|
32
|
+
SELECT * FROM users WHERE id = 1;
|
33
|
+
```
|
34
|
+
|
35
|
+
And I could expect to get some data back like this:
|
36
|
+
|
37
|
+
```
|
38
|
+
| id | firstname | lastname |
|
39
|
+
|----|-----------|----------|
|
40
|
+
| 1 | Mary | Souza |
|
41
|
+
```
|
42
|
+
|
43
|
+
However, from my _application_, I might instead want to write something like this:
|
44
|
+
|
45
|
+
```
|
46
|
+
user = User.select_where(id: 1)
|
47
|
+
```
|
48
|
+
|
49
|
+
And get back and object like this:
|
50
|
+
|
51
|
+
```
|
52
|
+
#{User "id": 1, "firstname": "Mary", "lastname":"Souza"}
|
53
|
+
```
|
54
|
+
|
55
|
+
And supply behavior to that data, such as with a method called `fullname`:
|
56
|
+
|
57
|
+
```
|
58
|
+
user.fullname
|
59
|
+
#=> "Mary Souza"
|
60
|
+
```
|
61
|
+
|
62
|
+
This object is an Active Record object!
|
63
|
+
|
64
|
+
It 1) wraps data from a row in the database, 2) encapsulates the retrieval of that data with the `.select_where` method, and 3) adds domain-specific behavior, with the `.fullname` method.
|
65
|
+
|
66
|
+
Martin Fowler extends this descrption with the following:
|
67
|
+
|
68
|
+
>The Active Record class typically has methods that do the following:
|
69
|
+
>
|
70
|
+
>- Construct an instance of the Active Record from a SQL result set row
|
71
|
+
>- Construct a new instance for later insertion into the table
|
72
|
+
>- Static finder methods to wrap commonly used SQL queries and return Active Record objects
|
73
|
+
>- Update the database and insert into it the data in the Active Record
|
74
|
+
>- Get and set the fields
|
75
|
+
>- Implement some pieces of business logic
|
76
|
+
|
77
|
+
___
|
78
|
+
|
79
|
+
This sounds pretty good, and more often than not, if you've used Ruby on Rails, you are familiar with the core feature-set of the active record implementation. However, throughout the apprenticeship, I've learned to always take a moment ask if this is the most cost-effective solution.
|
80
|
+
|
81
|
+
**Side Note**: What defines "cost" will always vary widely from project to project, feature to feature. Some general things to keep in mind are how easy a solution is to implement/maintain, how much time will the implementation take, does it directly solve our problem, is the expected return greater than the cost of delivery, and does it matter? The definition of cost must be defined for every feature, so for our purposes, I'll define cost as "does it directly solve our problem," and with that, we must first identify the problem!
|
82
|
+
|
83
|
+
For this project, the problem is, "Thomas needs a fun and exciting project to work on, he could benefit with some more time working with databases, and he loves working with Ruby, what should he build?" But, pushing that meta-problem aside, what problem does the active record pattern solve/when would we generally aim to implement it?
|
84
|
+
|
85
|
+
> Active Record is a good choice for domain logic that isn’t too complex, such as creates, reads, updates, and deletes. Derivations and validations based on a single record work well in this structure... Their primary problem is that they work well only if the Active Record objects correspond directly to the database tables... Another argument against Active Record is the fact that it couples the object design to the database design.
|
86
|
+
>
|
87
|
+
> Martin Fowler, _Patterns of Enterprise Application Architecture_
|
88
|
+
|
89
|
+
## What's The Problem
|
90
|
+
|
91
|
+
Let's invent a little MVP scenario to get shake things loose and get us thinking creatively.
|
92
|
+
|
93
|
+
> We want to build a simple CRUD app to manage a global list of books in our library. We should be able to record a book's title, author, and ISBN number.
|
94
|
+
|
95
|
+
The Active Record pattern solves our scenario. Let's also say that we've weighed all of the costs and the decision to implement Active Record has gotten the green light!
|
96
|
+
|
97
|
+
Where do we go from here?
|
98
|
+
|
99
|
+
## Initial Thoughts on Implementing Active Record
|
100
|
+
|
101
|
+
Without doing any more research at this point, I'll begin by reiterating what I understand as the three major features that need to be implemented in order to deliver an Active Record object:
|
102
|
+
|
103
|
+
1. Abstract database connections/queries
|
104
|
+
2. Encapsulate data into an "Active Record" object
|
105
|
+
3. Provide an interface to add domain-specific behavior
|
106
|
+
|
107
|
+
In the example presented in _Patterns of Enterprise Application Architecture_, Martin Fowler's houses all of this within a single object, and I think I'll begin with that approach.
|
108
|
+
|
109
|
+
If we can implement a single Active Record object that connects to the database, wraps data, and provides an interface to that data, then maybe we can iterate until we end up with a reusable abstracted interfaces for composing new domain-specific Active Record objects.
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# Day 1
|
2
|
+
|
3
|
+
## Where to Begin
|
4
|
+
|
5
|
+
First, let's quickly revisit our problem domain:
|
6
|
+
|
7
|
+
> We want to build a simple CRUD app to manage a global list of books in our library. We should be able to record a book's title, author, and ISBN number.
|
8
|
+
|
9
|
+
Now, let's start with some initial, sometimes arbitrary, design decisions so that I can zero-in on the meat of the problem. First, like Gregory Brown's [Broken Record project](https://practicingruby.com/articles/implementing-the-active-record-pattern-1), I think I'll begin with an SQLite database. SQLite is the default Ruby on Rails development database, and its light footprint means I should be able to get up and running quickly. We'll utilize the `SQLite3` gem, which you can read up on [here](https://github.com/sparklemotion/sqlite3-ruby/).
|
10
|
+
|
11
|
+
I'm also going to assume that the database table already exists. Later in the project, I hope to take a look at managing schema migrations, but for now, the creation and alteration of database tables/columns will happen outside of the active record object. This is a good example of how the `ActiveRecord` implementation from Ruby on Rails goes above and beyond the basic definition of the Active Record pattern.
|
12
|
+
|
13
|
+
The database is build like this:
|
14
|
+
|
15
|
+
```sqlite
|
16
|
+
CREATE TABLE IF NOT EXISTS books (
|
17
|
+
id INTEGER PRIMARY KEY,
|
18
|
+
title VARCHAR(255),
|
19
|
+
author VARCHAR(50),
|
20
|
+
isbn VARCHAR(13)
|
21
|
+
);
|
22
|
+
```
|
23
|
+
|
24
|
+
The line `CREATE TABLE IF NOT EXISTS` does exactly what it sounds like, it will either create the table based on the schema provided, or, if a `books` table exits, it will do nothing.
|
25
|
+
|
26
|
+
This is important to note because a change to this SQL statement will not _update_ the `books` table. For example, if we wanted to add another column `pages`, to record the number of pages in our books, we couldn't simply add another line to our statement and run it again.
|
27
|
+
|
28
|
+
```sqlite
|
29
|
+
/* This new statement will not update an existing table */
|
30
|
+
CREATE TABLE IF NOT EXISTS books (
|
31
|
+
id INTEGER PRIMARY KEY,
|
32
|
+
title VARCHAR(255),
|
33
|
+
author VARCHAR(50),
|
34
|
+
isbn VARCHAR(13),
|
35
|
+
pages INTEGER
|
36
|
+
);
|
37
|
+
```
|
38
|
+
|
39
|
+
The next few lines create the columns of our table. `id`, `title`, `author`, and `isbn`, each hold the the data belonging to each book. The interesting one to note is `id`, which is declared as `INTEGER PRIMARY KEY`
|
40
|
+
|
41
|
+
> If you declare a column of a table to be [INTEGER PRIMARY KEY](https://www.sqlite.org/lang_createtable.html#rowid), then whenever you insert a `NULL` into that column of the table, the `NULL` is automatically converted into an integer which is one greater than the largest value of that column over all other rows in the table, or `1` if the table is empty.
|
42
|
+
>
|
43
|
+
> -[SQLite FAQ](https://www.sqlite.org/faq.html#q1)
|
44
|
+
|
45
|
+
## SQLite3 Gem
|
46
|
+
|
47
|
+
In order to build an Active Record object, we need a database, and in order to use a database, we need an adapter. Fortunately for us, it is not the responsibility of an Active Record object to facility the infrastructure of communication between itself and the database. For our purposes, we'll begin with the [SQLite3 gem](https://github.com/sparklemotion/sqlite3-ruby) that abstracts the database away from our object.
|
48
|
+
|
49
|
+
Later we'll see how we can wrap this gem in an object we own, so that we can move towards becoming database agnostic.
|
50
|
+
|
51
|
+
## Spiking an Active Record Object
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
require 'sqlite3'
|
55
|
+
|
56
|
+
db = SQLite3::Database.new("books.db").execute <<-SQL
|
57
|
+
CREATE TABLE IF NOT EXISTS books (
|
58
|
+
id INTEGER PRIMARY KEY,
|
59
|
+
title VARCHAR(255),
|
60
|
+
author VARCHAR(50),
|
61
|
+
isbn VARCHAR(13)
|
62
|
+
);
|
63
|
+
SQL
|
64
|
+
|
65
|
+
class Book
|
66
|
+
class << self
|
67
|
+
def create(title:, author:, isbn:)
|
68
|
+
db = SQLite3::Database.new("books.db")
|
69
|
+
db.execute(
|
70
|
+
"INSERT INTO books (title, author, isbn) VALUES (?, ?, ?)",
|
71
|
+
[title, author, isbn]
|
72
|
+
)
|
73
|
+
id = db.last_insert_row_id
|
74
|
+
Book.new(id: id, title: title, author: author, isbn: isbn)
|
75
|
+
end
|
76
|
+
|
77
|
+
def find(id)
|
78
|
+
result = SQLite3::Database.new("books.db").execute(
|
79
|
+
"SELECT * FROM books WHERE id = ? LIMIT 1", id
|
80
|
+
)
|
81
|
+
result.empty? ? result : Book.new(id: result[0][0],
|
82
|
+
title: result[0][1],
|
83
|
+
author: result[0][2],
|
84
|
+
isbn: result[0][3]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(id: nil, title:, author:, isbn:)
|
90
|
+
@id = id
|
91
|
+
@title = title
|
92
|
+
@author = author
|
93
|
+
@isbn = isbn
|
94
|
+
end
|
95
|
+
|
96
|
+
def update(title: nil, author: nil, isbn: nil)
|
97
|
+
title = title || @title
|
98
|
+
author = author || @author
|
99
|
+
isbn = isbn || @isbn
|
100
|
+
|
101
|
+
SQLite3::Database.new("books.db").execute <<-SQL
|
102
|
+
UPDATE books
|
103
|
+
SET title = '#{title}', author = '#{author}', isbn = '#{isbn}'
|
104
|
+
WHERE id = #{@id};
|
105
|
+
SQL
|
106
|
+
Book.new(id: @id, title: title, author: author, isbn: isbn)
|
107
|
+
end
|
108
|
+
|
109
|
+
def destroy
|
110
|
+
SQLite3::Database.new("books.db").execute("DELETE FROM books WHERE id = ?", @id)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Example Usage ##
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
Book.create(title: "Practical Object Oriented Design in Ruby", author: "Metz, Sandi", isbn: "9780321721334")
|
119
|
+
# => #<Book:0x007fa904193e80 @author="Metz, Sandi", @id=1, @isbn="9780321721334", @title="Practical Object Oriented Design in Ruby">
|
120
|
+
|
121
|
+
Book.create(title: "Patterns of Enterprise Apllication Architecture", author: "Fowler, Martin", isbn: "9780321127426")
|
122
|
+
# => #<Book:0x007fa9039290b0 @author="Fowler, Martin", @id=2, @isbn="9780321127426", @title="Patterns of Enterprise Apllication Architecture">
|
123
|
+
|
124
|
+
book = Book.find(1)
|
125
|
+
# => #<Book:0x007fa903965e98 @author="Metz, Sandi", @id=1, @isbn="9780321721334", @title="Practical Object Oriented Design in Ruby">
|
126
|
+
|
127
|
+
book.title
|
128
|
+
# => "Practical Object Oriented Design in Ruby"
|
129
|
+
|
130
|
+
new_book = book.update(title: "POODR")
|
131
|
+
# => #<Book:0x007fa90291f638 @author="Metz, Sandi", @id=1, @isbn="9780321721334", @title="POODR">
|
132
|
+
|
133
|
+
Book.find(2)
|
134
|
+
# => <Book:0x007fa9028a72c8 @author="Fowler, Martin", @id=2, @isbn="9780321127426", @title="Patterns of Enterprise Apllication Architecture">
|
135
|
+
|
136
|
+
Book.find(2).destroy
|
137
|
+
# => []
|
138
|
+
|
139
|
+
Book.find(2)
|
140
|
+
# => []
|
141
|
+
```
|
142
|
+
|
143
|
+
This is an Active Record object. It's not pretty, but it illustrates the core features of the pattern.
|
144
|
+
|
145
|
+
From the client's perspective, they're only dealing with a POOR, _plain old Ruby object_. Without debating the pros and cons of obfuscating database interactions, I think our `Book` class does a pretty good job!
|
146
|
+
|
147
|
+
Here is a list that Martin Fowler describes as typical Active Record object behaviors and a mapping to what our object does:
|
148
|
+
|
149
|
+
1. Construct an instance of the Active Record from a SQL result set row *(e.g. `Book.find()`)*
|
150
|
+
2. Static finder methods to wrap commonly used SQL queries and return Active Record objects *(e.g. `Book.find()` & `Book.create()`)*
|
151
|
+
3. Update the database and insert into it the data in the Active Record *(e.g. `Book#update()`)*
|
152
|
+
4. Get and set the fields *(e.g. `Book#id()`, `Book#title()`, etc)*
|
153
|
+
|
154
|
+
Two other behaviors are:
|
155
|
+
|
156
|
+
1. Implement some pieces of business logic
|
157
|
+
2. Construct a new instance for later insertion into the table
|
158
|
+
|
159
|
+
Which aren't currently implemented, however, we could follow the pattern above and implement these features pretty quickly.
|
160
|
+
|
161
|
+
## What I Learned
|
162
|
+
|
163
|
+
The spiked implementation highlights a few things for me.
|
164
|
+
|
165
|
+
1. The `ActiveRecord` implementation provided by Ruby on Rails does more than just implement the Active Record pattern.
|
166
|
+
|
167
|
+
`ActiveRecord` isn't just an *implementation* of a pattern, it's an _abstraction_ of the pattern. `ActiveRecord` allows its clients to _create_ Active Record objects, rather than being one. It uses object-orientation to produce interfaces which allow you or I to more easily create a `Book` class that interacts with a database. Using the Active Record pattern to build an ORM is not the same as using Active Record pattern to build an object.
|
168
|
+
|
169
|
+
2. Testing is tricky.
|
170
|
+
|
171
|
+
The clearest way of testing the spiked version of `Book` is to write end-to-end tests that actually touch the database. This isn't ideal, so it's worth taking a step back and considering #1, above. The goal is to build an ORM abstraction that other classes can use, not just build an Active Record class, so there's some pieces missing in the current iteration.
|
172
|
+
|
173
|
+
3. It's all about generalization.
|
174
|
+
|
175
|
+
The spike has left me with ugly code, but "working" code. I say, "working," because I've only got manual tests to verify what "working" means. However, with working code in hand, now I can begin to see abstractions that I could only guess about before.
|
176
|
+
|
177
|
+
## Next Steps
|
178
|
+
|
179
|
+
How can I make SQL queries dynamic? How can I generalize a class' attributes, based on database columns? How can I remove hardcoded dependencies on the `SQLite3` gem?
|
180
|
+
|
181
|
+
Tomorrow, it's into the fire with these questions in hand. Thankfully, Ruby allows us to answer these questions in many different ways. Hopefully, tests can drive us to the answers!
|