Class: DBStruct
- Inherits:
-
Object
- Object
- DBStruct
- Extended by:
- ErrorHelpers
- Includes:
- ErrorHelpers
- Defined in:
- lib/dbstruct.rb,
lib/internal/util.rb,
lib/internal/error.rb,
lib/internal/dbstruct_base.rb
Overview
DBStruct presents a (SQLite3) database table in a way that closely mimics a Ruby Hash of Struct objects.
This is an abstract base class. To use it, you create a subclass using the with method and define the fields to be used. This also associates the new class with a Sequel dataset.
Subinstances behave similarly to Ruby’s Struct class. However, setting or reading a value accesses the corresponding database row.
Each object also knows its database row ID, an integer that can be used to look up this object. If a subinstance outlives its database row, attempting to access the fields will raise a MissingRowError. You can use methods #present? or #deleted? to test if this has happened.
Defined Under Namespace
Modules: ErrorHelpers Classes: BogoHash, Failure, FieldError, MissingRowError, TypeError
Instance Attribute Summary collapse
-
#rowid ⇒ Object
readonly
Returns the numeric Row ID that is used as the primary key for this row in both the database and any BogoHash instances that may contain it.
Class Method Summary collapse
-
.items(*selectors) ⇒ Object
Return a
DBStruct::BogoHashcontaining some or all of the items in this table. -
.transaction(*args, **kwargs, &block) ⇒ Object
Evaluates
blockwithin a transaction. -
.where(*cond, **kwcond, &block) ⇒ Object
Convenience method; equivalent to self.items.where(…).
-
.with(db, name, &body) ⇒ Class
Define a concrete DBStruct subclass and create the corresponding table if needed.
Instance Method Summary collapse
-
#[](field) ⇒ Object
Equivalent to
get_field. -
#[]=(field, value) ⇒ Object
Equivalent to
set_field. -
#deleted? ⇒ Boolean
Test if the database row associated with
selfno longer exists. -
#eql?(other) ⇒ Boolean
(also: #==)
Test for equality.
-
#get_field(name) ⇒ Object
Sets the value of the field named by
fieldto the given value. -
#hash ⇒ Object
Return a hash; here because we’ve also overridden #eql?.
-
#initialize(_id: nil, **kwargs) ⇒ DBStruct
constructor
Creates a new instance of class, creates the corresponding database row and initializes the fields.
-
#inspect ⇒ Object
(also: #to_s)
Return a human-friendly description.
-
#present? ⇒ Boolean
Test if the database row associated with
selfstill exists. -
#set_field(name, value) ⇒ Object
Retrieve the value of the field named by symbol
field. -
#set_fields(**kwargs) ⇒ Object
Set zero or more fields at once.
-
#to_a ⇒ Object
Return the contents as an array of pairs of key and value.
-
#to_h ⇒ Object
Return the contents as a hash.
Methods included from ErrorHelpers
Constructor Details
#initialize(_id: nil, **kwargs) ⇒ DBStruct
Creates a new instance of class, creates the corresponding database row and initializes the fields. This only works if the class is a subclass of DBStruct; DBStruct itself cannot be instantiated.
If keyword arguments are given, they must correspond to the names of the fields and have values of the correct type or nil. Omitted arguments are equivalent to nil.
The keyword argument _id: is intended for internal use only and should not be used.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/internal/dbstruct_base.rb', line 20 def initialize(_id: nil, # Internal use only! **kwargs) check("Attempted to instantiate abstract class DBStruct.") { self.class < DBStruct } @rowid = nil # Case 1: existing row if _id check("Using '_id' with other arguments!") { kwargs.empty? } @rowid = _id check("Row '#{_id}' does not exist!", MissingRowError) { present? } return end @rowid = dataset().insert({}) set_fields(**kwargs) end |
Instance Attribute Details
#rowid ⇒ Object (readonly)
Returns the numeric Row ID that is used as the primary key for this row in both the database and any BogoHash instances that may contain it.
7 8 9 |
# File 'lib/internal/dbstruct_base.rb', line 7 def rowid @rowid end |
Class Method Details
.items(*selectors) ⇒ Object
Return a DBStruct::BogoHash containing some or all of the items in this table. If arguments are given, they must correspond to group fields. In that case, the returned BogoHash contains only those items whose values in those field match the argument.
(This is roughly equivalent to using the Sequel where method to narrow a Dataset.)
So
Books.items("non-fiction", "dragons")
will return a BogoHash that contains only entries whose first group field has the value “non-fiction” and whose second group field has the value 42.
nil arguments are treated as wildcards and will match everything. Thus,
Books.items(nil, "dragons")
will return all items whose second group matches “dragons”.
249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/internal/dbstruct_class.rb', line 249 def items(*selectors) check_not_base_class() check("Too many group specifiers!") { selectors.size <= self.groups.size } ds = get_group_selection_dataset(selectors) or return nil return DBStruct::BogoHash.new(self, ds, selectors) end |
.transaction(*args, **kwargs, &block) ⇒ Object
Evaluates block within a transaction. These can be safely nested and will (usually) roll back if an exception occurs in the block.
This is trivial convenience wrapper around ‘Sequel::Database#transaction`; see its documentation for details.
289 290 |
# File 'lib/internal/dbstruct_class.rb', line 289 def transaction(*args, **kwargs, &block) = self.db.transaction(*args, **kwargs, &block) |
.where(*cond, **kwcond, &block) ⇒ Object
Convenience method; equivalent to self.items.where(…).
See DBStruct::BogoHash#where for details.
278 279 |
# File 'lib/internal/dbstruct_class.rb', line 278 def where(*cond, **kwcond, &block) = self.items.where(*cond, **kwcond, &block) |
.with(db, name, &body) ⇒ Class
Define a concrete DBStruct subclass and create the corresponding table if needed.
The database and table are both accessed via Sequel classes (Sequel::Database and Sequel::Dataset respectively) and so are subject to their constraints and abilities. In addition, the underlying database must be SQLite.
The new class is unnamed unless assigned to a global constant.
If the corresponding table does not exist, it is created from the class’s layout.
If a table with a matching name already exists, it is assumed to have previously been created by this method with this layout. While there is some rudimentary consistency checking done to ensure that the expected columns are there, it is not exhaustive. If it is possible for an incompatible table with the same name to appear (e.g. due to version upgrades), you will need to use other mechanisms to detect this.
The new class’s layout (and corresponding table) is defined via the attached block. It is evaluated via class_eval and so may be used to define methods for the new subclass. In addition, it provides a minimal DSL for defining fields (i.e. database columns).
The DSL adds two methods, field and group:
field :name, type
group :name, type
field declares a new field and matching database column. It will create a database column named name with matching getter and setter methods that will access it. name must be a Symbol.
Note that name must begin with a lower-case letter. This is a limitation imposed by DBStruct itself so that can add extra internal fields. (Currently, the only one used is called _id; this is the numeric primary key used as a unique row ID.)
type may be any Ruby type that Sequel knows how to convert to a database column (see the link below). Unlike Sequel, DBStruct requires actual Ruby classes and will ensure that only objects of that specific type (or nil) may be stored in that field. This is enforced by the setter methods. (Exception: TrueClass and FalseClass are interchangeable.)
group is like field except that the field is also treated as a category when used to select subsets with arguments to items. group calls may be freely intermixed with field calls; however, their order is significant. Positional argumetns to items must match the order of group declarations.
The new class is unnamed unless assigned to a global constant.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/internal/dbstruct_class.rb', line 71 def with(db, name, &body) check("Argument 'db' is not a valid Sequel::Database") { db.is_a?(Sequel::Database) } # It should be pretty straightfoward to support other databases, # but I have neither the time nor resources to do so right now. check("DBStruct currently only supports SQLite; #{db.database_type} " + "is unsupported.") { db.database_type == :sqlite } check("Must provide a block argument") { body } sc = create_the_subclass(db, name, body) create_table_if_needed(sc) return sc end |
Instance Method Details
#[](field) ⇒ Object
Equivalent to get_field
115 |
# File 'lib/internal/dbstruct_base.rb', line 115 def [](field) = get_field(field) |
#[]=(field, value) ⇒ Object
Equivalent to set_field
118 119 120 |
# File 'lib/internal/dbstruct_base.rb', line 118 def []=(field, value) return set_field(field, value) end |
#deleted? ⇒ Boolean
Test if the database row associated with self no longer exists. Inverse of present?
73 |
# File 'lib/internal/dbstruct_base.rb', line 73 def deleted? = dataset().where(_id:@rowid).empty? |
#eql?(other) ⇒ Boolean Also known as: ==
Test for equality. Instances are equal if and only if they refer to the same database row and (as implied) are of the same class.
126 |
# File 'lib/internal/dbstruct_base.rb', line 126 def eql?(other) = self.class == other.class && @rowid == other.rowid |
#get_field(name) ⇒ Object
Sets the value of the field named by field to the given value. field must be one of the declared fields and value must have the declared class.
78 79 80 81 |
# File 'lib/internal/dbstruct_base.rb', line 78 def get_field(name) check_field(name, nil) return dataset[_id: @rowid][name] end |
#hash ⇒ Object
Return a hash; here because we’ve also overridden #eql?
130 |
# File 'lib/internal/dbstruct_base.rb', line 130 def hash = [self.class, @rowid].hash |
#inspect ⇒ Object Also known as: to_s
Return a human-friendly description.
135 136 137 138 139 140 141 |
# File 'lib/internal/dbstruct_base.rb', line 135 def inspect max_line = 60 values = to_h.map{|k,v| "#{k}: #{v.inspect}"}.join(", ") values = values[0..max_line] + "..." if values.size > max_line return "#{self.class}.new(#{values})" end |
#present? ⇒ Boolean
Test if the database row associated with self still exists. Inverse of deleted?.
69 |
# File 'lib/internal/dbstruct_base.rb', line 69 def present? = !deleted? |
#set_field(name, value) ⇒ Object
Retrieve the value of the field named by symbol field. Raises an exception if field is not one of the declared fields.
85 86 87 88 89 90 91 92 93 |
# File 'lib/internal/dbstruct_base.rb', line 85 def set_field(name, value) check_field(name, value) dataset() .where(_id: @rowid) .update({name => value}) return value end |
#set_fields(**kwargs) ⇒ Object
Set zero or more fields at once. Keywords must be the names of defined fields.
The update is enclosed in an transaction.
99 100 101 102 103 104 |
# File 'lib/internal/dbstruct_base.rb', line 99 def set_fields(**kwargs) self.class.transaction { kwargs.each{|k, v| set_field(k, v)} } return self end |
#to_a ⇒ Object
Return the contents as an array of pairs of key and value
109 |
# File 'lib/internal/dbstruct_base.rb', line 109 def to_a = self.columns.map{|key| [key, get_field(key)] } |
#to_h ⇒ Object
Return the contents as a hash
112 |
# File 'lib/internal/dbstruct_base.rb', line 112 def to_h = to_a.to_h |