• Stars
    star
    191
  • Rank 196,008 (Top 4 %)
  • Language
    Crystal
  • License
    MIT License
  • Created about 9 years ago
  • Updated about 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Active Record pattern implementation for Crystal.

active_record Build Status

Active Record pattern implementation for Crystal.

Don't confuse with Ruby's activerecord: aim of this is to be true to OO techniques and true to the Active Record pattern. Small, simple, useful for non-complex domain models. For complex domain models better use Data Mapper pattern.

Work in progress

Contents:

TODO

  • Implement model definition syntax
  • Implement field_level
  • Implement NullAdapter (in-memory, for specs)
  • Implement #create, .create and .get
  • Implement .where
  • Implement query_level
  • Implement #update and #delete
  • Implement better query DSL
  • Default table_name implementation
  • Implement mysql adapter
  • Populate this list further by making some simple app on top of it
  • Describe in readme how to implement your own adapter
  • Add transaction features
  • Implement postgres driver
  • Support more types (currently only Int | String are supported)
    • Time
    • Bool
    • Arbitrary type that supports specific interface
  • Support joins

Installation

Add it to shard.yml:

dependencies:
  active_record:
    github: waterlink/active_record.cr
    version: ~> 0.4

Additionally you would need to choose your database driver adapter. For example, if you want to use waterlink/postgres_adapter.cr, then you would add this dependency:

  postgres_adapter:
    github: waterlink/postgres_adapter.cr

See list of known adapters.

Usage

require "active_record"
# And require your chosen database adapter:
require "postgres_adapter"

Define your model

class Person < ActiveRecord::Model

  # Set adapter to your chosen adapter name
  adapter postgres

  # Set table name, defaults to "#{lowercase_name}s"
  # table_name people

  # Database fields
  primary id                 : Int
  field last_name            : String
  field first_name           : String
  field number_of_dependents : Int

  # Domain logic
  def get_tax_exemption
    # ...
  end

  def get_taxable_earnings
    # ...
  end

end

Create a new record

# Combine .new(..) and #create
Person.new({ "first_name"           => "John",
             "last_name"            => "Smith",
             "number_of_dependents" => 3 }).create #=> #<Person: ...>

# Or shortcut with .create(..)
Person.create({ "first_name"           => "John",
                "last_name"            => "Smith",
                "number_of_dependents" => 3 })     #=> #<Person: ...>

Get existing record by id

Person.get(127)  #=> #<Person: @id=127, ...>

Query multiple records

# Get all records
Person.all         # => [#<Person: ...>, #<Person: ...>, ...]

# Query by hash
Person.where({ "number_of_dependents" => 0 })   #=> [#<Person: ...>, #<Person: ...>, ...]

# Or construct a query object
include ActiveRecord::CriteriaHelper
Person.where(criteria("number_of_dependents") > 3)    #=> [#<Person: ...>, #<Person: ...>, ...]

See Query DSL

Update existing record

person = Person.get(127)
person.number_of_dependents = 0
person.update

Delete existing record

Person.get(127).delete

Enforcing encapsulation

If you care about OO techniques, code quality and handling complexity, please enable this for your models.

class Person < ActiveRecord::Model

  # Default is public for ease of use
  field_level :private
  # field_level :protected

  query_level :private
  # default is public, there is no point in protected here

  # ...
end

# Enforces you to maintain encapsulation, ie: not expose your data -
# put behavior in the same place the data it needs
person = Person.get(127)    # or Person[127]
person.first_name   #=> Error: unable to call private method first_name

# Enforces you to maintain DRYness to some extent, ie: not leak
# knowledge about your database structure, but put it in active record
# model and expose your own nit-picked methods
Person.where({ :first_name => "John" })    #=> Error: unable to call private method where

Query DSL

This library uses https://github.com/waterlink/query.cr/ for handling query construction and query DSL.

Generally to use #criteria DSL method, you need to include ActiveRecord::CriteriaHelper, but inside of your model code you don't need to do that.

Examples (comment is in format [sql_query, params]):

criteria("person_id") == 3                            # [person_id = :1, { "1" => 3 }]
criteria("person_id") == criteria("other_person_id")  # [person_id = other_person_id, {}]

criteria("number") <= 3                               # [number < :1, { "1" => 3 }]

