понеділок, 9 листопада 2009 р.

Привіт, DataMapper!

DataMapper - це ORM бібліотека (англ. Object-relational mapping, Обє'ктно-реляційна проекція - технологія, яка зв'язую бази даних з концепцією об'єктно-орієнтовного програмування, створюючи "віртуальну об'єктну базу даних"). DataMapper написана на Ruby, і широко використовується у таких фреймворках як Merb та Sinatra. Вона була розроблена, щоб усунути недоліки бібліотеки ActiveRecord, яка використовується в Ruby on Rails по замовчуванню. DataMapper слідує стандартам ORM-моделі: таблиці відображаються у вигляді класів, записи - у вигляді об'єктів, а стовпці - у вигляді властивостей цих об'єктів. Методи класу використовуються для здійснення операцій на рівні таблиці, а методи екземпляра здійснюють операції над окремими рядками.
Якщо в базі даних є таблиця з назвою posts (записи), то наш застосунок матиме клас Post. Рядки цієї таблиці відповідають об'єктам класу - конкретний запис представляється як об'єкт класу Post. У межах цього об'єкту для отримання доступу до окремих стовпців і встановлення їм значення, використовуються властивості.

Інсталяція DataMapper

DataMapper доступний через менеджер пакетів Ruby Gem:
gem install dm-core

Якщо ви плануєте використовувати DataMapper з базою даних, встановіть адаптер бази даних з проекту DataObjects. В залежності від ваших уподобань, це може бути do_mysql, do_postgres або do_sqlite3.
gem install do_sqlite3


Давайте уявимо, що ми створюємо деякі моделі для застосунку блогу. Ми отримаємо їх просто і красиво. Перше рішення, яке потрібно ухвалити - які моделі нам потрібні. Очевидно нам потрібна модель для записів (Post) і коментарів (Comment). Також нам можуть знадобитися категорії (Category).

Для роботи з DataMapper необхідно додати наступні рядки до нашого застосунку:
require 'rubygems'
require 'dm-core'

Перед тим, як зайнятися визначенням моделей, потрібно з'єднатися з базою даних:
# An in-memory SQLite3 connection:
DataMapper.setup(:default, 'sqlite3::memory:')

# A file SQLite3 connection:
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/the_database_name.sqlite3")

# A MySQL connection:
DataMapper.setup(:default, 'mysql://localhost/the_database_name')

# A Postgres connection:
DataMapper.setup(:default, 'postgres://localhost/the_database_name')

В нашому застосунку буде використовуватися база даних SQLite3 (яку потрібно встановити, якщо ви збираєтесь відтворювати приведений тут код). Якщо ви користуєтесь іншим сервером баз даних (наприклад, MySQL чи PostgreSQL), вам потрібно заздалегідь подбати про створення бази даних і надання повноважень користувачу.

Визначення моделей

Таблиця для моделі Post повинна існувати в чистому вигляді, тому включаємо DataMapper::Resource. Це вказує на те, що клас Post є ресурсом DataMapper(тобто описує записи в БД). У базі буде створена таблиця posts і ми отримаємо методи для роботи з нею. Угода з іменуванням моделей - використовувати однину, а не множину. Але це лише угода, ви можете робити як бажаєте. Це означає, що в базі буде створена таблиця posts і ми отримаємо методи для роботи з нею.

class Post
  include DataMapper::Resource

  property :id,         Serial
  property :title,      String
  property :body,       Text
  property :created_at, DateTime
  property :published,  Boolean, :default => false

  default_scope(:default).update(:order => [created_at.desc])
end

class Comment
  include DataMapper::Resource

  property :id,         Serial
  property :posted_by,  String
  property :email,      String
  property :url,        String
  property :body,       Text
end

class Category
  include DataMapper::Resource

  property :id,         Serial
  property :name,       String
end

Всередині класу моделі викликається метод property для кожної властивості. Він має два необхідних аргументи - ім'я і тип, а все інше не є обов'язковим.
Тип Serial вказує на те, що властивість буде первинним ключем, тобто AUTOINCREMENT(автоматично отримувати значення).
property :id,  Serial

Властивість created_at буде автоматично заповнюватися часом створення запису TIMESTAMP(для цього необхідно підключити 'dm-timestamps').
DM-Timestamps забезпечує автоматичне оновлення властивостей created_at або created_on і updated_at або updated_on.
property :created_at, DateTime

Аргумент :default, встановлює значення по замовчуванню для властивості.
property :published,  Boolean, :default => false

І нарешті
default_scope(:default).update(:order => [created_at.desc])
говорить нам про те, що записи по замовчуванню треба діставати впорядкованими за спаданням часу створення.

Доступні типи

DM-Core підтримує наступні типи даних:
  • Boolean
  • String
  • Text
  • Float
  • Integer
  • BigDecimal
  • DateTime, Date, Time
  • Object
  • Discriminator

Якщо включити DM-Types, стають доступні наступні типи даних:
  • Csv
  • Enum
  • EpochTime
  • FilePath
  • Flag
  • IPAddress
  • URI
  • Yaml
  • Json
  • BCryptHash
  • Regex


Асоціації

Асоціації - це спосіб оголошення відносин між моделями. Вони надають ряд методів, які дозволяють створювати відносини та отримувати пов'язані моделі. У DataMapper є декілька видів асоціацій.

Один до багатьох(One-To-Many)

Запис(Post) потенціально може мати безліч коментарів(Comment). Така прив'язка до запису обумовлюється тим, що кожен коментар містить посилання на ідентифікатор запису, якому він належить. Тому ми повинні налаштувати one-to-many асоціацію між ними.

class Post
  has n, :comments
end

class Comment
  belongs_to :post
end

Має і належить до багатьох(Has and belongs to many або One-To-Many-Through)

Запис може належати багатьом категоріям, і кожна категорія може містити безліч записів. Це і є прикладом зв'язку "багато до багатьох", який також носить дивовижну назву "має і належить до багатьох". Схоже на те, що кожна із взаємодіючих сторін містить колекцію елементів іншої сторони. Всередині БД зв'язки "багато до багатьох" здійснюються з використанням проміжної об'єднавчої таблиці. У ній містяться пари зовнішніх ключів, які зв'язують дві задані таблиці.

class Categorization
  include DataMapper::Resource

  property :id,         Serial
  property :created_at, DateTime

  belongs_to :category
  belongs_to :post
end

class Post
  has n, :categorizations
  has n, :categories, :through => :categorizations
end

class Category
  has n, :categorizations
  has n, :posts,      :through => :categorizations
end

Перевірка даних

gem install dm-validations

DataMapper дозволяє перевіряти дані до того, як вони будуть збережені до бази даних за допомогою так званих валідаторів. Щоб зробити перевірку доступною для нашого затосунку, просто додамо рядок:
require 'dm-validations'

У DataMapper є два способи для перевірки властивостей у класі - ручна і автоматична.

Ручна перевірка

Подібно до інших Ruby ORM, ми можемо викликати метод перевірки безпосередньо передавши йому ім'я властивості(або масив імен) для перевірки:
validates_length :title
validates_length [:title, :body]

На даний момент доступні наступні перевірки:
  • validates_present
  • validates_absent
  • validates_is_accepted
  • validates_is_confirmed
  • validates_format
  • validates_length
  • validates_with_method
  • validates_with_block
  • validates_is_number
  • validates_is_unique
  • validates_within

Автоматична перевірка

Перевірку можна створити разом із оголошенням властивості у класі моделі:
# неявно створює validates_present
:nullable => false
:length => (1..n)

# неявно створює validates_length
:length => 20
:length => (1..20) # не може бути нульовим
:lenth => (0..20)  # може бути нульовим

# неявно створює validates_format
:format => :email_address
:format => /\w+_\w+/
:format => lambda {|str| str}
:format => Proc.new {|str| str}

Тепер давайте створимо клас як з ручною перевіркою, так і з автоматичною:
require 'dm-validations'

class Post
  include DataMapper::Resource

  property :title, String
  validates_length :name, :max => 30

  property :body, Text, :length => (1..5000)
end

Робота з помилками перевірки

Якщо валідатор знаходить помилку в моделі, він генерує об'єкт Validate::ValidationErrors, який доступний моделі через виклик методу #errors :
my_post = Post.new(:title => "Is It Alright?")
if my_post.save
  # my_post є дійсним і буде збережений
else
  my_post.errors.each do |e|
    puts e
  end
end

Повідомлення про помилки, які надаються DataMapper як правило чіткі і точно пояснюють, що пішло не так. Але у вас є можливість змінити їх, шляхом передання опції :message :
validates_is_unique :body, :scope => :post_id,
  :message => "There's already a comment of that body in this post"

Цей приклад демонструє використання опції :scope для перевірки унікальності властивості в обмеженій області. Об'єкт не буде дійсним, якщо інший об'єкт з таким самим post_id матиме ідентичне body.

Щось подібне також можна зробити і для автоматичної перевірки через встановлення :messages у параметрах властивості:
property :email, String, :nullable => false, :unique => true, :format => :email_address,
                         :messages => {
                           :presence => "We need your email address.",
                           :is_unique => "We already have that email."
                           :format => "Doesn't look like an email address."
                        }


Робота з даними


CRUD - (англ. create read update delete) 4 базові функції управління даними "створення, зчитування, зміна і видалення".
Стосовно СУБД:
Створити - INSERT 
Вибірка   - SELECT
Оновити  - UPDATE
Знищити  - DELETE

Для прикладу створимо новий екземпляр моделі Post, оновимо його властивості і збережемо. Якщо збереження пройде успішно функція поверне true, або false, якщо щось пішло не так.
post = Post.new
post.attributes = {:title => "Hello world!", :body => "DataMapper is awesome!"}
post.save

Є декілька способів встановлення властивостей моделі:
post = Post.new(:title => 'Pass in a hash to the new method')
post.title = 'Set individual property'
post.attributes = {:title => "Hello world!", :body => "DataMapper is flexible!"}

Також можна оновити властивості моделі і зберегти за допомогою одного методу:
post.update = {:title => "Hello world!", :body => "DataMapper is simple!"}

Щоб знищити запис, ви просто викликаєте на ньому метод #destroy!. Він поверне true або false, в залежності від того чи запис був видалений. Ось приклад знаходження існуючого запису, та його знищення:
post = Post.get(3)
post.destroy! #=> true

Методи пошуку для об'єктів DataMapper визначені в DataMapper::Repository. Вони включають get(), all(), first().
DataMapper має методи, які дозволяють отримати:
  • один запис по ключу;
  • перший запис, що збігається з набором умов;
  • множину записів, які відповідають умовам.

post = Post.get(1)                          # отримати запис із первинним ключем 1
post = Post.get!(1)                         # аналогічно get(), але у випадку невдачі поверне ObjectNotFoundError
post = Post.first(:title => 'Hello world!') # перший запис із заголовком 'Hello world!'
posts = Post.all                            # всі записи
posts = Post.all(:published => true)        # всі опубліковані записи

Методи all() і first() можуть бути з'єднані в ланцюжок для подальшої побудови запиту до бази даних:
all_posts = Post.all
published_posts = all_posts.all(:published => true)

Як прямий наслідок цього:
class Post
  def self.published
    all(:published => true)
  end
end

published_posts = Post.published

Замість визначення умов, використовуючи SQL, можна вказувати умови, використовуючи хеш значень.
Наведені нище приклади є досить простими, ви буде здивовані, як ви можете визначити умови рівності не вдаючись до SQL.
posts = Post.all(:created_at.gt => 2, :created_at.lt => 10)
# SQL: 'created_at > 2 AND created_at < 10'

Доступні символи операцій для умов:
gt    # більше
lt    # менше
gte   # більше або рівне
lte   # менше або рівне
not   # не рівне
eql   # рівне
like  # схоже
in    # міститься - використовується автоматично, якщо в якості аргументу переданий масив

Щоб визначити порядок, в якому повинні бути відсортовані результати, використовується:
posts = Post.all(:all => [:created_at.desc]
# SQL: select * from posts ORDER BY created_at DESC)
Сортувати можна за зростанням(asc) і за спаданням(desc).

Крім того DataMapper підтримує й інший синтаксис для умов:
posts = Post.all(:conditions => {:id => 1})
posts = Post.all(:conditions => ["id = ?", 1])
posts = Post.all(:conditions => {:id => 1}, :title.like => '%foo%')

Іноді потрібно зробити запит до БД вручну:
posts = repository(:default).adapter.query('SELECT title, body FROM posts WHERE published = 1')
Зауважте, така форма запиту поверне об'єкт Struct, а не Post.

Ну і на сам кінець варто згадати конструкцію before, яка може стати у нагоді тоді, коли потрібно виконати якусь дію з даними до того як вони будуть збережені у базу.
Доступні події:
  • create
  • update
  • save
  • destroy
Для прикладу:
class Post
  include DataMapper:Resource

  before :save, :format_body

  def format_body
    # ... деякий код
  end
end

class Comment
  include DataMapper:Resource

  before :save do
    # ... деякий код
  end
end

Спочатку я планував вкластися в одну статтю, але практика показала, що навіть двох мало. Тому чекайте продовження, в якому ми спробуємо написати свій затишний блог на Sinatra по мотивах бестселера "Creating a weblog in 15 minutes with Rails".

Що почитати на дозвіллі?


Користуючись нагодою, вітаю співвітчизників з Днем української писемності та мови.
UA DAY

2 коментарі:

Паша сказав...

Тільки вирішив, щось написати на sinatra з використанням datamapper і тут ваша стаття, дуже дякую, тепер буде легше розібратися.
Щодо статті про блог, то мені хотілось би почути поради щодо організації файлів у проекті(тому що в rails є чітка структура, а в sinatra мені поки що не зрозуміло де і що має бути) та про конфігурацію(бачив в різних проектах використовують різний підхід).

Anton Maminov сказав...

Якщо хочеться чіткої структури каталогів поверх Sinatra, рекомендую подивитися на Padrino (http://www.padrinorb.com)