web-dev-qa-db-fra.com

Rails Jointure et inclut les colonnes de la table des jointures

Je ne comprends pas comment obtenir les colonnes que je veux de Rails. J'ai deux modèles - un utilisateur et un profil. Un utilisateur: profil has_many (car les utilisateurs peuvent revenir à une version antérieure de leur profil):

> DESCRIBE users;
+----------------+--------------+------+-----+---------+----------------+
| Field          | Type         | Null | Key | Default | Extra          |
+----------------+--------------+------+-----+---------+----------------+
| id             | int(11)      | NO   | PRI | NULL    | auto_increment |
| username       | varchar(255) | NO   | UNI | NULL    |                |
| password       | varchar(255) | NO   |     | NULL    |                |
| last_login     | datetime     | YES  |     | NULL    |                |
+----------------+--------------+------+-----+---------+----------------+

> DESCRIBE profiles;
+----------------+--------------+------+-----+---------+----------------+
| Field          | Type         | Null | Key | Default | Extra          |
+----------------+--------------+------+-----+---------+----------------+
| id             | int(11)      | NO   | PRI | NULL    | auto_increment |
| user_id        | int(11)      | NO   | MUL | NULL    |                |
| first_name     | varchar(255) | NO   |     | NULL    |                |
| last_name      | varchar(255) | NO   |     | NULL    |                |
|      .                .          .      .       .             .       |
|      .                .          .      .       .             .       |
|      .                .          .      .       .             .       |
+----------------+--------------+------+-----+---------+----------------+

En SQL, je peux exécuter la requête:

> SELECT * FROM profiles JOIN users ON profiles.user_id = users.id LIMIT 1;
+----+-----------+----------+---------------------+---------+---------------+-----+
| id | username  | password | last_login          | user_id | first_name    | ... |
+----+-----------+----------+---------------------+---------+---------------+-----+
| 1  | john      | ******   | 2010-12-30 18:04:28 | 1       | John          | ... |
+----+-----------+----------+---------------------+---------+---------------+-----+

Voyez comment j'obtiens toutes les colonnes des DEUX tables JOINTES ensemble? Cependant, lorsque j'exécute cette même requête dans Rails, je n'obtiens pas toutes les colonnes que je veux - je n'obtiens que celles de Profile:

