Ruby on Rails – jak vytvořit perfektní Enum v 5 krocích

Kopírovat do schránky

při spuštění projektu pravděpodobně navrhnete ERD diagram nebo podobný. Pokaždé, když vám klient předá nové požadavky, je nutné je upravit. Tento proces pomáhá pochopit konkrétní doménu a odráží realitu. Entity, které modelujete, obsahují mnoho atributů různých typů. Docela populární požadavek je Vytvořit atribut, který lze přiřadit k jedné z několika dostupných hodnot. V programování se tento typ nazývá výčet nebo jen enum.

jako příklad lze uvést typ doručení: „kurýr“, „zásilková stanice“ nebo „osobní“. Kolejnice podporuje enums od verze 4.1.

tento článek se skládá ze 3 částí:

  1. základní řešení-zavést ActiveRecord::Enum, tak jednoduché, jak je to možné
  2. 5 různé kroky ke zlepšení funkce enums
  3. konečné řešení-zabalit všechna vylepšení do jedné implementace

pro lepší pochopení tématu přidejme nějaké skutečné pozadí. V našem nedávném projektu jsme pracovali na systému souvisejícím s uměleckými díly. Artworks byly shromážděny do Catalogs. Katalog byl jedním z největších modelů v naší aplikaci. Mezi mnoha atributy jsme měli 4 typu enum.

state:

auction_type:

status:

localization:

základní řešení

přidání enum k existujícímu modelu je opravdu jednoduchý úkol. Nejprve je třeba vytvořit vhodnou migraci. Všimněte si, že typ sloupce je nastaven na celé číslo, a to je, jak Rails udržuje hodnoty enums v databázi.

rails g migration add_status_to_catalogs status:integer

class AddStatusToCatalogs < ActiveRecord::Migration def change add_column :catalogs, :status, :integer endend

dalším krokem je deklarovat atribut enum v modelu.

class Catalog < ActiveRecord::Base enum status: end

spusťte migrace a to je vše! Od této chvíle můžete využít celou řadu dalších metod.

můžete například zkontrolovat, zda je aktuální stav nastaven na určitou hodnotu:

catalog.published? # false

nebo změňte stav na jinou hodnotu:

catalog.status = "published" # publishedcatalog.published! # published

seznam všech publikovaných katalogů:

Catalog.published

Chcete-li zobrazit všechny poskytnuté metody, zkontrolujte ActiveRecord::Enum.

toto jednoduché řešení je skvělé pro začátek, ale můžete narazit na některé problémy, když váš projekt poroste. Chcete-li být připraveni, můžete implementovat několik vylepšení, která usnadní údržbu ENUM:

deklarovat enum jako Hash not Array

zranitelnost před změnou: mapování mezi deklarovanými hodnotami a celými čísly vedenými v databázi je založeno na pořadí v poli.

v tomto příkladu by mapování bylo následující:

class Catalog < ActiveRecord::Base enum localization: end

0 -> home1 -> foreign2 -> none

tento přístup není vůbec flexibilní. Představte si, že se požadavky právě změnily. Od této chvíle by měla být“ zahraniční „lokalizace rozdělena na „Ameriku“ a „Asii“. V takovém případě byste měli odstranit starou hodnotu a přidat dvě nové. Ale … nemůžete odstranit nepoužitý „cizí“ typ, protože porušuje pořadí zbývajících hodnot. Chcete-li se vyhnout této situaci, měli byste deklarovat svůj enum jako Hash. Není toho moc co dělat:

class Catalog < ActiveRecord::Base enum localization: { home: 0, foreign: 1, none: 2 }end

toto prohlášení nezávisí na objednávce, takže budete moci implementovat změny a zbavit se nevyužité hodnoty enum.

Integrovat ActiveRecord::Enum s PostgreSQL enum

zranitelnost před změnou: bezvýznamné celočíselné hodnoty v databázi.

práce s atributy reprezentujícími celá čísla v databázi může být nepříjemná. Představte si, že chcete něco dotazovat v rails console nebo dokonce potřebujete vytvořit rozsah založený na poli enum. Když se vrátíme k našemu pozadí, předpokládejme, že chceme vrátit všechny Catalogs, které jsou stále aktuální. Takže můžeme napsat where clause takto:

Catalog.where.not("state = ?", "finished")

dostali jsme chybu, jak jsme očekávali:

ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation:ERROR: invalid input syntax for integer: "finished"

