Korzystając z Railsów, jak mogę ustawić mój klucz podstawowy tak, aby nie był kolumną typu całkowitego?

85

Używam migracji Rails do zarządzania schematem bazy danych i tworzę prostą tabelę, w której chciałbym użyć wartości niecałkowitej jako klucza podstawowego (w szczególności ciągu). Aby oderwać się od mojego problemu, załóżmy, że istnieje tabela, w employeesktórej pracownicy są identyfikowani za pomocą ciągu alfanumerycznego, np "134SNW".

Próbowałem utworzyć tabelę w migracji w następujący sposób:

create_table :employees, {:primary_key => :emp_id} do |t|
    t.string :emp_id
    t.string :first_name
    t.string :last_name
end

To, co mi to daje, to coś, co wygląda na to, że całkowicie zignorował linię t.string :emp_idi poszedł dalej i uczynił go kolumną całkowitą. Czy istnieje inny sposób na wygenerowanie przez szyny ograniczenia PRIMARY_KEY (używam PostgreSQL) bez konieczności pisania kodu SQL w executewywołaniu?

UWAGA : Wiem, że nie najlepiej jest używać kolumn łańcuchowych jako kluczy podstawowych, więc nie udzielaj odpowiedzi, mówiąc tylko o dodaniu klucza podstawowego w postaci całkowitej. I tak mogę dodać jedno, ale to pytanie jest nadal aktualne.

Rudd Zwolinski
źródło
Mam ten sam problem i chciałbym zobaczyć odpowiedź na to pytanie. Żadna z dotychczasowych sugestii nie zadziałała.
Sean McCleary
1
Żaden z nich nie zadziała, jeśli używasz Postgres. „Enterprise Rails” Dana Chaka zawiera kilka wskazówek dotyczących używania kluczy naturalnych / kompozytowych.
Azeem.Butt
Uważaj, gdy używasz czegoś takiego jak: <! - language: lang-rb -> execute "ALTER TABLE workers ADD PRIMARY KEY (emp_id);" Mimo że ograniczenie tabeli jest ustawione poprawnie po uruchomieniu rake db:migrateDefinicja schematu wygenerowanego automatycznie nie zawiera tego ograniczenia!
pymkin

Odpowiedzi:

107

Niestety stwierdziłem, że nie da się tego zrobić bez użycia execute.

Dlaczego to nie działa

Badając źródło ActiveRecord, możemy znaleźć kod dla create_table:

W schema_statements.rb:

def create_table(table_name, options={})
  ...
  table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
  ...
end

Widzimy więc, że próbując określić klucz podstawowy w create_tableopcjach, tworzy klucz podstawowy o tej określonej nazwie (lub, jeśli nie jest określony, id). Czyni to poprzez wywołanie tej samej metody można używać wewnątrz bloku definicji tabeli: primary_key.

W schema_statements.rb:

def primary_key(name)
  column(name, :primary_key)
end

Spowoduje to po prostu utworzenie kolumny o określonej nazwie typu :primary_key . Jest to ustawione w następujący sposób w standardowych adapterach bazy danych:

PostgreSQL: "serial primary key"
MySQL: "int(11) DEFAULT NULL auto_increment PRIMARY KEY"
SQLite: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL"

Obejście problemu

Ponieważ utknęliśmy z tymi typami kluczy podstawowych, musimy użyć ich executedo utworzenia klucza podstawowego, który nie jest liczbą całkowitą (PostgreSQLserial to liczba całkowita używająca sekwencji):

create_table :employees, {:id => false} do |t|
  t.string :emp_id
  t.string :first_name
  t.string :last_name
end
execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"