# in Rails console
>> p = Profile.joins(:user).limit(1)
>> [#<Profile ...>]
>> p.first_name
>> NoMethodError: undefined method `first_name' for #<ActiveRecord::Relation:0x102b521d0> from /Library/Ruby/Gems/1.8/gems/activerecord-3.0.1/lib/active_record/relation.rb:373:in `method_missing' from (irb):8
# I do NOT want to do this (AKA I do NOT want to use "includes")
>> p.user
>> NoMethodError: undefined method `user' for #<ActiveRecord::Relation:0x102b521d0> from /Library/Ruby/Gems/1.8/gems/activerecord-3.0.1/lib/active_record/relation.rb:373:in method_missing' from (irb):9

Je veux renvoyer (efficacement) un objet qui a toutes les propriétés de Profile et User ensemble. Je ne veux pas: inclure l'utilisateur parce que cela n'a pas de sens. L'utilisateur doit toujours faire partie du profil le plus récent comme s'il s'agissait de champs dans le modèle de profil. Comment est-ce que j'accomplis ceci?

Je pense que le problème a quelque chose à voir avec le fait que le modèle de profil n'a pas d'attributs pour l'utilisateur ...

29
sethvargo

Je ne pense pas que vous puissiez charger des utilisateurs et des profils avec join dans Rails. Je pense que dans les versions précédentes de Rails (<2.1) le chargement des modèles associés était fait avec des jointures, mais ce n'était pas efficace. Ici vous avez des explications et des liens à d'autres matériaux.

Donc, même si vous dites explicitement que vous souhaitez le rejoindre, Rails ne le mappera pas aux modèles associés. Donc, si vous dites Profile.whatever_here il sera toujours mappé à l'objet Profile.

Si vous voulez toujours faire ce que vous avez dit en question, vous pouvez appeler vous-même une requête SQL personnalisée et traiter les résultats:

p = ActiveRecord::Base.connection.execute("SELECT * FROM profiles JOIN users ON profiles.user_id = users.id LIMIT 1")

et obtenez les résultats ligne par ligne avec:

p.fetch_row

Il sera déjà mappet à un tableau.

Vos erreurs sont dues au fait que vous appelez first_name et user méthode sur AciveRecord::Relation objet et il stocke un tableau d'objets Profile, pas un seul objet. Alors

p = Profile.joins(:user).limit(1)
p[0].first_name

devrait travailler.

La meilleure façon de récupérer un seul enregistrement est d'appeler:

p = Profile.joins(:user).first
p.first_name
p.user

Mais quand vous appelez p.user il interrogera la base de données. Pour l'éviter, vous pouvez utiliser include, mais si vous ne chargez qu'un seul objet de profil, il est inutile. Cela fera une différence si vous chargez plusieurs profils à la fois et souhaitez inclure le tableau des utilisateurs.

11
klew

Utilisez select () pour nommer les colonnes souhaitées. Au moins, cela fonctionne dans Rails 3.0.9.

Contexte: mon application a une table principale nommée: droits. Je voulais être en mesure d'attribuer une étiquette et une couleur à un enregistrement donné: droit afin de pouvoir le retirer facilement d'une liste d'index. Cela ne correspond pas parfaitement à l'image Rails des enregistrements associés; la plupart: les droits ne seront jamais balisés et les balises sont complètement arbitraires (saisie par l'utilisateur via balise/modifier).

Je pourrais essayer de dupliquer les données de balise dans l'enregistrement: right, mais cela viole la forme normale. Ou je pourrais essayer d'interroger: balises pour chaque: bon enregistrement, mais c'est une approche douloureusement inefficace. Je veux pouvoir rejoindre les tables.

La console MySQL affiche:

mysql> describe rights;
+------------+---------------+------+-----+---------+----------------+
| Field      | Type          | Null | Key | Default | Extra          |
+------------+---------------+------+-----+---------+----------------+
| id         | int(11)       | NO   | PRI | NULL    | auto_increment |

  ...

| Tagid      | int(11)       | YES  |     | NULL    |                |
+------------+---------------+------+-----+---------+----------------+

mysql> describe tags;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| TagName    | varchar(255) | YES  |     | NULL    |                |
| TagColor   | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

Je vais utiliser TagName et TagColor dans views/rights/index.html.erb, donc je veux que le contrôleur de droits inclue ces colonnes dans l'objet @rights qu'il passe à la vue. Comme tous les: right n'ont pas de balise:, je veux utiliser une jointure externe:

@rights = Right.joins("LEFT OUTER JOIN tags ON rights.Tagid = tags.id")

Mais, comme tout le monde l'a trouvé, cela ne fonctionne pas à lui seul: une référence de bloc à TagName produit une erreur de serveur. Cependant, si j'ajoute une sélection à la fin, tout va bien:

@rights = Right.joins("LEFT OUTER JOIN tags ON rights.Tagid = tags.id").select("rights.*,tags.TagName as TagName,tags.TagColor as TagColor")

Note ajoutée le 6/7/13: la clause select ne nécessite pas d'alias - cela fonctionne aussi:

.select("rights.*,tags.TagName,tags.TagColor")

Maintenant, je peux référencer TagName et TagColor à mon avis:

<% @rights.each do |right| %>
  <tr ALIGN=Left <%=
  # color background if this is tagged
  " BGCOLOR=#{right.TagColor}" if right.TagColor
  %> > ...
<% end %>
18
Lex Lindsey

Essayez d'utiliser select("*").joins(:table)

Dans ce cas, vous devez taper:

User.select("*").joins(:profile)

J'espère que cela fonctionne pour vous.

4
snoopy0123

Après avoir lu ces conseils, j'ai obtenu que les jointures soient toutes chargées dans une seule requête en lisant façons de faire un chargement (préchargement) rapide en Rails 3 & 4 .

J'utilise Rails 4 et cela a fonctionné comme un charme pour moi:

refs = Referral.joins(:job)
          .joins(:referee)
          .joins(:referrer)
          .where("jobs.poster_id= ?", user.contact_id)
          .order(created_at: :desc) 
          .eager_load(:job, :referee, :referrer)

Voici mes autres tentatives.

#first attempt
#refs = Referral.joins(:job)
#          .where("jobs.poster_id= ?", user.contact_id)
#          .select("referrals.*, jobs.*")
# works, but each column needs to be explicitly referenced to be used later.
# also there are conflicts for columns with the same name like id

#second attempt
#refs = ActiveRecord::Base.connection.exec_query("SELECT jobs.id AS job_id, jobs.*, referrals.id as referral_id, referrals.* FROM referrals INNER JOIN jobs ON job_id = referrals.job_id WHERE (jobs.poster_id=#{user.contact_id});")
# this worked OK, but returned back a funky object, plus the column name
# conflict from the previous method remains an issue.


#third attempt using a view + Rails_db_views
#refs = JobReferral.where(:poster_id => user.contact_id)
# this worked well. Unfortunately I couldn't use the SQL statement from above
# instead of jobs.* I had to explicitly alias and name each column.
# Additionally it brought back a ton of duplicate data that I was putting
# into an array when really it is Nice to work with ActiveRecord objects.

#eager_load
#refs = Referral.joins(:job)
#          .where("jobs.poster_id= ?", user.contact_id)
#          .eager_load(:job)
# this was my base attempt that worked before I added in two more joins :)
3
David Silva Smith

J'ai contourné ce problème en créant une VUE dans la base de données qui est la jointure, puis en référençant cela comme s'il s'agissait d'une table ActiveRecord normale dans le code. C'est très bien pour extraire des données de la base de données, mais si vous avez besoin de les mettre à jour, alors vous devrez revenir aux classes de base qui représentent les tables "réelles". J'ai trouvé cette méthode très pratique lors de la création de rapports utilisant des tableaux volumineux - vous pouvez obtenir les données en une seule fois. Je suis surpris que cela ne semble pas être intégré à ActiveRecord, cela me semble évident!

Alors pour vous:

EN SQL:

CREATE VIEW User_Profiles
AS
SELECT P.*, U.first_name
FROM Users U
inner join Profiles P on U.id=P.user_id

IN Ruby file models:

class UserProfile < ActiveRecord::Base
  self.primary_key = :id 
  #same dependencies as profiles
end

** CONSEIL ... J'oublie toujours de définir le propriétaire de la vue (j'utilise des postgres), donc ça explose tout de suite avec beaucoup de malédiction et d'auto-récrimination.

2
richie