k tomuto problému dochází pouze ve formátu pole where clause , protože druhá hodnota je vložena přímo do SQL where clause a zjevně „dokončeno“ není celé číslo.

podobný případ se může objevit při implementaci komplexní SQL query vynechání ActiveRecord vrstvy. Když dotaz nemá přístup k modelu, ztratíte smysluplné informace o hodnotách a zůstanete s čistými celými čísly bez smyslu. V takovém případě je třeba vynaložit další úsilí, aby tato celá čísla byla opět smysluplná.

při práci se starší databází, jako je tato, může dojít k další nepříjemné situaci. Máte přístup do databáze a Zajímají Vás pouze data, která jsou v ní uchovávána. Nejste schopni získat okamžité informace z toho, co vidíte. Vždy je třeba mapovat tato čísla do reálných hodnot z domény.

stojí za to si uvědomit, že když je integer enum rozdělen od svého modelu jako ve výše uvedených příkladech, pak bohužel ztratíme informace.

abychom vás přesvědčili ještě více, existuje také bezpečnostní bod. Při deklaraci ActiveRecord::Enum neexistuje žádná záruka, že vaše údaje budou omezeny pouze na poskytnuté hodnoty. Změny by mohly být provedeny libovolnými VLOŽENÍMI SQL. Na druhou stranu, když deklarujete PostgreSQL enum, získáte omezení na úrovni databáze. Musíte se rozhodnout, jak sebevědomý chcete být.

PostgreSQL se běžně používá jako databáze v projektech Ruby on Rails. PostgreSQL enum můžete použít jako typ atributu v databázové tabulce.

podívejme se, jak to vypadá tentokrát.

rails g migration add_status_to_catalogs status:catalog_status

musíte změnit typ atributu. Nedoporučuji vytvářet typy jako „status“. Je pravděpodobné, že se v budoucnu objeví další stav. Dále je třeba trochu změnit migraci. Především musí být reverzibilní a může spustit SQL.

prohlášení je podobné předchozímu:

class Catalog < ActiveRecord::Base enum status: { published: "published", unpublished: "unpublished", not_set: "not_set" }end

přidejte index do atributu enum

zranitelnost před změnou: výkon dotazů.

tento bod je jednoduchý. Je velmi pravděpodobné, že váš atribut enum je to, co dokáže rozlišit objekty v konkrétním modelu. Stejně jako u našeho stavu: některé Catalogs jsou publikovány a jiné nejsou. V důsledku toho bude vyhledávání nebo filtrování podle tohoto atributu poměrně častým úkolem, takže stojí za to přidat do tohoto pole index. Upravme naši migraci:

class AddIndexToCatalogs < ActiveRecord::Migration def change add_index :catalogs, :status endend

před změnou použijte předponu nebo příponu ve svých souborech

zranitelnosti:

  • neintuitivní obory
  • špatná čitelnost pomocných metod
  • náchylné k chybám

s odkazem na náš nedávný projekt jsme měli v našem modelu Catalog několik ENUM:

state:

auction_type:

status:

localization:

Chcete-li přidat předponu nebo příponu do enum, stačí přidat tuto možnost do deklarace, například:

podívejme se, proč to může být tak užitečné. V Catalog modelu máme 4 enumy a 12 hodnot mezi nimi. Vytváří 12 oborů, velmi neintuitivních oborů.

Catalog.not_setCatalog.liveCatalog.unpublishedCatalog.in_progress

můžete s lehkostí říci, co tyto metody vracejí? Ne, musíte si pořád pamatovat, jak vypadají obory. Může to být nepříjemné, opravdu.

Catalog.status_not_setCatalog.live_auction_typeCatalog.status_unpublishedCatalog.state_in_progress

to vypadá mnohem lépe.

předpokládejme, že nyní musíte do svého modelu přidat ještě jeden výčet. Měla by uchovávat informace o pořadí každého katalogu uvnitř globálního katalogu. Pořadí některých katalogů nemusí být specifikováno. Nejdůležitější je vědět, který z nich je první a který Poslední. Můžeme vytvořit další výčet:

class Catalog < ActiveRecord::Base enum order: { first: "first", last: "last", other: "other", none: "none" }end

Pojďme otevřít rails console pro testování nových oborů:

Catalog.order

máme chybu. Je to samozřejmé.

 ArgumentError: You tried to define an enum named "order" on the model "Catalog", but this will generate a class method "first", which is already defined by Active Record.