Jak wspomniał Sean McCleary , model ActiveRecord powinien ustawić klucz podstawowy za pomocą set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end
Rudd Zwolinski
źródło
15
obejście sprawi, że rake db: test: clone lub rake db: schema: load utworzy emp_id jako kolumnę z liczbą całkowitą.
Donny Kurnia
18
właściwie teraz powinieneś używać self.primary_key=zamiast set_primary_key, ponieważ ta ostatnia jest przestarzała.
Gary S. Weaver
2
Oto jedyne rozwiązanie, które u mnie zadziałało. Mam nadzieję, że to pomoże innym: stackoverflow.com/a/15297616/679628
findchris
8
Problem z tym podejściem polega na tym, że po skonfigurowaniu bazy danych z schema.rb emp_idbędzie integerponownie kolumna typu. Wydaje się, że schema.rbnie można execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"w żaden sposób przechowywać .
Tintin81
4
@DonnyKurnia @ Tintin81 mozna wlaczac zrzutu schematu od schema.rbcelu structure.sqlpoprzez config.active_record.schema_formatustawienia, które mogą być :sqlalbo :ruby. guide.rubyonrails.org/v3.2.13/…
Svilen Ivanov
21

To działa:

create_table :employees, :primary_key => :emp_id do |t|
  t.string :first_name
  t.string :last_name
end
change_column :employees, :emp_id, :string

To może nie być ładne, ale efekt końcowy jest dokładnie taki, jakiego chcesz.

Austin
źródło
1
Wreszcie! To jedyne rozwiązanie, które u mnie zadziałało.
findchris
Czy musimy również określić kod do migracji w dół? czy też Railsy są wystarczająco sprytne, by wiedzieć, co zrobić po migracji w dół?
amey1908
2
W przypadku migracji w dół:drop_table :employees
Austin
6
Niestety, ta metoda pozostawia nam również schema.rbniezsynchronizowany ... zachowa :primary_key => :emp_iddeklarację, ale gdy rake db:schema:loadzostanie wywołana, tak jak na początku testów, zwróci kolumnę całkowitą. Jednak może się zdarzyć, że jeśli przełączysz się na używanie structure.sql(opcja konfiguracji), testy zachowają ustawienia i użyją tego pliku do załadowania schematu.
Tom Harrison,
18

Mam na to jeden sposób. Wykonywany kod SQL to ANSI SQL, więc prawdopodobnie będzie działać na większości relacyjnych baz danych zgodnych z ANSI SQL. Przetestowałem, że to działa dla MySQL.

Migracja:

create_table :users, :id => false do |t|
    t.string :oid, :limit => 10, :null => false
    ...
end
execute "ALTER TABLE users ADD PRIMARY KEY (oid);"

W swoim modelu zrób to:

class User < ActiveRecord::Base
    set_primary_key :oid
    ...
end

Sean McCleary
źródło
9

Wypróbowałem to w Railsach 4.2. Aby dodać niestandardowy klucz podstawowy, możesz zapisać migrację jako:

# tracks_ migration
class CreateTracks < ActiveRecord::Migration
  def change
    create_table :tracks, :id => false do |t|
      t.primary_key :apple_id, :string, limit: 8
      t.string :artist
      t.string :label
      t.string :isrc
      t.string :vendor_id
      t.string :vendor_offer_code

      t.timestamps null: false
    end
    add_index :tracks, :label
  end
end

Przeglądając dokumentację column(name, type, options = {})i czytając wiersz:

typeParametr jest zwykle jednym z migracji rodzimych typów, co jest jedną z następujących czynności:: primary_key, string,: tekst,: Integer: float: przecinku,: DateTime: czas,: datę,: binarny: Boolean .

Otrzymałem powyższe ides, jak pokazałem. Oto metadane tabeli po przeprowadzeniu migracji:

[arup@music_track (master)]$ rails db
psql (9.2.7)
Type "help" for help.

music_track_development=# \d tracks
                    Table "public.tracks"
      Column       |            Type             | Modifiers
-------------------+-----------------------------+-----------
 apple_id          | character varying(8)        | not null
 artist            | character varying           |
 label             | character varying           |
 isrc              | character varying           |
 vendor_id         | character varying           |
 vendor_offer_code | character varying           |
 created_at        | timestamp without time zone | not null
 updated_at        | timestamp without time zone | not null
 title             | character varying           |
Indexes:
    "tracks_pkey" PRIMARY KEY, btree (apple_id)
    "index_tracks_on_label" btree (label)

music_track_development=#

Z konsoli Railsów:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "apple_id"
>>
Arup Rakshit
źródło
3
Problem polega jednak na tym schema.rb, że generowany plik nie odzwierciedla stringtypu klucza podstawowego, więc podczas generowania bazy danych za pomocą loadklucza podstawowego jest tworzony jako liczba całkowita.
Peter Alfvin,
9

