module Sequel class Model @allowed_columns = nil @association_reflections = {} @dataset_methods = {} @hooks = {} @primary_key = :id @raise_on_save_failure = true @raise_on_typecast_failure = true @restrict_primary_key = true @restricted_columns = nil @sti_dataset = nil @sti_key = nil @strict_param_setting = true @typecast_empty_string_to_nil = true @typecast_on_assignment = true # Which columns should be the only columns allowed in a call to set # (default: all columns). metaattr_reader :allowed_columns # All association reflections defined for this model (default: none). metaattr_reader :association_reflections # Hash of dataset methods to add to this class and subclasses when # set_dataset is called. metaattr_reader :dataset_methods # The default primary key for classes (default: :id) metaattr_reader :primary_key # Whether to raise an error instead of returning nil on a failure # to save/create/save_changes/etc. metaattr_accessor :raise_on_save_failure # Whether to raise an error when unable to typecast data for a column # (default: true) metaattr_accessor :raise_on_typecast_failure # Which columns should not be update in a call to set # (default: no columns). metaattr_reader :restricted_columns # The base dataset for STI, to which filters are added to get # only the models for the specific STI subclass. metaattr_reader :sti_dataset # The column name holding the STI key for this model metaattr_reader :sti_key # Whether new/set/update and their variants should raise an error # if an invalid key is used (either that doesn't exist or that # access is restricted to it). metaattr_accessor :strict_param_setting # Whether to typecast the empty string ('') to nil for columns that # are not string or blob. metaattr_accessor :typecast_empty_string_to_nil # Whether to typecast attribute values on assignment (default: true) metaattr_accessor :typecast_on_assignment # Dataset methods to proxy via metaprogramming DATASET_METHODS = %w'<< all avg count delete distinct eager eager_graph each each_page empty? except exclude filter first from from_self full_outer_join get graph group group_and_count group_by having import inner_join insert insert_multiple intersect interval invert_order join join_table last left_outer_join limit map multi_insert naked order order_by order_more paginate print query range reverse_order right_outer_join select select_all select_more server set set_graph_aliases single_value size to_csv to_hash transform union uniq unfiltered unordered update where'.map{|x| x.to_sym} # Instance variables that are inherited in subclasses INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@cache_store=>nil, :@cache_ttl=>nil, :@dataset_methods=>:dup, :@primary_key=>nil, :@raise_on_save_failure=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil, :@sti_dataset=>nil, :@sti_key=>nil, :@strict_param_setting=>nil, :@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil, :@raise_on_typecast_failure=>nil, :@association_reflections=>:dup} # Returns the first record from the database matching the conditions. # If a hash is given, it is used as the conditions. If another # object is given, it finds the first record whose primary key(s) match # the given argument(s). If caching is used, the cache is checked # first before a dataset lookup is attempted unless a hash is supplied. def self.[](*args) args = args.first if (args.size == 1) if Hash === args dataset[args] else @cache_store ? cache_lookup(args) : dataset[primary_key_hash(args)] end end # Returns the columns in the result set in their original order. # Generally, this will used the columns determined via the database # schema, but in certain cases (e.g. models that are based on a joined # dataset) it will use Dataset#columns to find the columns, which # may be empty if the Dataset has no records. def self.columns @columns || set_columns(dataset.naked.columns) end # Creates new instance with values set to passed-in Hash, saves it # (running any callbacks), and returns the instance if the object # was saved correctly. If there was an error saving the object, # returns false. def self.create(values = {}, &block) obj = new(values, &block) return unless obj.save obj end # Returns the dataset associated with the Model class. def self.dataset @dataset || raise(Error, "No dataset associated with #{self}") end # Returns the database associated with the Model class. def self.db return @db if @db @db = self == Model ? DATABASES.first : superclass.db raise(Error, "No database associated with #{self}") unless @db @db end # Sets the database associated with the Model class. def self.db=(db) @db = db if @dataset set_dataset(db[table_name]) end end # Returns the cached schema information if available or gets it # from the database. def self.db_schema @db_schema ||= get_db_schema end # If a block is given, define a method on the dataset with the given argument name using # the given block as well as a method on the model that calls the # dataset method. # # If a block is not given, define a method on the model for each argument # that calls the dataset method of the same argument name. def self.def_dataset_method(*args, &block) raise(Error, "No arguments given") if args.empty? if block_given? raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1 meth = args.first @dataset_methods[meth] = block dataset.meta_def(meth, &block) end args.each{|arg| instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)} end # Deletes all records in the model's table. def self.delete_all dataset.delete end # Like delete_all, but invokes before_destroy and after_destroy hooks if used. def self.destroy_all dataset.destroy end # Returns a dataset with custom SQL that yields model objects. def self.fetch(*args) db.fetch(*args).set_model(self) end # Modify and return eager loading dataset based on association options def self.eager_loading_dataset(opts, ds, select, associations) ds = ds.select(*select) if select ds = ds.order(*opts[:order]) if opts[:order] ds = ds.eager(opts[:eager]) if opts[:eager] ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] ds = ds.eager(associations) unless associations.blank? ds = opts[:eager_block].call(ds) if opts[:eager_block] ds end # Finds a single record according to the supplied filter, e.g.: # # Ticket.find :author => 'Sharon' # => record def self.find(*args, &block) dataset.filter(*args, &block).first end # Like find but invokes create with given conditions when record does not # exists. def self.find_or_create(cond) find(cond) || create(cond) end # If possible, set the dataset for the model subclass as soon as it # is created. Also, inherit the INHERITED_INSTANCE_VARIABLES # from the parent class. def self.inherited(subclass) sup_class = subclass.superclass ivs = subclass.instance_variables.collect{|x| x.to_s} INHERITED_INSTANCE_VARIABLES.each do |iv, dup| next if ivs.include?(iv.to_s) sup_class_value = sup_class.instance_variable_get(iv) sup_class_value = sup_class_value.dup if dup == :dup && sup_class_value subclass.instance_variable_set(iv, sup_class_value) end unless ivs.include?("@dataset") begin if sup_class == Model subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.blank? elsif ds = sup_class.instance_variable_get(:@dataset) subclass.set_dataset(sup_class.sti_key ? sup_class.sti_dataset.filter(sup_class.sti_key=>subclass.name.to_s) : ds.clone, :inherited=>true) end rescue nil end end hooks = subclass.instance_variable_set(:@hooks, {}) sup_class.instance_variable_get(:@hooks).each{|k,v| hooks[k] = v.dup} end # Returns the implicit table name for the model class. def self.implicit_table_name name.demodulize.underscore.pluralize.to_sym end # Initializes a model instance as an existing record. This constructor is # used by Sequel to initialize model instances when fetching records. # #load requires that values be a hash where all keys are symbols. It # probably should not be used by external code. def self.load(values) new(values, true) end # Mark the model as not having a primary key. Not having a primary key # can cause issues, among which is that you won't be able to update records. def self.no_primary_key @primary_key = nil end # Returns primary key attribute hash. If using a composite primary key # value such be an array with values for each primary key in the correct # order. For a standard primary key, value should be an object with a # compatible type for the key. If the model does not have a primary key, # raises an Error. def self.primary_key_hash(value) raise(Error, "#{self} does not have a primary key") unless key = @primary_key case key when Array hash = {} key.each_with_index{|k,i| hash[k] = value[i]} hash else {key => value} end end # Restrict the setting of the primary key(s) inside new/set/update. Because # this is the default, this only make sense to use in a subclass where the # parent class has used unrestrict_primary_key. def self.restrict_primary_key @restrict_primary_key = true end # Whether or not setting the primary key inside new/set/update is # restricted, true by default. def self.restrict_primary_key? @restrict_primary_key end # Serializes column with YAML or through marshalling. Arguments should be # column symbols, with an optional trailing hash with a :format key # set to :yaml or :marshal (:yaml is the default). Setting this adds # a transform to the model and dataset so that columns values will be serialized # when saved and deserialized when returned from the database. def self.serialize(*columns) format = columns.extract_options![:format] || :yaml @transform = columns.inject({}) do |m, c| m[c] = format m end @dataset.transform(@transform) if @dataset end # Whether or not the given column is serialized for this model. def self.serialized?(column) @transform ? @transform.include?(column) : false end # Set the columns to allow in new/set/update. Using this means that # any columns not listed here will not be modified. If you have any virtual # setter methods (methods that end in =) that you want to be used in # new/set/update, they need to be listed here as well (without the =). # # It may be better to use (set|update)_only instead of this in places where # only certain columns may be allowed. def self.set_allowed_columns(*cols) @allowed_columns = cols end # Sets the dataset associated with the Model class. ds can be a Symbol # (specifying a table name in the current database), or a Dataset. # If a dataset is used, the model's database is changed to the given # dataset. If a symbol is used, a dataset is created from the current # database with the table name given. Other arguments raise an Error. # # This sets the model of the the given/created dataset to the current model # and adds a destroy method to it. It also extends the dataset with # the Associations::EagerLoading methods, and assigns a transform to it # if there is one associated with the model. Finally, it attempts to # determine the database schema based on the given/created dataset. def self.set_dataset(ds, opts={}) inherited = opts[:inherited] @dataset = case ds when Symbol db[ds] when Dataset @db = ds.db ds else raise(Error, "Model.set_dataset takes a Symbol or a Sequel::Dataset") end @dataset.set_model(self) @dataset.transform(@transform) if @transform if inherited @columns = @dataset.columns rescue nil else @dataset.extend(DatasetMethods) @dataset.extend(Associations::EagerLoading) @dataset_methods.each{|meth, block| @dataset.meta_def(meth, &block)} if @dataset_methods end @db_schema = (inherited ? superclass.db_schema : get_db_schema) rescue nil self end metaalias :dataset=, :set_dataset # Sets primary key, regular and composite are possible. # # Example: # class Tagging < Sequel::Model # # composite key # set_primary_key :taggable_id, :tag_id # end # # class Person < Sequel::Model # # regular key # set_primary_key :person_id # end # # You can set it to nil to not have a primary key, but that # cause certain things not to work, see #no_primary_key. def self.set_primary_key(*key) @primary_key = (key.length == 1) ? key[0] : key.flatten end # Set the columns to restrict in new/set/update. Using this means that # any columns listed here will not be modified. If you have any virtual # setter methods (methods that end in =) that you want not to be used in # new/set/update, they need to be listed here as well (without the =). # # It may be better to use (set|update)_except instead of this in places where # only certain columns may be allowed. def self.set_restricted_columns(*cols) @restricted_columns = cols end # Makes this model a polymorphic model with the given key being a string # field in the database holding the name of the class to use. If the # key given has a NULL value or there are any problems looking up the # class, uses the current class. # # This should be used to set up single table inheritance for the model, # and it only makes sense to use this in the parent class. # # You should call sti_key after any calls to set_dataset in the model, # otherwise subclasses might not have the filters set up correctly. # # The filters that sti_key sets up in subclasses will not work if # those subclasses have further subclasses. For those middle subclasses, # you will need to call set_dataset manually with the correct filter set. def self.set_sti_key(key) m = self @sti_key = key @sti_dataset = dataset dataset.set_model(key, Hash.new{|h,k| h[k] = (k.constantize rescue m)}) before_create(:set_sti_key){send("#{key}=", model.name.to_s)} end # Returns the columns as a list of frozen strings instead # of a list of symbols. This makes it possible to check # whether a column exists without creating a symbol, which # would be a memory leak if called with user input. def self.str_columns @str_columns ||= columns.map{|c| c.to_s.freeze} end # Defines a method that returns a filtered dataset. Subsets # create dataset methods, so they can be chained for scoping. # For example: # # Topic.subset(:popular, :num_posts > 100) # Topic.subset(:recent, :created_on > Date.today - 7) # # Allows you to do: # # Topic.filter(:username.like('%joe%')).popular.recent # # to get topics with a username that includes joe that # have more than 100 posts and were created less than # 7 days ago. def self.subset(name, *args, &block) def_dataset_method(name){filter(*args, &block)} end # Returns name of primary table for the dataset. def self.table_name dataset.opts[:from].first end # Allow the setting of the primary key(s) inside new/set/update. def self.unrestrict_primary_key @restrict_primary_key = false end # Add model methods that call dataset methods def_dataset_method(*DATASET_METHODS) ### Private Class Methods ### # Create the column accessors def self.def_column_accessor(*columns) # :nodoc: columns.each do |column| im = instance_methods.collect{|x| x.to_s} meth = "#{column}=" overridable_methods_module.module_eval do define_method(column){self[column]} unless im.include?(column.to_s) unless im.include?(meth) define_method(meth) do |*v| len = v.length raise(ArgumentError, "wrong number of arguments (#{len} for 1)") unless len == 1 self[column] = v.first end end end end end # Get the schema from the database, fall back on checking the columns # via the database if that will return inaccurate results or if # it raises an error. def self.get_db_schema(reload = false) # :nodoc: set_columns(nil) return nil unless @dataset schema_hash = {} ds_opts = dataset.opts single_table = ds_opts[:from] && (ds_opts[:from].length == 1) \ && !ds_opts.include?(:join) && !ds_opts.include?(:sql) get_columns = proc{columns rescue []} if single_table && (schema_array = (db.schema(table_name, :reload=>reload) rescue nil)) schema_array.each{|k,v| schema_hash[k] = v} if ds_opts.include?(:select) # Dataset only selects certain columns, delete the other # columns from the schema cols = get_columns.call schema_hash.delete_if{|k,v| !cols.include?(k)} cols.each{|c| schema_hash[c] ||= {}} else # Dataset is for a single table with all columns, # so set the columns based on the order they were # returned by the schema. cols = schema_array.collect{|k,v| k} set_columns(cols) # Set the primary key(s) based on the schema information pks = schema_array.collect{|k,v| k if v[:primary_key]}.compact pks.length > 0 ? set_primary_key(*pks) : no_primary_key # Also set the columns for the dataset, so the dataset # doesn't have to do a query to get them. dataset.instance_variable_set(:@columns, cols) end else # If the dataset uses multiple tables or custom sql or getting # the schema raised an error, just get the columns and # create an empty schema hash for it. get_columns.call.each{|c| schema_hash[c] = {}} end schema_hash end # Module that the class includes that holds methods the class adds for column accessors and # associations so that the methods can be overridden with super def self.overridable_methods_module include(@overridable_methods_module = Module.new) unless @overridable_methods_module @overridable_methods_module end # Set the columns for this model, reset the str_columns, # and create accessor methods for each column. def self.set_columns(new_columns) # :nodoc: @columns = new_columns def_column_accessor(*new_columns) if new_columns @str_columns = nil @columns end private_class_method :def_column_accessor, :get_db_schema, :overridable_methods_module, :set_columns end end