Ok, můžeme to opravit:

a znovu, další chyba:

ArgumentError (You tried to define an enum named "order" on themodel "Catalog", but this will generate an instance method"none?", which is already defined by another enum.)

Ok, to je také zřejmé. Zapomněli jsme, že hodnota „none“ byla deklarována také v jiném atributu.

Možnosti předpony nebo přípony jsou perfektní, aby se předešlo těmto druhům problémů. Můžeme deklarovat hodnoty, jak chceme, není důvod měnit slova, která jsou nejvíce popisná. V tomto přístupu, obory jsou intuitivnější a smysluplnější. Podle nového atributu by prohlášení mělo vypadat takto:

class Catalog < ActiveRecord::Base enum order: { first: "first", last: "last", other: "other", none: "none" }, _prefix: :orderend

implementujte objekt Value pro zpracování zranitelnosti enum

před změnou: Fat model

mohu doporučit extrakci atributu enum do odděleného objektu hodnoty ve dvou případech:

  1. atribut Enum se používá u mnoha modelů (alespoň 2)
  2. atribut Enum má specifickou logiku, která komplikuje model.

Ok, představme si ukázkovou situaci. V našem projektu jsou Aukční domy (kde se umělecká díla prodávají) umístěny po celé zemi. Polsko se dělí na 16 regionů, tzv. vojvodství. Každý model AuctionHouse má specifický atribut Address, který obsahuje atribut Voivodeship. Dokážete si představit, že z nějakého důvodu bude nutné uvést pouze severní Aukční domy nebo ty z nejpopulárnějších vojvodství. V takovém případě je nutné do našeho modelu vložit další logiku, díky čemuž je tlustší. Chcete-li se tomu vyhnout, můžete tuto logiku extrahovat do jiné třídy, díky čemuž je opakovaně použitelná a čistší.

class Voivodeship VOIVODESHIPS = %w(dolnoslaskie kujawsko-pomorskie lubelskie lubuskie lodzkie malopolskie mazowieckie opolskie podkarpackie podlaskie pomorskie slaskie swietokrzyskie warminsko-mazurskie wielkopolskie zachodnio-pomorskie).freeze NORTHERN_VOIVODESHIPS = %w(warminsko-mazurskie pomorskie zachodnio-pomorskie podlaskie).freeze MOST_POPULAR_VOIVODESHIPS = %w(dolnoslaskie mazowieckie slaskie malopolskie).freeze def initialize(voivodeship) @voivodeship = voivodeship end def northern? NORTHERN_VOIVODESHIPS.include? @voivodeship end def popular? MOST_POPULAR_VOIVODESHIPS.include? @voivodeship end def eql?(other) to_s.eql?(other.to_s) end def to_s @voivodeship.to_s endend

pak v odpovídajícím modelu musíte tento atribut přepsat. V našem projektu je to Address model. array_to_enum_hash je pouze pomocná metoda, která převede Array hodnot enum na Hash.

zde je to, čeho jste dosáhli. Celá logika vztahující se k vojvodství je zapouzdřena do jedné třídy. Můžete jej rozšířit, jak chcete, a Address model zůstal tlustý.

Nyní, když chcete získat atribut vojvodství, je vrácen objekt třídy Voivodeship. Toto je váš objekt hodnoty.

podívejte se, že obě vojvodství mají stejnou hodnotu, ale jako objekty nejsou stejné. Díky naší pomocné metodě můžeme zkontrolovat rovnost tímto způsobem:

voivodeship_a.eql? voivodeship_b# truevoivodeship_a.eql? voivodeship_c# false

a co je nejsilnější, můžete využít všech definovaných metod, které představují požadavky, které jsme specifikovali dříve.

voivodeship_a.northern? # truevoivodeship_a.popular? # falsevoivodeship_c.northern? # falsevoivodeship_c.popular? # false

konečným řešením

Ok, jste prošli 5 vylepšení enum. Nyní je čas shrnout všechny kroky a vytvořit konečné řešení. Jako příklad si vezměme status attribute z Catalog modelu. Implementace může vypadat takto:

generování migrace:

rails g migration add_status_to_catalogs status:catalog_status

migrace:

ValueObject:

class CatalogStatus STATUSES = %w(published unpublished not_set).freeze def initialize(status) @status = status end # what you need hereend

katalogový model & prohlášení enum:

závěr

to je vše – 5 kroků k vybudování lepší implementace ENUM v kolejnicích.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.