2017-10-20 7 views
3

Existe-t-il un moyen de créer et d'utiliser dynamiquement des modèles dans Phoenix? J'ai une application qui stocke des métadonnées sur les tables des clients: ils définissent une poignée de champs (noms et types de colonnes), puis m'envoient des fichiers CSV pour analyser et stocker. A partir des métadonnées stockées, je voudrais générer un modèle afin que je puisse utiliser Ecto pour gérer la table client et effectuer des requêtes à son encontre. Je viens d'un environnement Django où je peux utiliser la fonction intégrée ORM et type() pour créer des modèles à la volée et les utiliser sans avoir à générer de migrations ou d'autres codes de modèles dans l'application.Modèles dynamiques dans le framework Phoenix

En Python, je (à peu près) faire:

class MetaModel(models.Model): 
    table_name = models.CharField(...) 
    model_name = models.CharField(...) 
    field_defs = JSONField(...) 

    def resolve_fields(self): 
     # takes values from `field_defs` and converts them into 
     # django field instances 

    def get_model(self): 
     class Meta: 
      app_label = 'dynamic' 
      db_table = self.table_name 
     fields = self.resolve_fields() 
     attrs = {'__module__': 'dynamic', 'Meta': Meta} 
     attrs.update(fields) 
     model = type(self.model_name, (models.Model,), attrs) 
     return model 

    def create_table(self): 
     with connection.schema_editor() as se: 
      model = self.get_model() 
      se.create_model(model) 

Avec cela, je suis en mesure de créer la table dans la base de données, puis tirer parti de l'ORM de travailler avec des données fournies par des clients. Je sais que je peux le faire avec SQL brut et utiliser Ecto pour exécuter les commandes et les requêtes, mais je voudrais le rendre plus codifié et s'appuyer sur les internes d'Ecto plutôt que d'écrire et de maintenir un tas de SQL modèles

Un conseil (même un "non, vous ne pouvez pas faire cela") est super utile. Merci!

Répondre

5

Oui, c'est possible avec Module.create/3. Il y a quelques mises en garde: vous devez choisir un nom unique pour chaque module et le code du module restera en mémoire jusqu'à ce que la VM soit redémarrée, donc vous pourriez vouloir limiter le nombre de fois que vous appelez cette fonction.

Voici une implémentation de base sur laquelle vous pouvez construire. Il vous permet de transmettre un nom de module, un nom de table et une liste de paires de noms de champs et de types.

defmodule A do 
    def go(module, table, fields) do 
    Module.create(module, quote do 
     use MyApp.Web, :model 
     schema unquote(table) do 
     unquote(for {name, type} <- fields do 
      quote do 
      field unquote(name), unquote(type) 
      end 
     end) 
     end 
    end, Macro.Env.location(__ENV__)) 
    end 
end 

A.go MyPost, "posts", [ 
    {:title, :string}, 
    {:content, :string}, 
] 

# MyPost is now just like any other Schema module in Phoenix. 

IO.inspect MyApp.Repo.all(MyPost) 

Sortie:

[debug] QUERY OK source="posts" db=2.8ms queue=0.1ms 
SELECT p0."id", p0."title", p0."content" FROM "posts" AS p0 [] 
[%MyPost{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, 
    content: "Hello from Joe", id: 1, title: "Hello"}, 
%MyPost{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, 
    content: "Hello from Mike", id: 2, title: "Hello"}, 
%MyPost{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, 
    content: "Hello from Robert", id: 3, title: "Hello"}] 
+0

C'est vraiment cool, comment feriez-vous de redéfinir un module donné? – webdeb

+1

Got it, vous pouvez simplement remplacer le module avec le même nom/atome – webdeb

+0

Ceci est excellent! Merci beaucoup! – vforgione