Ruby on Rails – Sådan skaber du perfekt Enum i 5 trin

Kopier til udklipsholder

når dit projekt starter, designer du sandsynligvis ERD-diagram eller et lignende. Derefter, hver gang en klient passerer nye krav til dig, er det nødvendigt at ændre det. Denne proces hjælper med at forstå det specifikke domæne og afspejler virkeligheden. Enheder, som du model indeholder mange attributter af forskellige typer. Et ganske populært krav er at oprette en attribut, der kan tildeles en af nogle få tilgængelige værdier. I programmering kaldes denne type optælling eller bare enum.

som et eksempel kan det være en type levering: “courier”, “parcel station” eller “personal”. Rails understøtter enums fra version 4.1.

denne artikel består af 3 sektioner:

  1. grundlæggende løsning-introducer ActiveRecord::Enum, så enkel som muligt
  2. 5 forskellige trin til forbedring af Enums – funktion
  3. Ultimate solution-indpak alle forbedringer i en implementering

for bedre at forstå emnet lad os tilføje en reel baggrund. I vores nylige projekt arbejdede vi på et system relateret til kunstværker. Artworks blev indsamlet til Catalogs. Kataloget var en af de største modeller i vores app. Blandt mange attributter havde vi 4 af enum-typen.

state:

auction_type:

status:

localization:

grundlæggende løsning

tilføjelse af enum til en eksisterende model er en virkelig enkel opgave. Først og fremmest skal du oprette en passende migration. Bemærk, at kolonnetypen er indstillet til heltal, og det er sådan, Rails holder Enums-værdier i databasen.

rails g migration add_status_to_catalogs status:integer

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

næste skridt er at erklære enum attribut i modellen.

class Catalog < ActiveRecord::Base enum status: end

Kør migrationerne, og det er det! Fra nu af kan du drage fordel af hele bunken af ekstra metoder.

du kan f. eks. kontrollere, om den aktuelle status er indstillet til en bestemt værdi:

catalog.published? # false

eller ændre status til anden værdi:

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

liste over alle udgivne kataloger:

Catalog.published

for at se alle leverede metoder tjek ActiveRecord::Enum.

denne enkle løsning er fantastisk til en start, men du kan løbe ind i nogle problemer, når dit projekt vil vokse. For at være forberedt kan du implementere et par forbedringer, der gør dine enums lettere at vedligeholde:

erklære Enum som et Hash ikke-Array

sårbarhed før ændring: kortlægning mellem deklarerede værdier og heltal, der opbevares i databasen, er baseret på rækkefølge i arrayet.

i dette eksempel kortlægning ville være som følger:

class Catalog < ActiveRecord::Base enum localization: end
0 -> home1 -> foreign2 -> none

denne tilgang er slet ikke fleksibel. Forestil dig, at kravene bare har ændret sig. Fra nu af skal” udenlandsk “lokalisering opdeles i” Amerika “og”Asien”. I så fald skal du fjerne Gammel værdi og tilføje to nye. Men … du kan ikke fjerne ubrugt” fremmed ” type, fordi den overtræder en rækkefølge af resterende værdier. For at undgå denne situation skal du erklære din enum som en Hash. Der er ikke meget at gøre:

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

denne erklæring afhænger ikke af en ordre, så du vil være i stand til at implementere ændringerne og slippe af med ubrugt enum-værdi.

Integrer ActiveRecord::Enum med Postgraduate enum

sårbarhed før ændring: meningsløse heltalsværdier i databasen.

det kan være irriterende at arbejde med attributter, der repræsenterer heltal i databasen. Forestil dig, at du vil forespørge noget i rails console, eller endda skal du oprette et omfang baseret på dit enum-felt. Tilbage til vores baggrund, lad os antage, at vi vil returnere alle Catalogs, som stadig er opdaterede. Så vi kan skrive en where clause som denne:

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

vi fik en fejl, som vi forventede:

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

dette problem opstår kun i arrayformatet where clause, fordi den anden værdi sættes direkte i SQL where clause og naturligvis er “færdig” ikke et heltal.

en lignende sag kan vises, når du implementerer kompleks SQL query udelade ActiveRecord lag. Når forespørgslen ikke har adgang til modellen, mister du meningsfulde oplysninger om værdier og forbliver med rene heltal uden mening. I så fald skal du lægge en ekstra indsats for at gøre disse heltal meningsfulde igen.

en anden irriterende situation kan finde sted, når du arbejder med en ældre database som denne. Du har adgang til databasen, og du er kun interesseret i de data, der opbevares der. Du er ikke i stand til at få øjeblikkelig information fra det, du ser. Altid nødt til at kortlægge disse tal i reelle værdier fra domænet.

det er værd at huske, at når heltal enum er opdelt fra sin model som i eksemplerne ovenfor, mister vi desværre information.

for at overbevise dig endnu mere er der også sikkerhedspunkt. Når du erklærer ActiveRecord::Enum, er der ingen garanti for, at dine data kun begrænses til angivne værdier. Ændringer kan foretages ved eventuelle indsættelser af CVR. På den anden side, når du erklærer PostgreSQL enum, får du begrænsning på databaseniveau. Du skal beslutte, hvor sikker du vil være.

