# File lib/pakyow/data/sources/relational.rb, line 460 def primary_id primary_key :id attribute :id, default_primary_key_type end
class Pakyow::Data::Sources::Relational
A relational data source through which you interact with a persistence layer such as a sql database, redis, or http. Defines the schema, queries, and other adapter-specific metadata (e.g. sql table).
Each adapter provides its own interface for interacting with the underlying persistence layer. For example, the sql adapter exposes Sequel::Dataset
provided by the fantastic Sequel gem.
In normal use, the underlying dataset is inaccessible from outside of the source. Instead, access to the dataset occurs through queries defined on the source that interact with the dataset and return a result.
Results are always returned as a new source instance (or when used from the app, a {Pakyow::Data::Proxy} object). Access to the underlying value is provided through methods such as one
, to_a
, and each
. (@see Pakyow::Data::Container#wrap_defined_queries!
)
Mutations occur through commands. Commands do not implement validation other than checking for required attributes and checking that the given attributes are defined on the source. Use the input verifier pattern to verify and validate input before passing it to a command (@see Pakyow::Verifier).
@example
source :posts, adapter: :sql, connection: :default do table :posts primary_id timestamps attribute :title, :string command :create do |params| insert(params) end def by_id(id) where(id: id) end end data.posts.create(title: "foo") data.posts.by_id(1).first => #<Pakyow::Data::Object @values={:id => 1, :title => "foo", :created_at => "2018-11-30 10:55:05 -0800", :updated_at => "2018-11-30 10:55:05 -0800"}>
Constants
- IVARS_TO_RELOAD
@api private
- MODIFIER_METHODS
@api private
- NESTED_METHODS
@api private
Attributes
@api private
Public Class Methods
Pakyow::Data::Sources::Base::new
# File lib/pakyow/data/sources/relational.rb, line 69 def initialize(*) super @wrap_as = self.class.singular_name @included = [] if default_query = self.class.__default_query result = if default_query.is_a?(Proc) instance_exec(&default_query) else public_send(self.class.__default_query) end result = case result when self.class result.__getobj__ else result end __setobj__(result) end end
Private Class Methods
@api private
# File lib/pakyow/data/sources/relational.rb, line 582 def association_with_name?(name) associations.values.flatten.find { |association| association.name == name } end
# File lib/pakyow/data/sources/relational.rb, line 486 def attribute(name, type = :string, **options) attributes[name.to_sym] = { type: type, options: options } end
# File lib/pakyow/data/sources/relational.rb, line 501 def belongs_to(association_name, query: nil, source: association_name) Associations::BelongsTo.new( name: association_name, query: query, source: self, associated_source_name: source ).tap do |association| @associations[:belongs_to] << association end end
# File lib/pakyow/data/sources/relational.rb, line 432 def command(command_name, provides_dataset: true, creates: false, updates: false, deletes: false, &block) @commands[command_name] = { block: block, provides_dataset: provides_dataset, creates: creates, updates: updates, deletes: deletes } end
# File lib/pakyow/data/sources/relational.rb, line 482 def default_primary_key_type :integer end
@api private
# File lib/pakyow/data/sources/relational.rb, line 575 def find_association_to_source(source) associations.values.flatten.find { |association| association.associated_source == source.class } end
rubocop:disable Naming/PredicateName
# File lib/pakyow/data/sources/relational.rb, line 510 def has_many(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise) Associations::HasMany.new( name: association_name, query: query, source: self, associated_source_name: source, as: as, dependent: dependent ).tap do |association| @associations[:has_many] << association if through setup_as_through(association, through: through) end end end
rubocop:disable Naming/PredicateName
# File lib/pakyow/data/sources/relational.rb, line 524 def has_one(association_name, query: nil, source: association_name, as: singular_name, through: nil, dependent: :raise) Associations::HasOne.new( name: association_name, query: query, source: self, associated_source_name: source, as: as, dependent: dependent ).tap do |association| @associations[:has_one] << association if through setup_as_through(association, through: through) end end end
# File lib/pakyow/data/sources/relational.rb, line 545 def make(name, adapter: Pakyow.config.data.default_adapter, connection: Pakyow.config.data.default_connection, state: nil, parent: nil, primary_id: true, timestamps: true, **kwargs, &block) super(name, state: state, parent: parent, adapter: adapter, connection: connection, attributes: {}, **kwargs) do adapter_class = Connection.adapter(adapter) if adapter_class.const_defined?("SourceExtension") # Extend the source with any adapter-specific behavior. # extension_module = adapter_class.const_get("SourceExtension") unless ancestors.include?(extension_module) include(extension_module) end # Define default fields # self.primary_id if primary_id self.timestamps if timestamps end # Call the original block. # class_eval(&block) if block_given? end end
# File lib/pakyow/data/sources/relational.rb, line 465 def primary_key(field) @primary_key_field = field end
# File lib/pakyow/data/sources/relational.rb, line 478 def primary_key_attribute attributes[@primary_key_field] end
# File lib/pakyow/data/sources/relational.rb, line 469 def primary_key_type case primary_key_attribute when Hash primary_key_attribute[:type] else primary_key_attribute.meta[:mapping] end end
# File lib/pakyow/data/sources/relational.rb, line 497 def qualifications(query_name) @qualifications.dig(query_name) || {} end
# File lib/pakyow/data/sources/relational.rb, line 442 def queries instance_methods - superclass.instance_methods end
# File lib/pakyow/data/sources/relational.rb, line 446 def query(query_name = nil, &block) @__default_query = query_name || block end
rubocop:enable Naming/PredicateName
# File lib/pakyow/data/sources/relational.rb, line 537 def setup_as_through(association, through:) Associations::Through.new(association, joining_source_name: through).tap do |through_association| associations[association.specific_type][ associations[association.specific_type].index(association) ] = through_association end end
@api private
Pakyow::Data::Sources::Base::source_from_source
# File lib/pakyow/data/sources/relational.rb, line 570 def source_from_source(*) super.tap(&:reload) end
# File lib/pakyow/data/sources/relational.rb, line 493 def subscribe(query_name, qualifications) @qualifications[query_name] = qualifications end
# File lib/pakyow/data/sources/relational.rb, line 450 def timestamps(create: :created_at, update: :updated_at) @timestamp_fields = { create: create, update: update } attribute create, :datetime attribute update, :datetime end
Public Instance Methods
# File lib/pakyow/data/sources/relational.rb, line 117 def as(object) tap do @wrap_as = object end end
@api private
# File lib/pakyow/data/sources/relational.rb, line 256 def block_for_nested_source?(maybe_nested_name) NESTED_METHODS.include?(maybe_nested_name) end
# File lib/pakyow/data/sources/relational.rb, line 184 def command(command_name) if command = self.class.commands[command_name] Command.new( command_name, block: command[:block], source: self, provides_dataset: command[:provides_dataset], creates: command[:creates], updates: command[:updates], deletes: command[:deletes] ) else raise( UnknownCommand.new_with_message(command: command_name).tap do |error| error.context = self.class end ) end end
@api private
# File lib/pakyow/data/sources/relational.rb, line 237 def command?(maybe_command_name) self.class.commands.include?(maybe_command_name) end
# File lib/pakyow/data/sources/relational.rb, line 204 def count if self.class.respond_to?(:count) self.class.count(__getobj__) else super end end
# File lib/pakyow/data/sources/relational.rb, line 93 def including(association_name, &block) tap do association_name = association_name.to_sym association_to_include = self.class.associations.values.flatten.find { |association| association.name == association_name } || raise(UnknownAssociation.new("unknown association `#{association_name}'").tap { |error| error.context = self.class }) included_source = association_to_include.associated_source.instance if association_to_include.query included_source = included_source.send(association_to_include.query) end final_source = if block_given? included_source.instance_exec(&block) || included_source else included_source end @included << [association_to_include, final_source] end end
# File lib/pakyow/data/sources/relational.rb, line 123 def limit(count) __setobj__(__getobj__.limit(count)); self end
@api private
# File lib/pakyow/data/sources/relational.rb, line 249 def modifier?(maybe_modifier_name) MODIFIER_METHODS.include?(maybe_modifier_name) end
# File lib/pakyow/data/sources/relational.rb, line 176 def on_commit(&block) self.class.container.connection.adapter.connection.after_commit(&block) end
# File lib/pakyow/data/sources/relational.rb, line 180 def on_rollback(&block) self.class.container.connection.adapter.connection.after_rollback(&block) end
# File lib/pakyow/data/sources/relational.rb, line 156 def one return @results.first if instance_variable_defined?(:@results) return @result if instance_variable_defined?(:@result) if result = self.class.one(__getobj__) include_results!([result]) @result = finalize(result) else nil end end
# File lib/pakyow/data/sources/relational.rb, line 127 def order(*ordering) __setobj__( __getobj__.order( *ordering.flat_map { |order| case order when Array Sequel.public_send(order[1].to_sym, order[0].to_sym) when Hash order.each_pair.map { |key, value| Sequel.public_send(value.to_sym, key.to_sym) } else Sequel.asc(order.to_s.to_sym) end } ) ); self end
@api private
# File lib/pakyow/data/sources/relational.rb, line 242 def query?(maybe_query_name) self.class.queries.include?(maybe_query_name) end
# File lib/pakyow/data/sources/relational.rb, line 217 def reload IVARS_TO_RELOAD.select { |ivar| instance_variable_defined?(ivar) }.each do |ivar| remove_instance_variable(ivar) end self end
@api private
# File lib/pakyow/data/sources/relational.rb, line 232 def source_name self.class.__object_name.name end
# File lib/pakyow/data/sources/relational.rb, line 146 def to_a return @results if instance_variable_defined?(:@results) @results = self.class.to_a(__getobj__) include_results!(@results) @results.map! { |result| finalize(result) } end
# File lib/pakyow/data/sources/relational.rb, line 227 def to_json(*) to_a.to_json end
# File lib/pakyow/data/sources/relational.rb, line 168 def transaction(&block) self.class.container.connection.transaction(&block) end
# File lib/pakyow/data/sources/relational.rb, line 172 def transaction? self.class.container.connection.adapter.connection.in_transaction? end
Private Instance Methods
# File lib/pakyow/data/sources/relational.rb, line 262 def finalize(result) wrap(typecast(result)) end
# File lib/pakyow/data/sources/relational.rb, line 290 def include_results!(results) @included.each do |association, combined_source| combined_source = combined_source.source_from_self( combined_source.__getobj__.dup ) group_by_key, assign_by_key, remove_keys = if association.type == :through joining_source = association.joining_source.instance if combined_source.class == association.joining_source combined_source.__setobj__( combined_source.class.container.connection.adapter.result_for_attribute_value( combined_source.class.container.connection.adapter.qualify_attribute( association.right_foreign_key_field, combined_source ), results.map { |result| result[association.associated_query_field] }, combined_source ) ) else aliased = SecureRandom.hex(4).to_sym if joining_source.class.container.connection == combined_source.class.container.connection # Optimize with joins. # combined_source.__setobj__( combined_source.class.container.connection.adapter.restrict_to_source( combined_source, combined_source.class.container.connection.adapter.result_for_attribute_value( combined_source.class.container.connection.adapter.qualify_attribute( association.right_foreign_key_field, joining_source ), joining_source.class.container.connection.adapter.restrict_to_attribute( association.query_field, source_from_self(__getobj__.dup) ), combined_source.class.container.connection.adapter.merge_results( association.left_foreign_key_field, association.associated_source.primary_key_field, joining_source, combined_source ) ), combined_source.class.container.connection.adapter.alias_attribute( combined_source.class.container.connection.adapter.qualify_attribute( association.right_foreign_key_field, joining_source ), aliased ) ) ) else # Manually join. # self_ids = self.class.container.connection.adapter.restrict_to_attribute( self.class.primary_key_field, self ).map { |result| result[self.class.primary_key_field] } joined_results = joining_source.class.container.connection.adapter.restrict_to_attribute( [association.right_foreign_key_field, association.left_foreign_key_field], joining_source.class.container.connection.adapter.result_for_attribute_value( association.right_foreign_key_field, self_ids, joining_source ) ) combined_results = combined_source.class.container.connection.adapter.result_for_attribute_value( combined_source.class.primary_key_field, joined_results.map { |result| result[association.left_foreign_key_field] }, combined_source ) combined_results = joined_results.map { |joined_result| combined_results.find { |result| result[combined_source.class.primary_key_field] == joined_result[association.left_foreign_key_field] }.dup.tap do |combined_result| combined_result[aliased] = joined_result[association.right_foreign_key_field] end } end end [aliased, association.name, [aliased]] else combined_source.__setobj__( combined_source.class.container.connection.adapter.result_for_attribute_value( association.associated_query_field, results.map { |result| result[association.query_field] }, combined_source ) ) [association.associated_query_field, association.name, []] end # Group the raw results by associated column value. # combined_results = (combined_results || combined_source).to_a.group_by { |combined_result| combined_result[group_by_key] } # Add each result group to its associated object. # results.map! { |result| combined_results_for_result = combined_results[result[association.query_field]].to_a.map! { |combined_result| if combined_result.is_a?(Pakyow::Data::Object) combined_result = combined_result.values.dup end # Remove any keys, such as temporary values used for grouping. # remove_keys.each do |remove_key| combined_result.delete(remove_key) end # Wrap the result into the appropriate data object. # combined_source.send(:wrap, combined_result) } result[assign_by_key] = if association.result_type == :one combined_results_for_result[0] else combined_results_for_result end result } end end
# File lib/pakyow/data/sources/relational.rb, line 266 def typecast(result) result.each do |key, value| unless value.nil? || !self.class.attributes.include?(key) result[key] = self.class.attributes[key][value] end end result end
# File lib/pakyow/data/sources/relational.rb, line 276 def wrap(result) wrapped_result = if @wrap_as.is_a?(Class) @wrap_as.new(result) else self.class.container.object(@wrap_as).new(result) end if wrapped_result.is_a?(Object) wrapped_result.originating_source = self.class end wrapped_result end