Table of Contents
Wstęp
W pewnym projekcie nad którym ostatnio pracuję, postanowiłem skorzystać z UUIDów (co to są UUIDy dowiesz się tutaj) zamiast wykorzystywać domyślne ID'ki. Realizacja tego zadania w Railsach wymaga zastosowania pewnych niewielkich zabiegów, jednak całość jest całkiem prosta.
Generowanie UUIDów
Do generowania UUIDów wykorzystamy gem uuidtools:
gem 'uuidtools'
Generowanie odbywa się z pomocą poniższej metody:
UUIDTools::UUID.random_create.to_s
Migracje
Aby wykorzystać UUIDy, zamiast standardowego pola ID skorzystamy z pola (a jakże) UUID. Aby tego dokonac, wyłączymy całkowicie generowanie IDków dla migracji. Stworzymy w tym celu klasę bazową dla naszych migracji (można oczywiście deklarować to w każdej migracji z osobna ale komu się chce). Tworzymy w katalogu db/ plik uuid_migration.rb zawierający jedną metodę (migracja ta jest migracją bazową więc nie ma up, down czy też (w R3.1) change:
class UuidMigration < ActiveRecord::Migration def create_uuid_table(name, options = {}) create_table name, options.merge(:id => false) do |t| t.string :uuid, :limit => 36, :primary => true yield t t.timestamps end add_index name, :uuid, 'UNIQUE' end end
Od teraz możemy wykorzystać naszą migrację edytując migracje danych modeli, np:
require File.expand_path(File.dirname(__FILE__))+'/../uuid_migration.rb' class CreateProducts < UuidMigration def change create_uuid_table :products do |t| t.string :name, :null => false t.float :price, :null => false t.timestamps end end end
Taka migracja spowoduje, że oprócz pól zadeklarowanych powyżej, powstanie także (automatycznie) pole uuid oraz timestampy. Tworząc dużo modeli opartych na UUIDach, taka metoda jest wygodniejsza (nie zapomnimy np. przez przypadek wyłączyć automatycznie tworzonego pola id).
Klasa bazowa dla naszych modeli
Zanim zaczniemy, chciałbym zaznaczyć, że wykorzystamy klasę bazową zamiast modułu tylko dlatego, że w systemie nad którym pracuję, występuje takie rozwiązanie (powody pozostawiam dla siebie ;) ). Niemniej jednak, nic nie stoi na przeszkodzie aby przerobić rozwiązanie zaprezentowane poniżej wersję modułową.
W gruncie rzeczy cały "myk" sprowadza się do informowania klas o tym że zamiast id używamy uuida oraz do jego inicjowania. Inicjować uuida będziemy zaraz przed zapisem obiektu do bazy (before_create):
require 'uuidtools' class UuidRecord::Base < ActiveRecord::Base self.abstract_class = true self.primary_key = :uuid before_create :generate_uuid private def generate_uuid self.uuid = UUIDTools::UUID.random_create.to_s end end
Tworzymy klasę
Po utworzeniu naszej migracji oraz innych potrzebnych fragmentów kodu, możemy przystąpić do utworzenia naszej pierwszej klasy:
class Product < UuidRecord::Base set_table_name :products set_primary_key :uuid end
Wadą "uuidowania" jest to, że musimy deklarować nazwę tabeli (jeśli dziedziczymy, jako moduł includowany już nie), oraz że w każdym modelu (mimo że ustawiliśmy w nadrzędnym), musimy deklarować, że uuid jest naszym kluczem głównym:
set_table_name :products set_primary_key :uuid
Relacje pomiędzy obiektami
Opisując wykorzystanie UUIDów, warto wspomnieć także o tym, jak przebiega stosowanie ich w relacjach has, belongs_to, itd. Ogólnie rzecz ujmując, to przebiega... tak samo. Ale... należy pamiętać o tym, że nie mamy pola ID! Dlatego deklarując relacje, musimy podawać klucz obcy oraz klucz główny:
belongs_to :product, :primary_key => :uuid, :foreign_key => :product_uuid has_many :ordereds, :primary_key => :uuid, :foreign_key => :order_uuid
Tyle jeśli chodzi o UUIDy. Ich wykorzystanie nie jest trudne, pod warunkiem, że będziemy pamiętać o tym co napisałem powyżej.
June 13, 2011 — 14:40
Jaki jest zysk z tego, że nazwiemy to pole na bazie :uuid zamiast :id ?
Czy nie mogliście skorzystać z generowania uuid’ów po stronie bazy danych ?
June 13, 2011 — 14:47
1. Zysk żaden (pole to pole) ale – system nad którym pracuję podpięty jest pod kilka baz danych i chciałem wyraźnie rozgraniczyć kiedy mam do czynienia z UUIDami a kiedy ze zwykłymi ID, z tego względu pola które mają UUIDy nazywam z koncówką “_uuid” oraz tworzę odpowiednie klucze główne również według tej konwencji.
2. Mogłem ale wtedy idea niezależności od bazy (ORM) byłaby troszkę naruszona. Uważam, że Railsy same przez się “lubią” agnostycyzm i nie chcę go naruszać. Dodatkowo ktoś kto kiedyś będzie modyfikował mój kod łatwiej zrozumie skąd się biorą UUIDy (kto z nas przegląda pliki migracji jeśli nie musi ;) ).
June 13, 2011 — 15:06
1. A czym się różni ID od UUID, że trzeba to jakoś specjalnie zaznaczać ? Skoro żaden zysk to po co tracić na tym, że wszędzie później trzeba w opsiach relacji zmieniać ? Poza tym wydaje mi się, że nie jest to konieczne by w relacjach podawać :primary_key i :foreign_key gdyż railsy wezmą to z Product.primary_key oraz Order.primary_key. W railsach3 chyba out of box to działa. Próbowałeś ?
2. To tak jakby twierdzić, że autoincrement id powoduje naruszenie agnostycyzmu gdyż to baza danych nam zwraca id utworzonego obiektu. BTW: Jesteś pewny, że tak bardzo potrzebujesz niezależności od bazy ?
June 13, 2011 — 15:23
Zysk jest w logice. Inna część systemu (nie w Rubym) wykorzystuje UUIDy i IDki (z różnych baz) i twórcy tamtej części chcą mieć rozróżnienie. Ja zresztą się z tym zgadzam. W większości wypadków nie ma to znaczenia (bo UUID też jest IDkiem)
Out of box nie chciało działać (może coś pomieszałem ;) ) – mimo że ustawiłem jako klucz główny dla modelu. Może w 3.1 to się zmieni. Póki co nie uważam żeby te 2 linijki były aż takim problemem.
2. Masz trochę racji z tym ID ;) – nad UUID() z MySQLa się zastanawiałem, stwierdziłem jednak tak jak pisałem wyżej, że musi być agnostycznie. Czy potrzebuję agnostycyzmu? Tak. System będzie działał z kilkoma bazami (w tym najprawdopodobniej z MSSqlem) i nie chcę stosować “wprost” rzeczy które mogą być charakterystyczne dla jednej DB. Jeśli zapadłaby decyzja dot. pozostawienia jednego systemu bazodanowego to możliwe że przeniósłbym się na UUID(). Jak znajdę chwilkę to sprawdzę jak prezentuje się to pod względem wydajności.