postgraduate er almindeligt anvendt som en database i Ruby on Rails projekter. Du kan bruge PostgreSQL enum som en type attribut i databasetabellen.

lad os se, hvordan det ser ud denne gang.

rails g migration add_status_to_catalogs status:catalog_status

du skal ændre attributtype. Jeg anbefaler ikke at oprette typer som “status”. Det er sandsynligt, at en anden status vises i fremtiden. Dernæst skal du ændre migrationen lidt. Mest af alt skal det være reversibelt og kunne udføre CCL.

erklæring svarer til den foregående:

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

Tilføj et indeks til enum attribut

sårbarhed før ændring: forespørgsler ydeevne.

dette punkt er simpelt. Det er meget sandsynligt, at din enum-attribut er, hvad der kan skelne objekter inden for en bestemt model. Ligesom med vores status: nogle Catalogs er offentliggjort, og andre er ikke. Som følge heraf vil søgning eller filtrering efter denne attribut være en ret hyppig opgave, så det er værd at tilføje et indeks til dette felt. Lad os ændre vores migration:

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

brug præfiks eller suffiks mulighed i dine enums

sårbarheder før ændring:

  • Unintuitive scopes
  • dårlig læsbarhed af hjælpermetoder
  • tilbøjelige til fejl

med henvisning til vores nylige projekt igen havde vi et par enums i vores Catalog model:

state:

auction_type:

status:

localization:

for at tilføje præfiks eller suffiks til enum er det nok at tilføje denne mulighed til erklæringen, som sådan:

lad os se, hvorfor det kan være så nyttigt. I Catalog modellen har vi 4 enums og 12 værdier blandt dem. Det skaber 12 scopes, meget uintuitive scopes.

Catalog.not_setCatalog.liveCatalog.unpublishedCatalog.in_progress

kan du med lethed sige, hvad disse metoder vender tilbage? Nej, du skal huske hele tiden, hvordan omfanget ser ud. Det kan være irriterende, virkelig.

Catalog.status_not_setCatalog.live_auction_typeCatalog.status_unpublishedCatalog.state_in_progress

det ser meget bedre ud.

lad os antage nu, at du skal tilføje en mere enum til din model. Det skal holde oplysninger om rækkefølgen af hvert katalog inde i det globale katalog. Rækkefølgen af nogle kataloger er muligvis ikke specificeret. Det vigtigste er at vide, hvilken der er først, og hvilken der er sidst. Vi kan skabe en anden enum:

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

lad os åbne rails console for at teste nye anvendelsesområder:

Catalog.order

vi har en fejl. Det er selvforklarende.

 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, vi kan ordne det:

og igen, en anden fejl:

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, det er også indlysende. Vi glemte, at værdien “Ingen” også blev erklæret i en anden attribut.

Indstillinger for præfiks eller suffiks passer perfekt til at undgå denne slags problemer. Vi kan erklære værdier, ligesom vi vil, der er ingen grund til at ændre ord, der er mest beskrivende. I denne tilgang er scopes mere intuitive og meningsfulde. Ifølge ny attribut skal erklæringen se sådan ud:

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

implementere Værdiobjekt til at håndtere en enum

sårbarhed før ændring: Fat model

jeg kan anbefale at udtrække enum attribut i adskilt værdi objekt i to tilfælde:

  1. enum attribut bruges blandt mange modeller (mindst 2)
  2. Enum attribut har specifik logik, der komplicerer en model.

Ok, lad os introducere prøve situation. I vores projekt er Auktionshuse (hvor kunstværker sælges) placeret over hele landet. Polen opdeles i 16 regioner, kaldet voivodeships. Hver AuctionHouse model har specifik Address, der indeholder Voivodeship attribut. Du kan forestille dig, at der af en eller anden grund vil være en nødvendighed at liste kun nordlige Auktionshuse eller disse fra de mest populære voivodeships. I så fald er det nødvendigt at sætte ekstra logik i vores model, hvad der gør det federe. For at undgå det kan du udtrække den logik i en anden klasse, hvilket gør den genanvendelig og renere.

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

så i din tilsvarende model skal du overskrive denne attribut. I vores projekt er det Address model. array_to_enum_hash er bare hjælper metode konvertere Array af enum værdier i en Hash.

her er hvad du opnåede. Hele logik relateret til voivodeships er indkapslet i en enkelt klasse. Du kan udvide det som du vil og Address model forblev tyk.

nu, når du ønsker at få voivodeship attribut, objektet af Voivodeship klasse returneres. Dette er dit Værdiobjekt.

se, at begge voivodeships har samme værdi, men som objekter er de ikke ens. Takket være vores hjælpermetode kan vi kontrollere ligestillingen på den måde:

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

og hvad er mest magtfulde du kan drage fordel af alle definerede metoder, der repræsenterer krav, vi har angivet tidligere.

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

Ultimate solution

Ok, du har passeret gennem 5 enum forbedringer. Nu er det tid til at opsummere alle trin og skabe ultimative løsning. Som et eksempel, lad os tage status attribute fra Catalog model. Implementering kan se sådan ud:

Migreringsgenerering:

rails g migration add_status_to_catalogs status:catalog_status

Migration:

Værdiobjekt:

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

Katalogmodel & enum – erklæring:

konklusion

det er alt-5 trin til at opbygge en bedre implementering af Enums i Skinner.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.