(!(criteria("number") <= 3))                          # [(NOT (number <= :1)) AND (number <> :2),
  .and(criteria("number") != 5)                       #  { "1" => 3, "2" => 5 }]

criteria("subject_id").is_not_null                    # [(subject_id) IS NOT NULL, {}]

Supported comparison operators: == != > >= < <=

Supported logic operators: or | and & xor ^ not

Supported is operators: is_true is_not_true is_false is_not_false is_unknown is_not_unknown is_null is_not_null

Filtering with IN (..., ..., ...) query:

criteria("subject_id").in([37, 42, 45])

Connection configuration

Connection configuration is delegated to the database adapter library. So find it in the respective library's documentation. Known adapters.

Connection Pool configuration (Experimental)

Each distinct model class has its own associated connection pool. By default, pool capacity is 1 and timeout is 2 seconds. These settings can be changed on per model basis:

class Person < ActiveRecord::Model
  @@connection_pool_capacity = 25
  @@connection_pool_timeout = 0.03  # seconds
end

Joins (TODO)

This is still not implemented.

class User < ActiveRecord::Model
  has_many Post, criteria("posts.author_id") == criteria("users.id")
  # ...
end

class Post < ActiveRecord::Model
  belongs_to User, criteria("posts.author_id") == criteria("users.id")
  # ...
end

# makes only one join query
posts = Post.join(User).all
posts.first.title      # => "Hello world post"
posts.first.user.name  # => "John Smith"

# makes 2 queries - 'select from users' and 'select from posts'
user = User.all.first
user.posts.first.title  # => "Hello world post"
user.posts[1].title     # => "Yet another post"

Known database adapters

Development

After cloning the project:

cd active_record.cr
crystal deps   # install dependencies
crystal spec   # run specs

Just use normal TDD development style.

Creating your own database adapter

So, lets create a postgres adapter. First lets init the repo:

# This creates 'postgres_adapter' library and names directory as 'postgres_adapter.cr'.
# Effectively giving you structure './postgres_adapter.cr/src/mysql_adapter.cr'.
crystal init lib postgres_adapter postgres_adapter.cr

# And lets cd into it right away:
cd postgres_adapter.cr/

Next feel free to edit the README to reflect the usage as you see fit. And check out if generated LICENSE file is OK.

At this point it is a good idea to make an initial commit to git and push your changes to Github (or whatever git upstream you use).

Before the next step you will need active_record bundled as a submodule at path modules/active_record, for that you do:

git submodule add https://github.com/waterlink/active_record.cr modules/active_record

You need to have it as a submodule to be able to require code from spec/ directory.

Next step is to add appropriate integration test boilerplate:

Integration spec:

# integration/integration_spec.cr
require "./spec_helper"

Integration spec helper:

# integration/spec_helper.cr
require "spec"
require "../src/postgres_adapter"
require "active_record/null_adapter"

# Register our adapter as 'null' adapter, effectively overriding what was
# registered before by 'active_record':
ActiveRecord::Registry.register_adapter("null", PostgresAdapter::Adapter)

# Cleanup database before and after each example:
Spec.before_each do
  PostgresAdapter::Adapter._reset_do_this_only_in_specs_78367c96affaacd7
end
Spec.after_each do
  PostgresAdapter::Adapter._reset_do_this_only_in_specs_78367c96affaacd7
end

# Require fake adapter and kick off the integration spec
require "../modules/active_record/spec/fake_adapter"
require "../modules/active_record/spec/active_record_spec"

Integration test runner script:

# bin/test
#!/usr/bin/env bash

set -e

# Run unit tests
crystal spec

# Compile integration tests that are shipped with
crystal build integration/integration_spec.cr -o integration/integration_spec -D active_record_adapter
./integration/integration_spec --fail-fast -v $*

Script for setting up the database:

# script/setup-test-db.sh
#!/usr/bin/env bash

# By providing 'PG_USER' and ('PG_PASS' or `PG_ASK_PASS`) you can
# control how this script will authenticate to local pg server.
PARAMS="-U ${PG_USER:-postgres}"
[[ -z "$PG_PASS" ]] || PGPASSWORD="$PG_PASS"
[[ -z "$PG_ASK_PASS" ]] || PARAMS="$PARAMS -W"

psql $PARAMS -c "create database crystal_pg_test"
psql $PARAMS -c "create user crystal_pg with superuser password 'crystal_pg'"

