A Freeze map is a persistent, associative container in which the key and value types can be any primitive or user-defined Slice types. For each pair of key and value types, the developer uses a code-generation tool to produce a language-specific class that conforms to the standard conventions for maps in that language. For example, in C++, the generated class resembles a
std::map, and in Java it implements the
java.util.SortedMap interface. Most of the logic for storing and retrieving state to and from the database is implemented in a Freeze base class. The generated map classes derive from this base class, so they contain little code and therefore are efficient in terms of code size.
You can only store data types that are defined in Slice in a Freeze map. Types without a Slice definition (that is, arbitrary C++ or Java types) cannot be stored because a Freeze map reuses the Ice-generated marshaling code to create the persistent representation of the data in the database. This is especially important to remember when defining a Slice class whose instances will be stored in a Freeze map; only the “public” (Slice-defined) data members will be stored, not the private state members of any derived implementation class.
As illustrated in Figure 39.3, a Freeze map is associated with a single connection and a single database file. Connection and map objects are not thread-safe: if you want to use a connection or any of its associated maps from multiple threads, you must serialize access to them. If your application requires concurrent access to the same database file (persistent map), you must create several connections and associated maps.
Freeze connections provide operations that allow you to begin a transaction, access the current transaction, get the communicator associated with a connection, close a connection, and remove a map index. See the
online Slice reference for more information on these operations.
You may optionally use transactions with Freeze maps. Freeze transactions provide the usual ACID (atomicity, concurrency, isolation, durability) properties. For example, a transaction allows you to group several database updates in one atomic unit: either all or none of the updates within the transaction occur.
You start a transaction by calling beginTransaction on the
Connection object. Once a connection has an associated transaction, all operations on the map objects associated with this connection use this transaction. Eventually, you end the transaction by calling
commit or
rollback:
commit saves all your updates while
rollback undoes them. The
currentTransaction operation returns the transaction associated with a connection, if any; otherwise, it returns nil.
module Freeze {
local interface Transaction {
void commit();
void rollback();
};
local interface Connection {
Transaction beginTransaction();
idempotent Transaction currentTransaction();
// ...
};
};
If you do not use transactions, every non-iterator update is enclosed in its own internal transaction, and every read-write iterator has an associated internal transaction that is committed when the iterator is closed.
ConnectionPtr connection = ...;
TransactionPtr tx = connection‑>beginTransaction();
try {
// DB updates that might throw here...
tx‑>commit();
// More code that might throw here...
} catch (...) {
try {
tx‑>rollback();
} catch (...) {
}
throw;
}
The outer try-catch blocks are necessary because, if the code encounters an exception, we must roll back any updates that were made. In turn, the attempt to roll back might throw itself, namely, if the code following the commit throws an exception (in which case the transaction cannot be rolled back because it is already committed).
Code such as this is difficult to maintain: for example, an early return statement can cause the transaction to be neither committed nor rolled back. The
TransactionHolder class ensures that such errors cannot happen:
namespace Freeze {
class TransactionHolder {
public:
TransactionHolder(const ConnectionPtr&);
~TransactionHolder();
void commit();
void rollback();
private:
// Copy and assignment are forbidden.
TransactionHolder(const TransactionHolder&);
TransactionHolder& operator=(const TransactionHolder&);
};
}
The constructor calls beginTransaction if the connection does not already have a transaction in progress, so instantiating the holder also starts a transaction. When the holder instance goes out of scope, its destructor calls
rollback on the transaction and suppresses any exceptions that the rollback attempt might throw. This ensures that the transaction is rolled back if it was not previously committed or rolled back and ensures that an early return or an exception cannot cause the transaction to remain open:
ConnectionPtr connection = ...;
{ // Open scope
TransactionHolder tx(connection); // Begins transaction
// DB updates that might throw here...
tx.commit();
// More code that might throw here...
} // Transaction rolled back here if not previously
// committed or rolled back.
If you instantiate a TransactionHolder when a transaction is already in progress, it does nothing: the constructor notices that it could not begin a new transaction and turns
commit,
rollback, and the destructor into no‑ops. For example, the nested
TransactionHolder instance in the following code is benign and does nothing:
ConnectionPtr connection = ...;
{ // Open scope
TransactionHolder tx(connection); // Begins transaction
// DB updates that might throw here...
{ // Open nested scope
TransactionHolder tx2(connection); // Does nothing
// DB updates that might throw here...
tx2.commit(); // Does nothing
// More code that might throw here...
} // Destructor of tx2 does nothing
tx.commit();
// More code that might throw here...
} // Transaction rolled back here if not previously
// committed or rolled back.
Connection connection = ...;
Transaction tx = connection.beginTransaction();
try {
// DB updates that might throw here...
tx.commit();
// More code that might throw here...
} catch (java.lang.RuntimeException ex) {
try {
tx.rollback();
} catch (DatabaseException e) {
}
throw ex;
}
The catch handler ensures that the transaction is rolled back before re-throwing the exception. Note that the nested try-catch blocks are necessary: if the transaction committed successfully but the code following the commit throws an exception, the rollback attempt will fail therefore we need to suppress the corresponding
DatabaseException that is raised in that case.
Connection connection = ...;
Transaction tx = connection.beginTransaction();
try {
// DB updates that might throw here...
if (error) {
// ...
return; // Oops, bad news!
}
// ...
tx.commit();
// More code that might throw here...
} catch (java.lang.RuntimeException ex) {
try {
tx.rollback();
} catch (DatabaseException e) {
}
throw ex;
}
The early return statement in the preceding code causes the transaction to be neither committed nor rolled back. To deal with this situation, avoid early return statements or ensure that you either commit or roll back the transaction before returning. Alternatively, you can use a
finally block to ensure that the transaction is rolled back:
Connection connection = ...;
try {
Transaction tx = connection.beginTransaction();
// DB updates that might throw here...
if (error) {
// ...
return; // No problem, see finally block.
}
// ...
tx.commit();
// More code that might throw here...
} finally {
if (connection.currentTransaction() != null)
connection.currentTransaction().rollback();
}
Iterators allow you to traverse the contents of a Freeze map. Iterators are implemented using Berkeley DB cursors and acquire locks on the underlying database page files. In C++, both read-only (
const_iterator) and read-write iterators (
iterator) are available. In Java, an iterator is read-write if it is obtained in the context of a transaction and read-only if it is obtained outside a transaction.
Locks held by an iterator are released when the iterator is closed (if you do not use transactions) or when the enclosing transaction ends. Releasing locks held by iterators is very important to let other threads access the database file through other connection and map objects. Occasionally, it is even necessary to release locks to avoid self-deadlock (waiting forever for a lock held by an iterator created by the same thread).
To improve ease of use and make self-deadlocks less likely, Freeze often closes iterators automatically. If you close a map or connection, associated iterators are closed. Similarly, when you start or end a transaction, Freeze closes all the iterators associated with the corresponding maps. If you do not use transactions, any write operation on a map (such as inserting a new element) automatically closes all iterators opened on the same map object, except for the current iterator when the write operation is performed through that iterator. In Java, Freeze also closes a read-only iterator when no more elements are available.
Read operations in C++ never close iterators automatically: you need to either use transactions or explicitly close the iterator that holds the write lock. This is not an issue in Java because you cannot use an iterator to update a map outside of a transaction.
If you use multiple threads to access a database file, Berkeley DB may acquire locks in conflicting orders (on behalf of different transactions or iterators). For example, an iterator could have a read-lock on page P1 and attempt to acquire a write-lock on page P2, while another iterator (on a different map object associated with the same database file) could have a read-lock on P2 and attempt to acquire a write-lock on P1.
When this occurs, Berkeley DB detects a deadlock and resolves it by returning a “deadlock” error to one or more threads. For all non-iterator operations performed outside any transaction, such as an insertion into a map, Freeze catches such errors and automatically retries the operation until it succeeds. (In that case, the most-recently acquired lock is released before retrying.) For other operations, Freeze reports this deadlock by raising
Freeze::DeadlockException. In that case, the associated transaction or iterator is also automatically rolled back or closed. A properly written application is expected to catch deadlock exceptions and retry the transaction or iteration.
Keys in Freeze maps and indexes are always sorted. By default, Freeze sorts keys according to their Ice-encoded binary representation; this is very efficient but the resulting order is rarely meaningful for the application. Starting with Ice 3.0, Freeze offers the ability to specify your own comparator objects so that you can customize the traversal order of your maps. Note however that the comparator of a Freeze map should remain the same throughout the life of the map. Berkeley DB stores records according to the key order provided by this comparator; switching to another comparator will cause undefined behavior.
In C++, you specify the name of your comparator objects during code generation. The generated map provides the standard features of
std::map, so that iterators return entries according to the order you have defined for the main key with your comparator object. The
lower_bound,
upper_bound, and
equal_range provide range-searches (see the definition of these functions on
std::map).
Apart from these standard features, the generated map provides additional functions and methods to perform range searches using secondary keys. The additional functions are
lowerBoundForMember,
upperBoundForMember, and
equalRangeForMember, where
Member is the name of the secondary-key member. These functions return regular iterators on the Freeze map.
In Java, you supply comparator objects (instances of the standard Java interface java.util.Comparator) at run time when instantiating the generated map class. The map constructor accepts a comparator for the main key and optionally a collection of comparators for secondary keys. The map also provides a number of methods for performing range searches on the main key and on secondary keys (see
page 1559).
Freeze maps support efficient reverse lookups: if you define an index when you generate your map (with
slice2freeze or
slice2freezej), the generated code provides additional methods for performing reverse lookups. If your value type is a structure or a class, you can also index on a member of the value, and several such indexes can be associated with the same Freeze map.
Indexed searches are easy to use and very efficient. However, be aware that an index adds significant write overhead: with Berkeley DB, every update triggers a read from the database to get the old index entry and, if necessary, replace it.
If you later add an index to an existing map, Freeze automatically populates the index the next time you open the map. Freeze populates the index by instantiating each map entry, so it is important that you register the object factories for any class types in your map before you open the map.
Note that the index key comparator of a Freeze map index should remain the same throughout the life of the index. Berkeley DB stores records according to the key order provided by this comparator; switching to another comparator will cause undefined behavior.
The Slice-to-Freeze compiler, slice2freeze, creates C++ classes for Freeze maps. The compiler offers the following command-line options in addition to the standard options described in
Section 4.20:
•
‑‑add‑header HDR[,GUARD]
#ifndef __PRECOMPILED_H__
#define __PRECOMPILED_H__
#include <precompiled.h>
#endif
Modifies #include directives in source files to prepend the path name of each header file with the directory
DIR. See
Section 6.16.1 for more information.
Use SYMBOL to control DLL exports or imports. See the
slice2cpp description for details.
•
‑‑dict NAME,
KEY,
VALUE[,sort[,COMPARE]]
Generate a Freeze map class named NAME using
KEY as key and
VALUE as value. This option may be specified multiple times to generate several Freeze maps.
NAME may be a scoped C++ name, such as
Demo::Struct1ObjectMap. By default, keys are sorted using their binary Ice-encoded representation. Include
sort to sort with the
COMPARE functor class. If
COMPARE is not specified, the default value is
std::less<KEY>.
•
‑‑dict‑index MAP[,MEMBER]
[,case‑sensitive|case‑insensitive][,sort[,COMPARE]]
Add an index to the Freeze map named MAP. If
MEMBER is specified, the map value type must be a structure or a class, and
MEMBER must be a member of this structure or class. Otherwise, the entire value is indexed. When the indexed member (or entire value) is a string, the index can be case-sensitive (default) or case-insensitive. An index adds additional member functions to the generated C++ map:
• int MEMBERCount(MEMBER_TYPE) const;
When MEMBER is not specified, these functions are
findByValue (const and non-const),
lowerBoundForValue (const and non-const),
valueCount, and so on. When
MEMBER is specified, its first letter is capitalized in the
findBy function name.
MEMBER_TYPE corresponds to an in-parameter of the type of
MEMBER (or the type of the value when
MEMBER is not specified). For example, if
MEMBER is a string,
MEMBER_TYPE is a
const std::string&.
findByMEMBER returns an iterator to the first element in the Freeze map that matches the given index value. It returns
end() if there is no match. When the second parameter is true (the default), the returned iterator provides only the elements with an exact match (and then skips to
end()). Otherwise, the returned iterator sets a starting position and then provides all elements until the end of the map, sorted according to the index comparator.
lowerBoundForMEMBER returns an iterator to the first element in the Freeze map whose index value is not less than the given index value. It returns
end() if there is no such element. The returned iterator provides all elements until the end of the map, sorted according to the index comparator.
upperBoundForMEMBER returns an iterator to the first element in the Freeze map whose index value is greater than the given index value. It returns
end() if there is no such element. The returned iterator provides all elements until the end of the map, sorted according to the index comparator.
beginForMEMBER returns an iterator to the first element in the map.
endForMEMBER returns an iterator to the last element in the map.
equalRangeForMEMBER returns a range (pair of iterators) of all the elements whose index value matches the given index value. This function is similar to
findByMEMBER (see above).
MEMBERCount returns the number of elements in the Freeze map whose index value matches the given index value.
•
‑‑index CLASS,TYPE,MEMBER
[,case‑sensitive|case‑insensitive]
Generate an index class for a Freeze evictor (see Section 39.3.7).
CLASS is the name of the class to be generated.
TYPE denotes the type of class to be indexed (objects of different classes are not included in this index).
MEMBER is the name of the data member in
TYPE to index. When
MEMBER has type
string, it is possible to specify whether the index is case-sensitive or not. The default is case-sensitive.
Section 6.16.1 provides a discussion of the semantics of
#include directives that is also relevant for users of
slice2freeze.
$ slice2freeze ‑‑dict StringIntMap,string,int StringIntMap
This command directs the compiler to create a map named StringIntMap, with the Slice key type
string and the Slice value type
int. The final argument is the base name for the output files, to which the compiler appends the
.h and
.cpp suffixes. As a result, this command produces two C++ source files,
StringIntMap.h and
StringIntMap.cpp.
If you examine the contents of the header file created by the example in the previous section, you will discover that a Freeze map is an instance of the template class
Freeze::Map:
The Freeze::Map template class closely resembles the STL container class
std::map, as shown in the following class definition:
namespace Freeze {
template<...> class Map {
public:
typedef ... value_type;
typedef ... iterator;
typedef ... const_iterator;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
Map(const Freeze::ConnectionPtr& connection,
const std::string& dbName,
bool createDb = true,
const Compare& compare = Compare());
template<class _InputIterator>
Map(const Freeze::ConnectionPtr& connection,
const std::string& dbName,
bool createDb,
_InputIterator first, _InputIterator last,
const Compare& compare = Compare());
static void recreate(const Freeze::ConnectionPtr& connection,
const std::string& dbName,
const Compare& compare = Compare());
bool operator==(const Map& rhs) const;
bool operator!=(const Map& rhs) const;
void swap(Map& rhs);
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
bool empty() const;
size_type size() const;
size_type max_size() const;
iterator insert(iterator /*position*/,
const value_type& elem);
std::pair<iterator, bool> insert(const value_type& elem);
template <typename InputIterator>
void insert(InputIterator first, InputIterator last);
void put(const value_type& elem);
template <typename InputIterator>
void put(InputIterator first, InputIterator last);
void erase(iterator position);
size_type erase(const key_type& key);
void erase(iterator first, iterator last);
void clear();
void destroy(); // Non‑standard.
iterator find(const key_type& key);
const_iterator find(const key_type& key) const;
size_type count(const key_type& key) const;
iterator lower_bound(const key_type& key);
const_iterator lower_bound(const key_type& key) const;
iterator upper_bound(const key_type& key);
const_iterator upper_bound(const key_type& key) const;
std::pair<iterator, iterator>
equal_range(const key_type& key);
std::pair<const_iterator, const_iterator>
equal_range(const key_type& key) const;
const Ice::CommunicatorPtr&
communicator() const;
...
};
}
The semantics of the Freeze::Map methods are identical to those of
std::map unless otherwise noted. In particular, the overloaded
insert method shown below ignores the
position argument:
iterator insert(iterator /*position*/,
const value_type& elem);
A Freeze map class supports only those methods shown above; other features of std::map, such as allocators and overloaded array operators, are not available.
Map(const Freeze::ConnectionPtr& connection,
const std::string& dbName,
bool createDb = true,
const Compare& compare = Compare());
template<class _InputIterator>
Map(const Freeze::ConnectionPtr& connection,
const std::string& dbName,
bool createDb,
_InputIterator first, _InputIterator last,
const Compare& compare = Compare());
The recreate function copies an existing database:
static void recreate(const Freeze::ConnectionPtr& connection,
const std::string& dbName,
const Compare& compare = Compare())
The dbName parameter specifies an existing database name. The copy has the name
<dbName>.old-<uuid>. For example, if the database name is
MyDB, the copy might be named
MyDB.old-edefd55a-e66a-478d-a77b-f6d53292b873. (Obviously, a different UUID is used each time you recreate a database).
A Freeze map’s iterator works like its counterpart in std::map. The iterator class supports one convenient (but nonstandard) method:
The program below demonstrates how to use a StringIntMap to store <
string,
int> pairs in a database. You will notice that there are no explicit
read or
write operations called by the program; instead, simply using the map has the side effect of accessing the database.
#include <Freeze/Freeze.h>
#include <StringIntMap.h>
int
main(int argc, char* argv[])
{
// Initialize the Communicator.
//
Ice::CommunicatorPtr communicator =
Ice::initialize(argc, argv);
// Create a Freeze database connection.
//
Freeze::ConnectionPtr connection =
Freeze::createConnection(communicator, "db");
// Instantiate the map.
//
StringIntMap map(connection, "simple");
// Clear the map.
//
map.clear();
Ice::Int i;
StringIntMap::iterator p;
// Populate the map.
//
for (i = 0; i < 26; i++) {
std::string key(1, 'a' + i);
map.insert(make_pair(key, i));
}
// Iterate over the map and change the values.
//
for (p = map.begin(); p != map.end(); ++p)
p.set(p‑>second + 1);
// Find and erase the last element.
//
p = map.find("z");
assert(p != map.end());
map.erase(p);
// Clean up.
//
connection‑>close();
communicator‑>destroy();
return 0;
}
The second argument is the name of a Berkeley DB database environment; by default, this is also the file system directory in which Berkeley DB creates all database and administrative files. Note that properties with the prefix
Freeze.DbEnv can modify a number of environment settings (see
page 1946), including the file system directory. For the preceding example, you could change the directory to
FreezeDir by setting the property
Freeze.DbEnv.db.DbHome to
FreezeDir.
Next, the code instantiates the StringIntMap on the connection. The constructor’s second argument supplies the name of the database file, which by default is created if it does not exist:
Iterating over the map will look familiar to std::map users. However, to modify a value at the iterator’s current position, we use the nonstandard
set method:
The Slice-to-Freeze compiler, slice2freezej, creates Java classes for Freeze maps. The compiler offers the following command-line options in addition to the standard options described in
Section 4.20:
Generate a Freeze map class named NAME using
KEY as key and
VALUE as value. This option may be specified multiple times to generate several Freeze maps.
NAME may be a scoped Java name, such as
Demo.Struct1ObjectMap.
•
‑‑dict‑index MAP[,MEMBER]
[,case‑sensitive|case‑insensitive]
Add an index to the Freeze map named MAP. If
MEMBER is specified, the map value type must be a structure or a class, and
MEMBER must be the name of a member of that type. If
MEMBER is not specified, the entire value is indexed. When the indexed member (or entire value) is a string, the index can be case-sensitive (default) or case-insensitive. See
page 1565 for more information on the Freeze map API for indices.
•
‑‑index CLASS,
TYPE,
MEMBER
[,case‑sensitive|case‑insensitive]
Generate an index class for a Freeze evictor (see Section 39.3.7).
CLASS is the name of the index class to be generated.
TYPE denotes the type of class to be indexed (objects of different classes are not included in this index).
MEMBER is the name of the data member in
TYPE to index. When
MEMBER has type
string, it is possible to specify whether the index is case-sensitive or not. The default is case-sensitive.
Define the global metadata directive META. Using this option is equivalent to defining the global metadata
META in each named Slice file, as well as in any file included by a named Slice file.
$ slice2freezej ‑‑dict StringIntMap,string,int
This command directs the compiler to create a map named StringIntMap, with the Slice key type
string and the Slice value type
int. The compiler produces one Java source file:
StringIntMap.java.
In addition to the ant task for executing slice2java, Ice also includes an ant task for executing
slice2freezej. The classes for
Slice2FreezeJTask are stored in the same JAR file (
ant-ice.jar) as
Slice2JavaTask. Both tasks also share the same logic for locating a compiler in your execution environment and for managing dependencies between Slice files; refer to
Section 10.18.2 for more information.
The Slice2FreezeJTask supports the parameters listed in
Table 39.1:
To enable the Slice2FreezeJTask in your ant project, define the following
taskdef element in your project’s build file:
This configuration assumes that ant-ice.jar is already present in ant’s class path. Alternatively, you can specify the JAR explicitly as follows:
<target name="generate" depends="init">
<mkdir dir="generated"/>
<slice2java outputdir="generated">
<fileset dir="." includes="Library.ice"/>
</slice2java>
<slice2freezej ice="on" outputdir="generated">
<fileset dir="/opt/Ice/slice/Ice"
includes="BuiltinSequences.ice"/>
<fileset dir="." includes="Library.ice"/>
<dict name="StringIsbnSeqDict" key="string"
value="Ice::StringSeq"/>
</slice2freezej>
</target>
This invocation of the slice2freezej task enables the
ice option because the generated Freeze map relies on a type that is defined in an Ice namespace and therefore loads the Slice file
BuiltinSequences.ice directly.
The class generated by slice2freezej implements the
Freeze.Map interface, as shown below:
package Freeze;
public interface Map<K, V> extends NavigableMap<K, V>
{
void fastPut(K key, V value);
void close();
int closeAllIterators();
void destroy();
public interface EntryIterator<T>
extends java.util.Iterator<T>
{
void close();
void destroy(); // an alias for close
}
}
The Map interface implements standard Java interfaces and provides nonstandard methods that improve efficiency and support database-oriented features.
Map defines the following methods:
Map inherits much of its functionality from the
Freeze.NavigableMap interface, which derives from the standard Java interface
java.util.SortedMap and also supports a subset of the
java.util.NavigableMap1 interface from Java6:
package Freeze;
public interface NavigableMap<K, V>
extends java.util.SortedMap<K, V>
{
java.util.Map.Entry<K, V> firstEntry();
java.util.Map.Entry<K, V> lastEntry();
java.util.Map.Entry<K, V> ceilingEntry(K key);
java.util.Map.Entry<K, V> floorEntry(K key);
java.util.Map.Entry<K, V> higherEntry(K key);
java.util.Map.Entry<K, V> lowerEntry(K key);
K ceilingKey(K key);
K floorKey(K key);
K higherKey(K key);
K lowerKey(K key);
java.util.Set<K> descendingKeySet();
NavigableMap<K, V> descendingMap();
NavigableMap<K, V> headMap(K toKey, boolean inclusive);
NavigableMap<K, V> tailMap(K fromKey, boolean inclusive);
NavigableMap<K, V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive);
java.util.Map.Entry<K, V> pollFirstEntry();
java.util.Map.Entry<K, V> pollLastEntry();
boolean fastRemove(K key);
}
The NavigableMap interface provides a number of useful methods:
Many of these methods raise UnsupportedOperationException if you fail to construct the Freeze map using a custom comparator object. The only exceptions are
firstEntry,
lastEntry,
pollFirstEntry,
pollLastEntry, and
fastRemove. (The same applies to
NavigableMap objects created for secondary keys.)
Note that NavigableMap also inherits overloaded methods named
headMap,
tailMap, and
subMap from the
SortedMap interface. These methods have the same semantics as the ones defined in
NavigableMap but they omit the boolean arguments (refer to the JDK documentation for complete details). Although these methods are declared as returning a
SortedMap, the actual type of the returned object is a
NavigableMap that you can downcast if necessary.
public class StringIntMap extends ...
// implements Freeze.Map<String, Integer>
{
public StringIntMap(
Freeze.Connection connection,
String dbName,
boolean createDb,
java.util.Comparator<String> comparator);
public StringIntMap(
Freeze.Connection connection,
String dbName,
boolean createDb);
public StringIntMap(
Freeze.Connection connection,
String dbName);
}
StringIntMap derives from an internal Freeze base class that implements the interface
Freeze.Map<String, Integer>. The generated class defines several overloaded constructors whose arguments are described below:
You can iterate over a Freeze map just as you can with any container that implements the
java.util.Map interface. For example, the code below displays the key and value of each element:
StringIntMap m = new StringIntMap(...);
java.util.Iterator<java.util.Map.Entry<String, Integer>> i =
m.entrySet().iterator();
while (i.hasNext()) {
java.util.Map.Entry<String, Integer> e = i.next();
System.out.println("Key: " + e.getKey());
System.out.println("Value: " + e.getValue());
}
Generally speaking, a program should close an iterator when it is no longer necessary for the reasons given in
Section 39.5.3. (An iterator that is garbage collected without being closed emits a warning message.) However, an explicit close was not necessary in the preceding example because Freeze automatically closes a read-only iterator when it reaches the last element (a read-only iterator is one that is opened outside of any transaction). If instead our program had stopped using the iterator prior to reaching the last element, an explicit close would have been necessary:
StringIntMap m = new StringIntMap(...);
java.util.Iterator<java.util.Map.Entry<String, Integer>> i =
m.entrySet().iterator();
while (i.hasNext()) {
java.util.Map.Entry<String, Integer> e = i.next();
System.out.println("Key: " + e.getKey());
System.out.println("Value: " + e.getValue());
if (e.getValue().intValue() == 5)
break;
}
((Freeze.Map.EntryIterator)i).close();
Closing the iterator requires downcasting it to a Freeze-specific interface named Freeze.Map.EntryIterator. The definition of this interface was shown in the previous section.
Freeze maps also support the enhanced for loop functionality in Java5. Here is a simpler way to write our original program:
StringIntMap m = new StringIntMap(...);
for (java.util.Map.Entry<String, Integer> e : m.entrySet()) {
System.out.println("Key: " + e.getKey());
System.out.println("Value: " + e.getValue());
}
As in the first example, Freeze automatically closes the iterator when no more elements are available. Although the enhanced
for loop is convenient, it is not appropriate for all situations because the loop hides its iterator and therefore prevents the program from accessing the iterator in order to close it. In this case, you can use the traditional
while loop instead of the
for loop, or you can invoke
closeAllIterators on the map as shown below:
StringIntMap m = new StringIntMap(...);
for (java.util.Map.Entry<String, Integer> e : m.entrySet()) {
System.out.println("Key: " + e.getKey());
System.out.println("Value: " + e.getValue());
if (e.getValue().intValue() == 5)
break;
}
int num = m.closeAllIterators();
assert(num <= 1); // The iterator may already be closed.
The closeAllIterators method returns an integer representing the number of iterators that were actually closed. This value can be useful for diagnostic purposes, such as to assert that a program is correctly closing its iterators.
Using the --dict-index option to define an index for a secondary key causes
slice2freezej to generate the following additional code in a Freeze map:
We discuss each of these additions in more detail below. In this discussion, MEMBER refers to the optional argument of the
--dict-index option, and
MEMBER_TYPE refers to the type of that member. As explained earlier, if
MEMBER is not specified,
slice2freezej creates an index for the value type of the map. The sample code presented in this section assumes we have generated a Freeze map using the following command:
$ slice2freezej ‑‑dict StringIntMap,string,int \
--dict-index StringIntMap
By default, index keys are sorted using their binary Ice-encoded representation. This is an efficient sorting scheme but does not necessarily provide a meaningful traversal order for applications. You can choose a different order by providing an instance of the
IndexComparators class to the map constructor. This class has a public data member holding a comparator (an instance of
java.util.Comparator<MEMBER_TYPE>) for each index in the map. The class also provides an empty constructor as well as a convenience constructor that allows you to instantiate and initialize the object all at once. The name of each data member is
MEMBERComparator. If
MEMBER is not specified, the
IndexComparators class has a single data member named
valueComparator. Note that much of the functionality offered by a map index requires that you provide a custom comparator.
Here is the definition of IndexComparators for
StringIntMap:
public class StringIntMap ... {
public static class IndexComparators {
public IndexComparators() {}
public IndexComparators(
java.util.Comparator<Integer> valueComparator);
public java.util.Comparator<Integer> valueComparator;
}
...
}
To instantiate a Freeze map using your custom comparators, you must use the overloaded constructor that accepts the
IndexComparators object. For our
StringIntMap, this constructor has the following definition:
public class StringIntMap ... {
public StringIntMap(
Freeze.Connection connection,
String dbName,
boolean createDb,
java.util.Comparator<String> comparator,
IndexComparators indexComparators);
...
}
java.util.Comparator<String> myMainKeyComparator = ...;
StringIntMap.IndexComparators indexComparators =
new StringIntMap.IndexComparators();
indexComparators.valueComparator = ...;
StringIntMap m =
new StringIntMap(connection, "stringIntMap", true,
myMainKeyComparator, indexComparators);
If you later need to change the index configuration of a Freeze map, you can use one of the
recreate methods to update the database. Here are the definitions from
StringIntMap:
public class StringIntMap ... {
public static void recreate(
Freeze.Connection connection,
String dbName,
java.util.Comparator<String> comparator);
public static void recreate(
Freeze.Connection connection,
String dbName,
java.util.Comparator<String> comparator,
IndexComparators indexComparators);
...
}
The first overloading is generated for every map, whereas the second overloading is only generated when the map has at least one index. As its name implies, the
recreate method creates a new copy of the database. More specifically, the method removes any existing indices, copies every key-value pair to a temporary database, and finally replaces the old database with the new one. As a side-effect, this process also populates any remaining indices. The first overloading of
recreate is useful when you have regenerated the map to remove the last index and wish to clean up the map’s database state.
slice2freezej also generates a number of index-specific methods. The names of these methods incorporate the member name (
MEMBER), or use
value if
MEMBER is not specified. In each method name, the value of
MEMBER is used unchanged if it appears at the beginning of the method’s name. Otherwise, if
MEMBER is used elsewhere in the method name, its first letter is capitalized. The index methods are described below:
Returns an iterator over elements of the Freeze map starting with an element with whose index value matches the given index value. If there is no such element, the returned iterator is empty (
hasNext always returns false). When the second parameter is true (or is not provided), the returned iterator provides only “duplicate” elements, that is, elements with the very same index value. Otherwise, the iterator sets a starting position in the map, and then provides elements until the end of the map, sorted according to the index comparator. Any attempt to modify the map via this iterator results in an
UnsupportedOperationException.
•
public NavigableMap<MEMBER_TYPE, Set<Map.Entry<K, V>>>
headMapForMEMBER(MEMBER_TYPE to, boolean inclusive)
public NavigableMap<MEMBER_TYPE, Set<Map.Entry<K, V>>>
headMapForMEMBER(MEMBER_TYPE to)
•
public NavigableMap<MEMBER_TYPE, Set<Map.Entry<K, V>>>
tailMapForMEMBER(MEMBER_TYPE from, boolean inclusive)
public NavigableMap<MEMBER_TYPE, Set<Map.Entry<K, V>>>
tailMapForMEMBER(MEMBER_TYPE from)
•
public NavigableMap<MEMBER_TYPE, Set<Map.Entry<K, V>>>
subMapForMEMBER(MEMBER_TYPE from, boolean fromInclusive,
MEMBER_TYPE to, boolean toInclusive)
public NavigableMap<MEMBER_TYPE, Set<Map.Entry<K, V>>>
subMapForMEMBER(MEMBER_TYPE from, MEMBER_TYPE to)
For the methods returning a NavigableMap, the key type is the secondary key type and the value is the set of matching key-value pairs from the Freeze map. (For the sake of readability, we have omitted the
java.util prefix from
Set and
Map.Entry.) In other words, the returned map is a mapping of the secondary key to all of the entries whose value contains the same key. Any attempt to add, remove, or modify an element via a sub map view or an iterator of a sub map view results in an
UnsupportedOperationException.
Note that iterators returned by the findByMEMBER methods, as well as those created for sub map views, may need to be closed explicitly, just like iterators obtained for the main Freeze map. (See
page 1564 for more information.)
public Freeze.Map.EntryIterator<Map.Entry<String, Integer>>
findByValue(Integer index);
public Freeze.Map.EntryIterator<Map.Entry<String, Integer>>
findByValue(Integer index, boolean onlyDups);
public int valueCount(Integer index);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
headMapForValue(Integer to, boolean inclusive);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
headMapForValue(Integer to);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
tailMapForValue(Integer from, boolean inclusive);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
tailMapForValue(Integer from);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
subMapForValue(Integer from, boolean fromInclusive,
Integer to, boolean toInclusive);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
subMapForValue(Integer from, Integer to);
public NavigableMap<Integer, Set<Map.Entry<String, Integer>>>
mapForValue();
The program below demonstrates how to use a StringIntMap to store <
string,
int> pairs in a database. You will notice that there are no explicit
read or
write operations called by the program; instead, simply using the map has the side effect of accessing the database.
public class Client
{
public static void
main(String[] args)
{
// Initialize the Communicator.
//
Ice.Communicator communicator = Ice.Util.initialize(args);
// Create a Freeze database connection.
//
Freeze.Connection connection =
Freeze.Util.createConnection(communicator, "db");
// Instantiate the map.
//
StringIntMap map =
new StringIntMap(connection, "simple", true);
// Clear the map.
//
map.clear();
int i;
// Populate the map.
//
for (i = 0; i < 26; i++) {
final char[] ch = { (char)('a' + i) };
map.put(new String(ch), i);
}
// Iterate over the map and change the values.
//
for (java.util.Map.Entry<String, Integer> e :
map.entrySet()) {
Integer in = e.getValue();
e.setValue(in.intValue() + 1);
}
// Find and erase the last element.
//
boolean b;
b = map.containsKey("z");
assert(b);
b = map.fastRemove("z");
assert(b);
// Clean up.
//
map.close();
connection.close();
communicator.destroy();
System.exit(0);
}
}
The second argument is the name of a Berkeley DB database environment; by default, this is also the file system directory in which Berkeley DB creates all database and administrative files.
Next, the code instantiates the StringIntMap on the connection. The constructor’s second argument supplies the name of the database file, and the third argument indicates that the database should be created if it does not exist:
We populate the map, using a single-character string as the key. As with java.util.Map, the key and value types must be Java objects but the compiler takes care of autoboxing the integer argument: