I would like to have one images table (which contains a file:string field), and something like this model structure

 class BookCover mount_uploader :file, BookCoverUploader end class Book has_many :covers, class: 'BookCover' end class UserAvatar mount_uploader :file, UserAvatarUploader end class User has_one :avatar, class: 'UserAvatar' end 

How to achieve this, what to use? Single table inheritance or polimorphic associations? I would like to easily catch images with certain user or book (for example, delete without imageable_id ). How would you organize the tables?

Update

In theory, this should work, right?

 # прием отсюда https://maulanaruby.wordpress.com/2007/02/17/sti-vs-polymorphic-association/ class CreateImages < ActiveRecord::Migration[5.0] def change create_table :images do |t| t.string :file, null: false t.string :type, null: false t.integer :source_id, null: false t.timestamps end end end class Image < ActiveRecord::Base end class Cover < Image mount_uploader :file, CoverUploader belongs_to :book, foreign_key: 'source_id' end class Avatar < Image mount_uploader :file, AvatarUploader belongs_to :user, foreign_key: 'source_id' end class Book < ActiveRecord::Base has_many :covers end class User < ActiveRecord::Base has_one :avatar end 
  • Read my answer. And no, in the above code you have at least forgotten about inheritance in a couple of places. - D-side
  • Oh, yes, yes, I made a mistake, and so? - srghma

1 answer 1

What you want to do (perhaps in vain, but more on that later) is really done by a polymorphic association . A table is made in which the "foreign key" (not real, but which one is) is represented by two fields: штука_type and штука_id , and Rails uses them when specified in the model:

 belongs_to :штука, polymorphic: true 

And the opposite has_many or has_one is set, respectively, with as: :штука (since everything fits the polymorph, guess what name to select, not what, you need to specify the name explicitly).

But in general, polymorphs are rather dirty. Control of data consistency for them at the database level will be very difficult to organize (if at all possible - I haven’t yet seen RDBMSs that allow making a foreign key not on the whole table, but only on a small subsample), and you can’t even make complex queries involving this association try.


Why not STI?

If you match one and only one owner class for each type value, you will get a crutch in the form of a polymorph, where instead of штука_type will be just type , and there will be not directly the type of owner, but something from which you can recognize it.

Very similar to the device on polymorph and all its shortcomings, too.


Perhaps it is better to throw out UserAvatar as an unnecessary entity?

If at any moment in time for each User record you expect one and only one UserAvatar record, then these two entities should be made into parts of one. Why not make the user's avatar a field right inside the model?

And a slightly more general rule: if two entities are in a 1-to-1 relationship, think about whether they should not really be one entity?


A field for terrible experiments: PostgreSQL and its inheritance

If you don’t have PostgreSQL, you can stop reading :)

If you really, really want to be able to climb behind the pictures of all the models at once, you can make a knight's move and use the inheritance not of ActiveRecord, but right at the database. PostgreSQL has it, here is an example of migration and models:

 class CreateImages < ActiveRecord::Migration[5.0] def change # Базовая таблица с общими полями # vvvvvvvvv ВАЖНО create_table :images, id: false do |t| t.string :url end # Просто модели create_table :apples do |t| t.timestamps end create_table :oranges do |t| t.timestamps end # А вот тут самое интересное! create_table :apple_images, options: "INHERITS (images)" do |t| t.references :apple, foreign_key: true end create_table :orange_images, options: "INHERITS (images)" do |t| t.references :orange, foreign_key: true end end end 

It is worth noting that:

  • schema is exported to losses in schema.rb , inheritance is successfully lost, you need to add export directly to SQL in config/application.rb

      config.active_record.schema_format = :sql` 
 class Image < ApplicationRecord end class Apple < ApplicationRecord has_many :apple_images end class AppleImage < ApplicationRecord belongs_to :apple end class Orange < ApplicationRecord has_many :orange_images end class OrangeImage < ApplicationRecord belongs_to :orange end 
  • The table under Image no primary key. And, unfortunately, it cannot be, because PostgreSQL does not support the creation of a unique key (and the primary key is unique, by definition) on the base table. And as a result, there is no way to update individual lines of the table in the usual ActiveRecord way, and there will also be no support for callbacks in which files can be deleted.

But you can make requests in general for all the pictures, if desired. And adding fields to images will add them to all tables inherited.

See if this solution can help you. At a minimum, it maintains control of consistency with foreign keys. As a workout, you can even add a foreign key column to the base table, and create foreign keys themselves in successor tables.

  • one
    Only the противоположный has_many или has_one is specified not with polymorphic: true but with as: :штука . (if I understood correctly what the term противоположный means) - anoam
  • one
    @anoam yes, I forgot, right, he must take the name from somewhere .-. - D-side
  • one
    @BjornMelgaard you read the answer, but clearly did not understand. Why do you need UserAvatar if it consists of one field and always belongs to exactly one user? What prevents to put the avatar field directly to the user, and to use the table of covers only for covers without polymorph and STI in general? - D-side
  • one
    @BjornMelgaard and why not immediately erase the pictures, in which the owner disappears, and avoid not only solving the problem, but also its occurrence? If you place the avatar's field right in the user, loading a new file on top should cause the removal of the old one. And if you have a field relating to all the pictures and you are not at all interested in concern about the consistency of data at the database level, you can do so. Is it worth it, you decide, but I would say that in 90% + cases is not worth it. - D-side
  • one
    @BjornMelgaard you can check the performance yourself :) Nothing that will interfere with the work, I do not see. But it contains all the above disadvantages. I am now looking to see if your case can be solved using inheritance in a postgres, more from personal interest, but this will also be a scary hack, although not as scary as polymorph and STI :) - D-side