psql $PARAMS crystal_pg_test -c "drop table if exists people; create table people( id serial primary key, last_name varchar(50), first_name varchar(50), number_of_dependents int )"
psql $PARAMS crystal_pg_test -c "drop table if exists something_else; create table something_else( id serial primary key, name varchar(50) )"
psql $PARAMS crystal_pg_test -c "drop table if exists posts; create table posts( id serial primary key, title varchar(50), content text, created_at timestamp )"

Make all scripts executable:

chmod a+x bin/test
chmod a+x script/setup-test-db.sh

And setup test db:

script/setup-test-db.sh

If you run tests at this point with bin/test, you should get compile error, since you have not implemented ActiveRecord::Adapter protocol. You can find it here.

First make some stub implementation for this protocol:

# src/postgres_adapter.cr
require "active_record"
require "active_record/adapter"

module PostgresAdapter
  class Adapter < ActiveRecord::Adapter
    def self.build(table_name, primary_field, fields, register = true)
      new(table_name, primary_field, fields, register)
    end

    def self.register(adapter)
      adapters << adapter
    end

    def self.adapters
      (@@_adapters ||= [] of self).not_nil!
    end

    getter table_name, primary_field, fields

    def initialize(@table_name, @primary_field, @fields, register = true)
      self.class.register(self)
    end

    def create(fields)
      0
    end

    def get(id)
      nil
    end

    def all
      [] of Hash(String, ActiveRecord::SupportedType)
    end

    def where(query_hash : Hash)
      all
    end

    def where(query : Query::Query)
      all
    end

    def update(id, fields)
    end

    def delete(id)
    end

    # Resets all data for all registered adapter instances of this kind
    def self._reset_do_this_only_in_specs_78367c96affaacd7
      adapters.each &_reset_do_this_only_in_specs_78367c96affaacd7
    end

    # Resets all data for current table (adapter instance)
    def _reset_do_this_only_in_specs_78367c96affaacd7
    end
  end
end

Of course you need to include active_record as a dependency in your shard.yml:

dependencies:
  active_record:
    github: waterlink/active_record.cr

To install it, run shards or crystal deps.

With this boilerplate you should have actually compiled integration test and it should be RED. Next step would be to follow TDD and make it green example-by-example while replacing stub implementation with real one.

When you are done, congratulate yourself and push first release (git tag) to Github (or whatever git upstream you use).

Don't forget to register your adapter:

# At the end of src/postgres_adapter.cr
ActiveRecord::Registry.register_adapter("postgres", PostgresAdapter::Adapter)

Congratulations, you made it!