W Rails 5 możesz to zrobić

create_table :employees, id: :string do |t|
  t.string :first_name
  t.string :last_name
end

Zobacz dokumentację create_table .

Pavel Chuchuva
źródło
Warto zauważyć, że musiałbyś dodać jakąś before_create :set_idmetodę w modelu, aby przypisać wartość klucza podstawowego
FloatingRock
8

Wygląda na to, że można to zrobić za pomocą tego podejścia:

create_table :widgets, :id => false do |t|
  t.string :widget_id, :limit => 20, :primary => true

  # other column definitions
end

class Widget < ActiveRecord::Base
  set_primary_key "widget_id"
end

Spowoduje to, że kolumna widget_id stanie się kluczem podstawowym dla klasy Widget, a następnie wypełnienie pola podczas tworzenia obiektów zależy od Ciebie. Powinieneś być w stanie to zrobić za pomocą wywołania zwrotnego przed utworzeniem.

Więc coś w stylu

class Widget < ActiveRecord::Base
  set_primary_key "widget_id"

  before_create :init_widget_id

  private
  def init_widget_id
    self.widget_id = generate_widget_id
    # generate_widget_id represents whatever logic you are using to generate a unique id
  end
end
paulthenerd
źródło
U mnie to nie działa, przynajmniej nie w PostgreSQL. W ogóle nie określono klucza podstawowego.
Rudd Zwolinski
Hmm interesujące, że przetestowałem to z MySQL na Railsach 2.3.2 - może to też może być powiązane z wersją railsów?
paulthenerd
Nie jestem pewien - nie sądzę, że to wersja Railsów. Używam Railsów 2.3.3.
Rudd Zwolinski
Niestety rzeczywisty klucz podstawowy nie jest ustawiony dla tabeli przy użyciu tego podejścia.
Sean McCleary
2
Takie podejście działa przynajmniej z Railsami 4.2 i Postgresql. Testowałem, ale musisz użyć primary_key: truezamiast po prostu primarypodać w odpowiedzi
Anwar
8

Jestem na Railsach 2.3.5 i mój następujący sposób działa z SQLite3

create_table :widgets, { :primary_key => :widget_id } do |t|
  t.string :widget_id

  # other column definitions
end

Nie ma potrzeby: id => false.

Trung LE
źródło
przepraszam, ale widget_id został zmieniony na liczbę całkowitą, kiedy zastosuję twoją technikę ... czy masz rozwiązanie eny?
kikicarbonell
1
To już nie działa, przynajmniej nie w ActiveRecord 4.1.4. Daje następujący błąd:you can't redefine the primary key column 'widgets'. To define a custom primary key, pass { id: false } to create_table.
Tamer Shlash
4

Po prawie każdym rozwiązaniu, które mówi, że „to działało dla mnie w bazie danych X”, widzę komentarz oryginalnego plakatu, że „nie działa dla mnie na Postgresie”. Prawdziwym problemem może być obsługa Postgres w Railsach, która nie jest bezbłędna i prawdopodobnie była gorsza w 2009 roku, kiedy to pytanie zostało pierwotnie opublikowane. Na przykład, jeśli dobrze pamiętam, jeśli korzystasz z Postgres, w zasadzie nie możesz uzyskać przydatnych wynikówrake db:schema:dump .

Sam nie jestem ninja Postgres, otrzymałem te informacje z doskonałego filmu Xaviera Shay'a PeepCode na Postgres. Ten film faktycznie przeoczył bibliotekę Aarona Pattersona, myślę, że Texticle, ale mogłem źle pamiętać. Ale poza tym jest całkiem niezły.

W każdym razie, jeśli napotkasz ten problem na Postgres, sprawdź, czy rozwiązania działają w innych bazach danych. Może użyj rails newdo wygenerowania nowej aplikacji jako piaskownicy lub po prostu utwórz coś takiego

sandbox:
  adapter: sqlite3
  database: db/sandbox.sqlite3
  pool: 5
  timeout: 5000

w config/database.yml.

A jeśli możesz zweryfikować, że jest to problem z obsługą Postgres i znajdziesz rozwiązanie, prześlij poprawki do Railsów lub zapakuj swoje poprawki w klejnot, ponieważ baza użytkowników Postgres w społeczności Rails jest dość duża, głównie dzięki Heroku .

