ActiveOrient
Use OrientDB to persistently store dynamic Ruby-Objects and use database queries to manage even very large datasets.
Other Documents
You need a ruby 2.3, 2.4 or a jruby 9.1x Installation and a working OrientDB-Instance (Version 2.2 prefered). The jruby-part is experimental.
Quick Start
- clone the project,
- run bundle install & bundle update,
- update config/connect.yml,
- create the documentation by calling »rdoc«
- start an irb-session by calling
cd bin ./active-orient-console test # or d)develpoment, p)roduction environment as defined in config/connect.yml
«ORD» or «DB» is the Database-Instance itself. If the Database noticed is not present, it is created on startup. A simple SQL-Query is submitted by providing a Block to »execute«
result = ORD.execute { "select * from Stock" }
Obviously, the class »Stock« has to exist. Let's create some classes
ORD.create_class 'ClassDocumentName' # creates or opens a basic document-class
ORD.create_vertex_class 'ClassVertexName' # creates or opens a vertex-class
ORD.create_edge_class 'ClassEdgeName' # creates or opens an edge-class, providing bidirectional links between documents
{Classname}.delete_class # removes the class in the database and destroys the ruby-object
Classnames appear unchanged as Database-Classes. Strings and Symbols are accepted. Depending on the namespace choosen in 'config/config.yml' Model-Classes are allocated and linked to database-classes. For simplicity, here we omit any namespace ( :namespace: :object in config.yml). Thus the Model-Obects are accessible directly.
Naming-Convention: The name given in the »create-class«-Statement becomes the Database-Classname. In Ruby-Space its Camelized, ie: ORD.create_class(:hut_ab) generates a Ruby-Class »HutAb«.
This can be customized in the "naming_convention"-class-method, which has to be defined in 'config/boot.rb'. The naming_convention changes the ruby-view to the classes. The Database-Class-Name is derived from the argument to #CreateClass, ORD.create_class('HANDS_UP') creates a database-class "HANDS_UP' and a Ruby-Class "HandsUp".
ActiveOrient::Model's can be customized through methods defined in the model-directory. These methods are loaded automatically afert executing #CreateClass (and through the preallocation process). Further details in the Examples-Section.
CRUD
The CRUD-Process (create, read = query, update and remove) is performed as
ORD.create_class :M
M.create name: 'Hugo', age: 46, interests: [ 'swimming', 'biking', 'reading' ]
# or
new_record = M.new age: 46, interests: [ 'swimming', 'biking', 'reading' ]
new_record.save # alternative: new_record.update
##
hugo = M.where( name: 'Hugo' ).first
hugo.update set: { :father => M.create( name: "Volker", age: 76 ) } # we create an internal link
hugo.father.name # --> volker
M.remove hugo
M.delete_class # removes the class from OrientDB and deletes the ruby-object-definition
Inherence
Create a Tree of Objects with create_classes
ORD.create_classes sector: [ :industry, :category, :subcategory ]
=> {Sector=>[Industry, Category, Subcategory]}
Industry.create name: 'Communications' #---> Create an Industry-Record with the attribute "name"
Sector.where name: 'Communications' #---> an Array with the Industry-Object
=> [#<Industry:0x0000000225e098 @metadata= (...) ]
notice to create inherent Vertices use ORD.create_classes( sector: [ :industry, :category, :subcategory ]){ :V }
Preallocation of Model-Classes
All database-classes are preallocated after connecting to the database. Thus you can use Model-Classes from the start.
If the "rid" is known, any Object can be retrieved and correctly allocated by
The database-class «V» is present in any case. Any model-class can be used, even the parent »ActiveOrient::Model«
Properties
The schemaless mode has many limitations. ActiveOrient offers a Ruby way to define Properties and Indexes
ORD.create_class :M, :item
M.create_property :symbol # the default-case: type: :string, no index
M.create_property :con_id, type: :integer
M.create_property :details, type: :link, other_class: 'Contracts'
M.create_property :items, type: :linklist, :linklist: Item
M.create_property :name, index: :unique # or M.create_property( 'name' ){ :unique }
(Experimental) You can put restrictions on your properties with the command "alter_property":
M.alter_property property: "value", attribute: "MIN", alteration: 0
M.alter_property property: "value", attribute: "MAX", alteration: 23
Active Model interface
As for ActiveRecord-Tables, the Model-class itself provides methods to inspect and filter datasets form the database.
M.all
M.first
M.last # notice: last does not work in orientdb version 2.2, because the sorting algorithm for rid's is damaged
M.all.last # or M.where( ... ).last walkaround for Orientdb V 2.2
M.where town: 'Berlin'
M.count where: { town: 'Berlin' }
»count« gets the number of datasets fulfilling the search-criteria. Any parameter defining a valid SQL-Query in Orientdb can be provided to the »count«, »where«, »first« and »last«-method.
A »normal« Query is submitted via
M.get_records projection: { projection-parameter },
distinct: { some parameters },
where: { where-parameter },
order: { sorting-parameters },
group_by: { one grouping-parameter},
unwind: ,
skip: ,
limit:
# or
query = OrientSupport::OrientQuery.new {paramter}
M.query_database query
To update several records, a class-method »update_all« is defined.
M.update_all connected: false # add a property »connected» to each record
M.update_all set:{ connected: true }, where: "symbol containsText 'S'"
Graph-support:
ORD.create_vertex_class :the_vertex
ORD.create_edge_class :the_edge
vertex_1 = TheVertex.create color: "blue"
vertex_2 = TheVertex.create flower: "rose"
TheEdge.create_edge attributes: {:birthday => Date.today }, from: vertex_1, to: vertex_2
It connects the vertexes and assigns the attributes to the edge.
To query a graph, SQL-like-Queries and Match-statements can be used (see below).
Links and LinkLists
A record in a database-class is defined by a »rid«. If this is stored in a class, a link is set. In OrientDB links are used to realize unidirectional 1:1 and 1:n relationships.
ActiveOrient autoloads Model-objects when they are accessed. Example: If an Object is stored in Cluster 30 and id 2, then "#30:2" fully qualifies the ActiveOrient::Model object and sets the link if stored somewhere.
ORD.create_class 'test_link'
ORD.create_class 'test_base'
link_document = TestLink.create att: 'one attribute'
base_document = TestBase.create base: 'my_base', single_link: link_document
base_document.single_link just contains the rid. When accessed, the ActiveOrient::Model::Testlinkclass-object is autoloaded and
base_document.single_link.att
reads the stored content of link_document.
To store a list of links to other Database-Objects, a simple Array is allocated
# predefined linkmap-properties
TestLink.create_property :links, type: :linklist, linkedClass: :test_links
base_document = TestBase.create links: []
(0 .. 20).each{|y| base_document.links << TestLink.create( nr: y )}
#or in schemaless-mode
base_document = TestBase.create links: (0..20).map{|y| TestLink.create nr: y}
base_document.update
base_document.links behaves like a ruby-array.
If you got an undirectional graph
a --> b ---> c --> d
the graph elements can be explored by joining the objects (a[6].b[5].c[9].d)
Refer to the "Time-Graph"-Example for an Implementation of an bidirectional Graph with the same Interface
Edges
Edges provide bidirectional Links. They are easily handled
ORD.create_vertex_class :the_vertex # --> TheVertex
ORD.create_edge_class :the_edge # --> TheEdge
start = TheVertex.create something: 'nice'
the_end = TheVertex.create something: 'not_nice'
the_edge = TheEdge.create attributes: {transform_to: 'very bad'},
from: start,
to: the_end
Edges are connected to vertices by »in« and »out«-Methods. Inherence is supported.
i.e.
ORD.create_class( top_edge ) { THE_EDGE }
ORD.create_class( on_top_edge ) { TOP_EDGE }
ON_TOP_Edge.create from: start, to: the_end
start.reload!
start.out :the_edge
=> [#<TOP_EDGE:0x00000001d92f28 @metadata= ... ]
Edge-links are displayed and retrieved by
start.edges # :in | :out | :all
=> ["#73:0"]
start.edges(:out).map &:from_orient
=> [#<TOP_EDGE:0x00000001d92f28 @metadata= ... ]
The create-Method od Edge-Classes takes a block. Then all statements are transmitted in batch-mode. Assume, Vertex1 and Vertex2 are Vertex-Classes and TheEdge is an Edge-Class, then
record1 = (1 .. 100).map{|y| Vertex1.create testentry: y }
record2 = (:a .. :z).map{|y| Vertex2.create testentry: y }
edges = TheEdge.create attributes: { study: 'Experiment1'} do | attributes |
# map returns an array, which is further processed by #create_edge
('a'.ord .. 'z'.ord).map do |o|
{ from: record1.find{|x| x.testentry == o },
to: record2.find{ |x| x.testentry.ord == o },
attributes: attributes.merge( key: o.chr ) }
end
connects the vertices and provides a variable "key" and a common "study" attribute to each edge.
Queries
Contrary to traditional SQL-based Databases OrientDB handles sub-queries very efficiently. In addition, OrientDB supports precompiled statements (let-Blocks).
ActiveOrient is equipped with a simple QueryGenerator: ActiveSupport::OrientQuery. It works in two ways: a comprehensive and a subsequent one
q = OrientSupport::OrientQuery.new
q.from = Vertex # If a constant is used, then the correspending
# ActiveOrient::Model-class is refered
q.where << a: 2
q.where << 'b > 3 '
q.distinct = :profession
q.order = {:name => :asc}
is equivalent to
q = OrientSupport::OrientQuery.new from: Vertex ,
where: [{ a: 2 }, 'b > 3 '],
distinct: :profession,
order: { :name => :asc }
q.to_s
=> select distinct( profession ) from Vertex where a = 2 and b > 3 order by name asc
Both eayss can be mixed.
If sub-queries are necessary, they can be introduced as OrientSupport::OrientQuery or as »let-block«.
OQ = OrientSupport::OrientQuery
q = OQ.new from: 'ModelQuery'
q.let << "$city = adress.city"
q.where = "$city.country.name = 'Italy' OR $city.country.name = 'France'"
q.to_s
# => select from ModelQuery let $city = adress.city where $city.country.name = 'Italy' OR $city.country.name = 'France'
or
q = OQ.new
q.let << {a: OQ.new( from: '#5:0' ) }
q.let << {b: OQ.new( from: '#5:1' ) }
q.let << '$c= UNIONALL($a,$b) '
q.projection << 'expand( $c )'
q.to_s # => select expand( $c ) let $a = ( select from #5:0 ), $b = ( select from #5:1 ), $c= UNIONALL($a,$b)
or
last_12_open_interest_records = OQ.new from: OpenInterest,
order: { fetch_date: :desc } , limit: 12
bunch_of_contracts = OQ.new from: last_12_open_interest_records,
projection: 'expand( contracts )'
distinct_contracts = OQ.new from: bunch_of_contracts,
projection: 'expand( distinct(@rid) )'
distinct_contracts.to_s
=> "select expand( distinct(@rid) ) from ( select expand( contracts ) from ( select from open_interest order by fetch_date desc limit 12 ) ) "
cq = ORD.get_documents query: distinct_contracts
this executes the query and returns the adressed rid's, which are eventually retrieved from the rid-cache.
Match
A Match-Query starts at the given ActiveOrient::Model-Class. The where-cause narrows the sample to certain records. In the simplest version this can be returned:
ORD.create_class :Industry
Industry.match where:{ name: "Communications" }
=> #<Query:0x00000004309608 @metadata={"type"=>"d", "class"=>nil, "version"=>0, "fieldTypes"=>"Industries=x"}, @attributes={"Industries"=>"#21:1", (...)}>
The attributes are the return-Values of the Match-Query. Unless otherwise noted, the pluralized Model-Classname is used as attribute in the result-set. Note that the Match statement returns a »Query«-record. Its up to the usere, to transform the attributes to Model-Objects. This is done by the »to_orient« directive, ie. »xx.Industries.to_orient «
Industry.match where name: "Communications"
## is equal to
Industry.match( where: { name: 'Communications' }).first.Industries
The Match-Query uses this result-set as start for subsequent queries on connected records. If a linear graph: Industry <- Category <- Subcategory <- Stock is build, Subcategories can accessed starting at Industry defining
var = Industry.match( where: { name: 'Communications'}) do | query |
query.connect :in, count: 2, as: 'Subcategories'
puts query.to_s # print the query prior sending it to the database
query # important: block has to return the query
end
=> MATCH {class: Industry, as: Industries} <-- {} <-- { as: Subcategories } RETURN Industries, Subcategories
The result-set has two attributes: Industries and Subcategories, pointing to the filtered datasets.
By using subsequent »connect« and »statement« method-calls even complex Match-Queries can be constructed.
Other Documents
deactivated behavior: to turn it on, some work on base.rb is required
start.the_edge # --> Array of "TheEdge"-instances connected to the vertex
start.the_edge.where transform_to: 'good' # -> empty array
start.the_edge.where transform_to: 'very bad' #--> previously connectd edge
start.something # 'nice'
end.something # 'not_nice'
start.the_edge.where( transform_to: 'very bad').in.something # => ["not_nice"]
(...)
the_edge.delete # To delete the edge