Contributing

  1. Fork it ( https://github.com/waterlink/active_record.cr/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

  • waterlink Oleksii Fedorov - creator, maintainer

More Repositories

1

rack-reverse-proxy

A Reverse Proxy for Rack
Ruby
194
star
2

Challenge-Build-Your-Own-Array-In-Js

This is a challenge that will allow you to practice your logical, analytical and problem-solving skills. Additionally, by the end of it you’ll have much better command of arrays in javascript.
JavaScript
177
star
3

rspec-json_expectations

Set of matchers and helpers to allow you test your APIs responses like a pro.
Gherkin
138
star
4

spec2.cr

Enhanced `spec` testing library for [Crystal](http://crystal-lang.org/).
Crystal
103
star
5

mocks.cr

General purpose mocking library for Crystal.
Crystal
51
star
6

crystal-mysql

Basic mysql bindings for crystal.
Crystal
32
star
7

timecop.cr

Mock with `Time.now` with the power of time travel, time freeze and time scale.
Crystal
19
star
8

refactoring-koans-js

Refactoring Koans to help you learn to refactor code smells in javascript
JavaScript
17
star
9

query.cr

Query abstraction for Crystal Language. Used by active_record.cr library.
Crystal
13
star
10

quick.cr

QuickCheck implementation for Crystal Language
Crystal
11
star
11

kotlin-spring-boot-mvc-starter

This is a starter repository for work with Kotlin on Back-end using Spring Boot 2 MVC, JdbcTemplate, Thymeleaf, Emails with Thymeleaf templates, Spring Security, Feature/UI tests using Fluentlenium, Clean Controller->Service->Repository pattern that is a sweet spot as your starting architecture. Includes a small demo in its source code.
Kotlin
11
star
12

postgres_adapter.cr

Postgres adapter for [active_record.cr](https://github.com/waterlink/active_record.cr). Uses [crystal-pg](https://github.com/will/crystal-pg) driver.
Crystal
9
star
13

spec2-mocks.cr

This library connects spec2.cr and mocks.cr, effectively enabling 'have_received' expectation for spec2.
Crystal
8
star
14

expand.cr

Crystal tool for macro debugging. Allows one to expand macro recursively.
Shell
6
star
15

mysql_adapter.cr

Mysql adapter for [active_record.cr](https://github.com/waterlink/active_record.cr). Uses [crystal-mysql library](https://github.com/waterlink/crystal-mysql)
Crystal
6
star
16

timestamp.cr

Timestamps in crystal-lang. Adds `.from_timestamp` and `#to_timestamp` methods to `Time`
Crystal
5
star
17

quizzykotlin

An example application for my free Getting Started With Kotlin Tutorial
Kotlin
4
star
18

rebecca

Simple database convenience wrapper for Go language.
Go
4
star
19

BuildYourOwnTestingFrameworkPart1

This is a source code for the first part of "Build Your Own Testing Framework" series
JavaScript
4
star
20

singleton.cr

Singleton library for Crystal Language.
Crystal
3
star
21

devpoll

devpoll - small web application for making polls written in http://crystal-lang.org/ (Crystal lang)
Crystal
3
star
22

aop

Very thin AOP gem for Ruby
Ruby
3
star
23

four-cycles-of-tdd-lightning-talk

Slides for my lightning talk: 4 Cycles of Test-Driven Development
HTML
3
star
24

money_tracking

CLI tool for tracking your expenses.
Ruby
2
star
25

restricted_struct

RestrictedStruct gem: create Struct-s with private or protected attributes
Ruby
2
star
26

contracts-rspec

Plugin for contracts.ruby that fixes issues with rspec-mocks.
Ruby
2
star
27

openproject-docker

Let OpenProject run in a docker container
Shell
2
star
28

race-conditions-testing-example

Kotlin
2
star
29

plugged

Library for writing extendable CLI applications for Golang.
Go
2
star
30

tspp_project

C++
2
star
31

messenger-ES6

JavaScript
1
star
32

goactor

Thin Actor implementation in Golang
Go
1
star
33

confident.ruby

Be confident and narrative when writing code in ruby
Ruby
1
star
34

secrets

Simple bash script to manage your secrets with symmetric key.
Shell
1
star
35

es-snapshot

This is a small script to make an elasticsearch snapshot. Tested with TDD in Bash in a very esoteric way.
Shell
1
star
36

crystal-blog

Simple blog in crystal using frank and active_record.cr
Crystal
1
star
37

cars-droid

Clojure
1
star
38

ShellTools

My bash shell tools. Unit-tested.
Shell
1
star
39

likes

Give it a list of people and their likings and it will tell what else could these people like. Ruby gem.
Ruby
1
star
40

docker-elasticsearch-kubernetes-aws

Docker image for elasticsearch with support for Kubernetes and AWS cloud
Shell
1
star
41

namegen.cr

This library provides facilities for generating random names/nicknames. Written in Crystal-Lang.
Crystal
1
star
42

stomp

Example game engine in Ruby. Uses Component Entity System + World paradigm.
Ruby
1
star
43

tdd-talk-ru

TDD talk (version in Russian, for Top-Engineer webinar). | Доклад о TDD, для вебинара Top-Engineer.
HTML
1
star
44

elm-todo

Simple ToDo application written in Elm lang. No backend (yet?).
Elm
1
star
45

explorative-tdd-talk

CSS
1
star
46

gas-template

Google Apps Script template for more comfortable development on local machine. Plus CI/CD included.
Shell
1
star
47

LoginSignupE2E

Example project for the blog post "Learning TDD with JS: End-to-End Testing". Implements Login and Signup for a web-application using TDD and E2E feature tests.
JavaScript
1
star
48

cars_api

Ruby
1
star
49

strong_ruby

Just playing around with strong typing in Ruby
Ruby
1
star
50

property-based-testing-talk

HTML
1
star
51

contracts-non_intrusive

Less intrusive version of Contracts DSL. Allows to use static/dynamic code analysis tools.
Ruby
1
star
52

recorder

Daemon that records all incoming HTTP request and allow to fetch them and put expectations on them. Useful for CLI tools testing.
Go
1
star