Giles Bowkett
źródło
2
Głosowanie za FUD i nieścisłością. Używam Postgres w większości moich projektów Railsowych od 2007 roku. Obsługa Postgres w Railsach jest doskonała i nie ma problemu z zrzutem schematu.
Marnen Laibow-Koser
2
z jednej strony to prawda, że ​​Postgres nie był tutaj problemem. z drugiej strony, tutaj jest błąd wywrotki schematu tylko dla pg z 2009 rails.lighthouseapp.com/projects/8994/tickets/2418, a oto kolejny rails.lighthouseapp.com/projects/8994/tickets/2514
Giles Bowkett
Wystąpił zdecydowany problem z Railsami i Postgresem związanymi z aktualizacją Postgres 8.3, w którym (poprawnie, IMO) zdecydowali się przejść na standard SQL dla literałów łańcuchowych i cytowania. Adapter Rails nie sprawdzał wersji Postgres i musiał być przez jakiś czas monitorowany.
Judson,
@Judson Ciekawe. Nigdy na to nie wpadłem.
Marnen Laibow-Koser
4

Znalazłem rozwiązanie, które działa z Railsami 3:

Plik migracji:

create_table :employees, {:primary_key => :emp_id} do |t|
  t.string :emp_id
  t.string :first_name
  t.string :last_name
end

A w modelu Employer.rb:

self.primary_key = :emp_id
shicholas
źródło
3

Sztuczka, która zadziałała dla mnie na Railsach 3 i MySQL była taka:

create_table :events, {:id => false} do |t|
  t.string :id, :null => false
end

add_index :events, :id, :unique => true

Więc:

  1. użyj: id => false, aby nie generować klucza podstawowego w postaci całkowitej
  2. użyj żądanego typu danych i dodaj: null => false
  3. dodaj unikalny indeks do tej kolumny

Wygląda na to, że MySQL konwertuje unikalny indeks w kolumnie innej niż null na klucz podstawowy!

Clark
źródło
W Railsach 3.22 z MySQL 5.5.15 tworzy tylko unikalny klucz, ale nie klucz podstawowy.
lulalala
2

musisz użyć opcji: id => false

create_table :employees, :id => false, :primary_key => :emp_id do |t|
    t.string :emp_id
    t.string :first_name
    t.string :last_name
end
BvuRVKyUVlViVIc7
źródło
U mnie to nie działa, przynajmniej nie w PostgreSQL. W ogóle nie określono klucza podstawowego.
Rudd Zwolinski
1
Takie podejście spowoduje pominięcie kolumny id, ale klucz podstawowy nie zostanie w rzeczywistości ustawiony w tabeli.
Sean McCleary
1

A co z tym rozwiązaniem,

Wewnątrz modelu pracownika, dlaczego nie możemy dodać kodu, który będzie sprawdzał unikalność w kolumnie, na przykład: Załóżmy, że pracownik jest modelem w tym, że masz EmpId, który jest ciągiem znaków, a następnie możemy dodać ": unikalność => prawda" do EmpId

    class Employee < ActiveRecord::Base
      validates :EmpId , :uniqueness => true
    end

Nie jestem pewien, czy to jest rozwiązanie, ale zadziałało.

Vijay Sali
źródło
1

Wiem, że to stary wątek, na który się natknąłem ... ale jestem trochę zszokowany, że nikt nie wspomniał o DataMapper.

Stwierdzam, że jeśli musisz odejść od konwencji ActiveRecord, stwierdziłem, że jest to świetna alternatywa. Jest to również lepsze podejście do starszych wersji i możesz obsługiwać bazę danych „tak jak jest”.

Ruby Object Mapper (DataMapper 2) zawiera wiele obietnic i również opiera się na zasadach AREL!

inżynier Dave
źródło
1

Dodanie indeksu działa dla mnie, używam MySql btw.

create_table :cards, {:id => false} do |t|
    t.string :id, :limit => 36
    t.string :name
    t.string :details
    t.datetime :created_date
    t.datetime :modified_date
end
add_index :cards, :id, :unique => true
Guster
źródło