Ruby Forum Rails Germany > Ternäre Beziehungen in Model u nd View

Posted by Michael Schuerig (Guest)
on 19.04.2008 02:36
(Received via mailing list)
Ich möchte eine Beziehung zwischen Model-Klassen modellieren, die
ähnlich wie die folgende ist. Es gibt die Klassen Compiler und System.
Ein Compiler läuft auf bestimmten Systemen und kann *davon abhängig*
Code für bestimmte Zielsysteme erzeugen.

Relational gedacht würde ich das so angehen

  create_table :compiler_host_targets, :id => false do |t|
    t.integer :compiler_id, :null => false
    t.integer :host_system_id, :null => false
    t.integer :target_system_id, :null => false
  end
  add_index :host_guest_virtualizations,
    [:compiler_id, :host_system_id, :target_system_id],
    :unique => true

Damit kann ich in Rails aber nicht viel anfangen, ich muss die
Beziehung "modelisieren" (und darf dabei den Primary Key (id) natürlich
nicht weglassen).

  class CompilerHostTarget < ActiveRecord::Base
    belongs_to :compiler
    belongs_to :host_system, :class_name => 'System'
    belongs_to :target_system, :class_name => 'System'
  end

Da ich das von der Klasse Compiler aus verwenden möchte, muss die etwa
so aussehen

  class Compiler < ActiveRecord::Base
    has_many :host_targets, :class_name => 'CompilerHostTarget' do
      def mappings
        self.inject(Hash.new { |h, k| h[k] = [] }) do |memo, mpg|
          memo[mpg.host_os] << mpg.guest_os
          memo
        end
      end
    end
  end

Für die Anzeige habe ich damit alles was ich brauche

<ul>
<% compiler.host_targets.mappings.each do |host, targets| -%>
  <li>
    Host: <%= host.name %>
    Targets: <%= targets.to_sentence %>
  </li>
<% end -%>
</ul>

Leider bin ich damit noch ein gutes Stück davon entfernt, diese
Beziehungen auch bearbeiten zu können. Dabei ist das geringere Problem
per Ajax oder purem JavaScript Formularelemente einzufügen und zu
entfernen. Wozu mir die passende Eingebung fehlt, ich noch nicht einmal
sicher bin, ob es überhaupt geht, das ist, wie ich die Elemente --
Selects für Hosts, Checkboxen für Targets -- benennen muss, damit ich
einen Params-Hash bekomme, der so aussieht:

  { "compiler" => {
      "host_targets" => [
        { "host_id" => "1", "target_ids" => ["1", "2"] },
        { "host_id" => "2", "target_ids" => ["2"] }
      ]
    }
  }

Wobei auch hier die host_ids nicht das Problem sind.

  <select name="compiler[host_targets][][host_id]">
    ...
  </select>

funktioniert wie gewünscht. Für die target_ids könnte man es so

  <input type="checkbox" value="1"
    name="compiler[host_targets][][target_ids][]" />

versuchen -- und scheitern. Denn dabei geht die Zuordnung von Targets zu
bestimmten Hosts verloren. Es kann so also nicht funktionieren und
entsprechend merkwürdig sieht der Params-Hash aus, den Rails in einem
Beispielfall mit durchaus selektierten Checkboxen produziert hat

  { "compiler" => {
      "host_targets" => [
        { "target_ids" => [], "host_id" => "1" },
        { "target_ids" => [] },
        { "target_ids" => [], "host_id" => "2" },
        { "target_ids" => [] },
        { "target_ids" => [] },
        { "target_ids" => [] }
      ]
    }
  }

Soweit der Schadensbericht. Wenn mir nun jemand erklären könnte, wie es
geht...

Michael

--
Michael Schuerig
mailto:michael@schuerig.de
http://www.schuerig.de/michael/
Posted by Björn Großmann (Guest)
on 19.04.2008 09:56
(Received via mailing list)
Hallo Michael,
ich hab mich nicht weiter damit beschäftig, aber es gibt (mindestens) 
ein
Plugin für zusammengesetzte Primärschlüssel:
z.B. http://compositekeys.rubyforge.org/
Vielleicht ist das ein erster Ansatz? Schau Dir mal den Code an, evtl.
sollte man da auch vorsichtig sein :-)
Evtl. gibts im Umfeld des LInks auch weitere Lösungsansätze für Deine
Wünsche.

Schöne Grüße,

Björn
Posted by Michael Schuerig (Guest)
on 20.04.2008 18:54
(Received via mailing list)
On Saturday 19 April 2008, Michael Schuerig wrote:
> Leider bin ich damit noch ein gutes Stück davon entfernt, diese
> Beziehungen auch bearbeiten zu können. Dabei ist das geringere
> Problem per Ajax oder purem JavaScript Formularelemente einzufügen
> und zu entfernen.

Für bestimmte Werte von "gelöst", habe ich das Problem inzwischen so
gelöst:

Ich generiere zufällige Indizes, die allein dazu dienen, das Host-Select
und die Target-Checkboxen einander zuzuordnen.

  <select
    name="compiler[host_targets][120870757932348][host_id]">
    ...
  </select>

  <input type="checkbox"
    name="compiler[host_targets][120870757932348][target_ids][]" />

In der Compiler-Model-Klasse sieht das dann so aus:

  class Compiler < ActiveRecord::Base
    has_many :host_targets, :class_name => 'CompilerHostTarget',
      :dependent => :delete_all do
      def mappings
        self.inject(Hash.new { |h, k| h[k] = [] }) do |hash, mpg|
          hash[mpg.host_os] << mpg.guest_os
          hash
        end
      end
    end

    def host_targets=(host_targets_params)
      # Loesche *alle* bisherigen host-target-Kombinationen
      # fuer diesen Compiler
      host_targets.clear
      mappings = merge_host_target_mappings(host_targets_params)
      # Fuege alle jetzt gueltigen host-target-Kombinationen
      # fuer diesen Compiler ein
      mappings.each do |host_id, target_ids|
        target_ids.each do |gid|
          host_targets.build(:host_os_id => host_id,
            :target_os_id => gid)
        end
      end
    end

    private

    def merge_host_target_mappings(host_targets_params)
      host_targets_params.values.inject(Hash.new { |h, k| h[k] = [] })
do |hash, host_targets|
        host_id = host_targets[:host_id].to_i
        unless host_id == 0
          hash[host_id] |=
Array(host_targets[:target_ids]).map(&:to_i).reject(&:zero?)
        end
        hash
      end
    end
  end

Das funktioniert so nur, weil die CompilerHostTarget-Objekte keine
Identität unabhängig von ihrer eindeutigen Wertkombination haben, es
sind reine Wertobjekte.

Nach wie vor würden mich Verbesserungsvorschläge interessieren.

Michael

--
Michael Schuerig
mailto:michael@schuerig.de
http://www.schuerig.de/michael/