rom-rb / rom

Data mapping and persistence toolkit for Ruby
https://rom-rb.org
MIT License
2.08k stars 161 forks source link

Top-level associations DSL #656

Closed solnic closed 3 years ago

solnic commented 3 years ago

Originally, you could only define associations within schema blocks. This is no longer needed due to automatic resolution of runtime objects, including schemas and their attributes. So now we can define associations anywhere, the most obvious place is a relation itself of course.

To make this possible I had to port schema and associations DSLs to the new component-based DSL. Backward-compatibility is maintained via rom/compat.

Bonus: improved plugins

This refactoring introduced additional challenge because of the way how schema DSL plugins worked. To make things better and easier to manage, plugins are now created per component with their own configurations. This is actually a pretty significant improvement because it allows you to write plugins for any component type and you have an isolated context with plugin's options and its target component configuration available to you.

This means that for example the customized Schema DSL in rom-sql can now be turned into a tiny plugin rather than having a full-blown Schema::DSL subclass and having to deal with it during setup.

Example

require "rom"
require "rom/core"
require "rom/types"
require "rom/compat"
require "rom/sql"
require "rom-changeset"

require "dry-monitor"

module Types
  include ROM::Types
end

rom = ROM::Runtime.new(:sql, "sqlite::memory") do |runtime|
  runtime.relation(:users) do
    schema do
      attribute :id, Types::Integer, primary_key: true
      attribute :name, Types::String
    end

    associations do
      has_many :tasks
    end
  end

  runtime.relation(:tasks) do
    schema do
      attribute :id, Types::Integer, primary_key: true
      attribute :user_id, Types.ForeignKey(:users)
      attribute :title, Types::String
    end

    associations do
      belongs_to :user
    end
  end
end

rom, time = Dry::Monitor::CLOCK.measure { ROM.runtime(rom) }

puts "rom loaded in #{time}ms"
# rom loaded in 0ms :D

rom.gateways[:default].auto_migrate!(rom, inline: true)

users = rom.relations[:users]
tasks = rom.relations[:tasks]

user = users.changeset(:create, name: "Jane").commit
# {:id=>1, :name=>"Jane"}

task = tasks.changeset(:create, title: "Do Something").associate(user, :user).commit
# {:id=>1, :user_id=>1, :title=>"Do Something"}