TweetFollow Us on Twitter

Tightwads Flat File db
Volume Number:12
Issue Number:12
Column Tag:Tools Of The Trade

A Tightwad’s Guide to Flat File Databases

Working with relations in a flat file data base engine

by Edward Ringel, Waterville, Maine

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

By Way of Introduction

One of the most naive, chutzpah-riddled tasks I ever set for myself was trying to build an accounts receivable program for my medical practice in my spare time after reading the Omnis 3 manual and tutorial. After spending a lot of time (and obviously not an inconsiderable sum on this program) I gave up, got wise and bought Tess 2.0, which we’ve used happily through to the present version of 4.4. It is interesting that this fiasco propelled me 1) to program in a language, rather than a database, so that I had complete control of the environment and 2) to think a bit about databases and database design.

Programming to solve database problems using a general purpose computer language inevitably turns one’s attention to database engines and source code libraries to lighten the programming load. While the ads for many of these products promise all kinds of relational muscle, a lot of serious programming tasks don’t need that kind of horsepower. In the course of completing several programming tasks of interest to me, I’ve developed some techniques for using “less powerful” flat file database engines that may be of general interest. For some of you, this may be fairly elementary; for others enlightening. Please bear with me.

Tools and Questions

When I try to solve any problem, I try to conceive the question as carefully as possible and then look at the tools available that might be useful. For the purpose of this discussion, let’s alter the normal thought process and start with the tools. The high priced libraries are expensive for several good reasons.

These database engines are powerful. They can handle thousands, if not millions of records. They can do wonderfully fast searches. They are optimized to read and write keys and data as fast as the OS will permit. They do extensive error checking and error correction. They permit the arbitrary changing of record size, key size, etc. They permit the creation of relational tables and thus the creation of relational databases. Some interface with object oriented paradigms.

Does Your Problem Really Need a Tool This Powerful?

Most of us write databases that handle hundreds or thousands of records, not millions. Most of us write programs where very fast search, read, and write functions are not critical because there is a user sitting there who can’t type that fast. I’m old fashioned about error checking; I don’t feel as though I can ever turn down responsibility for this aspect of programming. Good planning can obviate the need for after-the-fact changes to database structure, and even better planning can allow for it. Relational tables, while useful, are expensive in terms of efficiency and disk space, and besides 1) a lot of really important information in our lives is flat in structure, and 2) the data can be represented and interrelated with a simpler scheme than a relational table. Object persistence is neat, and I don’t know how to write an object persistence scheme; for all I know maybe you can’t come close to NeoAccess or its cousins with what I’m going to propose. Maybe I’m way off base. However, nothing in my thinking precludes a database of objects where constructor/initializer routines reestablish object relationships as the objects are loaded.

Why a Flat File Database Library?

It is time inefficient, error prone, and hard to write a set of routines that manages keys, records, and files. While it may be hard to justify the cost of some of the high end products, it is equally hard to justify the enormous investment of time and hassle in writing your own set of database routines. A flat file data base engine is an excellent compromise between cost and features. This tool is reasonably efficient, and is within most programmers’ budgets. It supplies you with the code that is the most difficult to write. There will still be an investment of time and energy that will be necessary, but it won’t be nearly as dear as starting from scratch. This kind of library has three main features:

• A scheme for record allocation, deletion, and reuse of deleted record file space.

• A record indexing scheme. This is usually some sort of b-tree, but can be a hash table or some other indexing system.

• Extensive error checking during record and key operations, and result codes that indicate 1) whether the database engine did what you asked it to do and 2) if there was an operating system I/O error.

These basic functions can be used very successfully to manage relatively complex data sets. I am going to assume that anyone reading this article understands the basic concepts of record and key operations.

I use a library called B-Tree Helper, from (m)agreeable software in Plymouth, Minnesota (Magreeable@aol.com). The current version is 2.2. This product comes as C source code. The function calls are straightforward. Mel Magree is delightfully old fashioned in that he actually includes a hard copy manual for no extra charge! The product can handle fixed and variable length records, many indices, and is very Mac-like in that a single physical file on disk can contain multiple index and record types; no separate index and record file is needed for each logical grouping of data (unless you wish to do so.)

Some of the earlier incarnations of B-Tree Helper had some clumsy function calls and too many pointers to too many temporary results. The current version is very nice and well worth the upgrade if you are an owner of an older version. This product is used for the demonstration application. The source code provided for the demo will compile only if you supply the .c files to B-Tree Helper; the .h files for the library are provided. B-Tree Helper is a robust, good product and has met my needs nicely. There are other flat file engines available, but this article is not a product review. The reader can easily find and compare different products.

Invoicing Revisited

To make my points, and to demonstrate some techniques, I would like to revisit the model problem of invoicing. This is a standard database problem that some of you may have encountered previously in examples or classes. The classical invoicing structure, at its simplest, contains four different types of records, each contained within its own file. Figure 1 shows the basic interrelationships of data.

Figure 1.

There is a customer, a part, an invoice, and a line item. The customer record contains data about the customer, how much he/she owes, etc. The parts record contains information about each item being sold. The invoice is an open-ended structure that tells us about who the customer is, and the various items sold to the customer at that interaction. Each line item consists of the part, the number of parts sold, the price, etc.

When there is a sale, the program would create an invoice, n number of line items, and relate the invoice and line items to pre-existing records that represented customer and part information. Each line item structure contains a reference to the part information for that line item. This can be done either through a relational table, or it may be done simply by embedding a reference to the part information in the line item. When a line item record is retrieved, the application is written so that the information about the part reference is retrieved.

I solve this problem without a relational database. The customer does not need to have a direct relation to a line item; the invoice does that for him/her. Similarly, the customer needs no relation to the parts catalog. With some planning, and understanding of the information problem, a series of special indices and embedded links can be constructed that give you all the data interrelations that you will need. In any setting where the interrelationships are fixed, and the data queries are predictable and have been anticipated, a combination of careful programming and good data structures are a powerful data management system.

Don’t Scare Your Customers - Think Ahead

This kind of programming requires a lot of forethought. Unlike an interface, it is hard to create data structures on the fly. It is critical to have a clear vision of the problem and how you are going to solve it, because you are hard-coding data relationships. It will be difficult for you, and frightening for your user (yes, frightening) when you twiddle an existing file to “add some stuff.” Why frightening? I’ll use myself as an example. My accounts receivable program handles billing and payments from insurance companies and from patients. I literally eat and pay my bills from the information in this file. Tess 4.4 and its programmer(s) are great.

However, what if this alert came up on the screen when I installed an upgrade? “Make sure you have three clean backup copies of your current data because we’re going to twiddle your file structure and add some stuff we forgot. We’re going to completely tear apart your file structure and rebuild it, from scratch, on the fly. Don’t do this operation during a thunderstorm. In fact, don’t even breath near the computer for the next couple of hours. This will take half an hour per megabyte of data on a Power Mac 9500. Press OK to continue.”

You and your users will need to live with your decisions for a long time. It may be worthwhile thinking more like a civil engineer in a litigious society than a software engineer trying to be elegant: overbuild, overbuild, overbuild. Talk to your end user(s). Know the problem before you try to solve it. Build in extensibility. We will see how to do that in a few paragraphs.

Invoice Example and B-Tree Helper in Detail

My example program, which is cleverly named Invoice Example, and B-Tree Helper deserve some explanation. Invoice Example is based loosely on some sample code from DTS, which I used as a framework to handle three modeless dialogs, a couple of menus, a bunch of alerts and a single modal dialog. The interface is not elegant, but it gets the job done. I have separated the code into GUI related files and function related files. The user interface code is fairly pedestrian and I have no secret techniques; the meat of this article is in the folder called “functional files.” Header files for B-Tree Helper are included, but obviously not the source files. A compiled 68K application with an example data file is available for download and on the subscription disk. The source code and the application genuinely complement this article. However, some of the important routines are long and are not well suited to hard copy presentation. Please review the source code (obtained from disk or ‘net) or many of the points may be murky.

To perform an operation, the user selects from a popup menu in the window. This sets the mode (find, next, insert, edit, etc.). When all of the data is ready, the user clicks the Do It button. In the Invoice window, two steps are required for insertion. First the user enters references to customer and parts and then clicks the Load Related button. This checks for valid data, does some math, and then loads the related records. Second, the user clicks the Do It button. Searches and deletions of invoices are simpler and require only a mode selection and a Do It click. There is a window each for Customer, Invoice, and Part operations. The fourth window is for a separate example which we will get to later.

The program reads and writes data to a file, and then uses embedded information or appropriate keys and indices to recall related information. The precise mechanism for this is in the commented sample code. We use B-Tree Helper for all file related functions with the exception of some resource calls.

B-Tree Helper consists of 40 calls that create, open and close files, insert and delete records, insert and delete keys, do complex searches, manage record buffering, add and delete index trees, and permit retrieval of raw page data for debugging. To create a file, the main programming task is the initialization of a series of index trees, which are then passed to the creation routine. These trees represent your index structures.

The other major planning task is to determine file block size (space in the file is allocated by blocks). The size of the block determines the maximum record size, because B-Tree Helper uses control records interspersed with your data to manage available file space. These control records are immovable islands in your data stream, and your records must fit between them. Thus, you must calculate your allocation block size so that your maximum sized data can fit between the control records. Although B-Tree Helper allows variable length records, you will hit a size ceiling of 8*fileBlockSize*(fileBlockSize-8) bytes unless you split your data into chunks (which is not the end of the world either). Upon successfully creating a file, a FileControlHandle - the record that manages the file while open - is returned and is used in virtually every other call to the B-Tree Helper library.

Interestingly, B-Tree Helper does not require formal definition of a record; you simply request n bytes of data from the file. If successful in allocating this block of file space, you are given a success result code and a file address. You can then write and subsequently read data from this address. This address is a four byte long and is also passed to key insertion functions. When a key is searched for and found, the key is returned and so is the file address. However, it is not required to pass a file address to the key. Any four byte value is legal, and this can be used very much to our advantage, as will be shown later. Because records are not defined, keys are also not irretrievably linked to a record type or logical file. This can also be used to our advantage. Finally, because a record is not specifically defined as to content or size, many kinds of records can coexist in a single file. Thus, I think of a B-Tree Helper file as a physical file enclosing any number of logical files, index structures, and map control records (Figure 2) B-Tree Helper is very flexible in this manner.

Figure 2.

Reduce Flexibility Creatively

When I have tackled more involved problems than Invoice Example, I have taken the time to reduce flexibility and create structures that describe each index and record type. This has saved me an enormous amount of headache when debugging and writing code.

Review of the B-Tree Helper functions shows that there is great similarity among the calls of a given function class; key related functions and record related functions all pretty much take the same kind of parameters. I have usually written parameter block structs that I then pass to a custom function that is a wrapper for the B-Tree Helper call. This relieves me of getting the parameters and degree of indirection right each time I call Insert_Key() or Find_Equal(). Additionally, I declare these parameter block structures as global or static. Just as each logical file in your database has an “active” or “current” record (i.e. a record that is loaded into memory and being operated upon), there is a corresponding active key and corresponding active file address. Although I usually separate the actual data from the parameter block, having current keys and current file addresses that can survive between function calls can be very useful, especially when doing successive operations upon multiple logical files. This is demonstrated by supporting Next and Previous functions in the example program.

I am not heavily into object oriented programming, but writing a series of wrapper classes for B-Tree Helper seems like a very sensible, productive thing for you C++ buffs out there to do. In particular, C++’s multiple inheritance would permit creating complex objects that could respond to a single command to insert a record, a key, and then link to a related record.

Making Relationships Work

Records with embedded addresses

I have repeatedly referred to linking to related records. How do we do this? I use three basic techniques.

First embed a file address into a record. Let’s examine the declaration for the CustomerType struct:

struct CustomerType{
 unsigned char   CustName[32];
 long   CustNo;
 long   StringAddr;
 };
typedef struct CustomerType CustomerType;
 unsigned char   gCustString[40];
 CustomerType  gCustomer;

I thought CustomerType had the information we need for the example; however I “forgot” to allow room for a comment. Luckily, I had thought ahead and had left an extra four byte field in CustomerType, which I then renamed StringAddr. After we retrieve all the information from the dialog, we Get_Bytes() for gCustString first, and Write_Data() from the memory address of gCustString to the file address returned by Get_Bytes(). We then set gCustomer.StringAddr to the file address returned by Get_Bytes() as well. Then, and only then, do we allocate space and write the data for the parent gCustomer record.

When information is retrieved, we follow the reverse sequence. We Read_Data() and get gCustomer first. We then Read_Data() using the file address stored in gCustomer.StringAddr to get the child gCustString.

I use this technique when the referenced record is truly a child record - it will never be looked for unto itself. This is a nice way of gracefully adding to a record which was missdeclared, and is a prime way of adding record extensibility. For this reason, I will generally add an extra unused four byte field to just about any record I declare in a “serious” program. This is one of the simplest ways of overbuilding your software to keep data integrity at a maximum. Had we used this scheme and needed to make an ex post facto change to an existing file, my update routine would have read into memory the parent record, allocated and wrote the child record (the new stuff), set the correct field of the parent record with the file address of the child record, and rewrote the parent record to file. No reindexing or reconstitution of existing data would have been necessary.

Sequence numbers to the rescue

This is a nice, simple system. It works well. Why limit its use to a strict parent/child relationship? The problem is that of editing or updating data. If gCustString were a record that had multiple links, and the file address of gCustString’s data were to change (which can easily happen when a record is edited or updated), I would need to find every record that had a link to the changed record and insert the new file address. There are two remedies to this problem. One is to create a pseudo file address that remains constant regardless of the true file address of the record, but always points to the true file address (like a handle). This strategy is used by DynaBase, a flat file database engine which is no longer commercially available. In DynaBase, all file addresses were pseudo addresses, adding overhead where none was needed.

The second approach is to use a sequence number; at allocation each record is assigned a number which is that record’s and only that record’s, never to change for the life of the file. There are a number of ways to issue sequence numbers. For the demo, I used a custom resource that is a handle to a small record whose fields are incremented whenever a record is allocated. This technique is shown in the example program and I will not dwell on it here. It is equally easy to put a sequence number dispatching record in the data fork; using B-Tree Helper to make a Get_Bytes() call for your sequence dispatching record and put the resulting file address into one of the .application1..4 fields of the FileControlHandle and the address of the record will survive across work sessions. See the Picture Creator demo for a simple example of this technique.

If we examine the declaration for the invoice struct, we see that the line item fields are in fact long integers:

struct InvoiceType {
 long   InvoiceNumber;
 long   LineItem1;
 long   LineItem2;
 long   CustNo;
 };

typedef struct InvoiceType InvoiceType;
InvoiceType gInvoice;

After each line item record has been filled in by the user, the program goes to work. A sequence number is issued to that line item record, and the appropriate field in the line item record is filled in with that sequence number. Second, that sequence number is inserted into field LineItem1 or LineItem2. Third, a key is inserted in an index tree, with the sequence number as the key pointing to the file address of the line item record.

This second way of relating a record, that is, to embed the sequence number of a record into the body of a related record, is shown in Figure 3.

Figure 3.

When an invoice is retrieved, the invoice record is retrieved first. A search is performed on the sequence number in either or both of the two line item fields in the invoice record. The file address that is retrieved is the file address of the line item to be read into memory, and a Read_Data() is performed.

This example is trivial, and each line item record belongs to one and only one invoice. Correcting the invoice record for a change in file address of the line item would be easy even if a sequence number had not been issued. However, more complex data structures may have a record that links to many other records, and each record would have to be brought into memory, fixed, and rewritten to disk. That’s a lot of error prone work. With B-Tree Helper, there is a slick function called Change_Address(), which will fix the address of a key when there is a file address change. Even if Change_Address() did not exist, this is only a few lines of code.

Indirect Index Trees

The final way to relate records is with the use of a key from one record pointing to the sequence number of a related record (Figure 4).

Figure 4.

One of the functions we have in Invoice Example is to find the invoices belonging to a customer. Using the name of the customer as the key, we insert this key pointing not to a file address, but to a long that is the sequence number for an invoice belonging to the customer - B-Tree Helper will accept any long for the “file address” portion of the key. We then use this retrieved sequence number as the key to another index where the sequence number key now points to a real file address. Thus, two key lookups must be performed for this search, but the relationship between the customer record and the invoice record will survive file address changes of the invoice nicely if just the second key is kept updated. This kind of relationship is very useful when a record may have a relation to many other relations.

For so-called “many-to-many” relationships, a series of intermediary relating records, consisting of just sequence numbers with appropriately directed keys, can fill the bill. Implementation of this solution is left as an exercise for the reader, which translated into plain English means that you may want to rethink buying one of the more complete solutions if your problem is very hairy. Just remember though, the invoice record that we created is in fact the very kind of record with pointers to other records in other files. Maybe it isn’t as hard as you thought.

General Data Management with a Database Library

The last idea on the creative use of flat file database engines has nothing to do with databases, just plain information management. Although some of this discourse may be rendered obsolete by OpenDoc, consider the problem of the complex document. Several years ago I wrote a program that did extensive data crunching of information on patients’ sleep. There were half a dozen header records of fixed length, and 7 arrays that could vary anywhere between a couple of hundred and several thousand bytes a piece, depending on the severity of the sleep disturbance. Rather than reinvent the wheel to save this complex document to disk, I simply used a database manager. I viewed each piece of information as a variable length record within a logical file. I let the database do all the insertions, deletions, and updates for me, rather than writing routines from scratch. With the arrays varying so greatly in size (I saved the arrays as a single block, rather than element by element), it was much easier to use generic pre-built routines that handled variable length records.

As an example of this solution, I have made a fourth dialog window that opens a picture of Joshua Chamberlain (one of our most famous Mainers) and a short text blurb about him. While this could have been saved as a couple of resources (and in fact the example file was created from resources), the picture and text are in the data fork of a B-Tree Helper file. This is one of the most intriguing uses of a database engine, particularly for the independent, small project programmer.

My last point to make has to do with error checking. Do it. Do it a lot. My example doesn’t do it half enough. The only specific advice I have is to insert data first and keys second. It is much worse to have a key pointing to nowhere than an orphan record, and it makes the cleanup procedure after a failed read/write much easier.

Punch Line

To summarize, here are the salient points of this article:

• Not every database problem needs a relational database.

• Use a flexible flat file database manager as your major tool for data management.

• Consider and construct your data relations very carefully.

• Add an extra four byte field or two to your record declarations. They may come in handy later.

• Consider writing custom functions around the database toolbox calls to improve readability, reduce errors during programming, and make file management easier. If working in an object oriented environment, consider building a series of good quality wrapper classes for the function/parameter block entities that can be reused.

• For simple parent-child data relations, simply embed the address of the child in the parent record.

• Use embedded sequence numbers for relatively simple relations where the child record may be moved within the file.

• Use keys from one record pointing the sequence number of another record to create complex data relations.

• Consider using database engines to manage complex documents.

• Error check till you can’t stand the sight of an if (myErr != noErr) clause.

May you all be spared the ignominy of my first project.

 
AAPL
$100.96
Apple Inc.
-0.83
MSFT
$47.52
Microsoft Corpora
+0.84
GOOG
$596.08
Google Inc.
+6.81

MacTech Search:
Community Search:

Software Updates via MacUpdate

Airfoil 4.8.9 - Send audio from any app...
Airfoil allows you to send any audio to AirPort Express units, Apple TVs, and even other Macs and PCs, all in sync! It's your audio - everywhere. With Airfoil you can take audio from any... Read more
WhatRoute 1.13.0 - Geographically trace...
WhatRoute is designed to find the names of all the routers an IP packet passes through on its way from your Mac to a destination host. It also measures the round-trip time from your Mac to the... Read more
Chromium 37.0.2062.122 - Fast and stable...
Chromium is an open-source browser project that aims to build a safer, faster, and more stable way for all Internet users to experience the web. FreeSMUG-Free OpenSource Mac User Group build is... Read more
Attachment Tamer 3.1.14b9 - Take control...
Attachment Tamer gives you control over attachment handling in Apple Mail. It fixes the most annoying Apple Mail flaws, ensures compatibility with other email software, and allows you to set up how... Read more
Duplicate Annihilator 5.0 - Find and del...
Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes. Duplicate Annihilator detects... Read more
jAlbum Pro 12.2 - Organize your digital...
jAlbum Pro has all the features you love in jAlbum, but comes with a commercial license. With jAlbum, you can create gorgeous custom photo galleries for the Web without writing a line of code!... Read more
jAlbum 12.2 - Create custom photo galler...
With jAlbum, you can create gorgeous custom photo galleries for the Web without writing a line of code! Beginner-friendly, with pro results Simply drag and drop photos into groups, choose a design... Read more
Quicken 2015 2.0.4 - Complete personal f...
Quicken 2015 helps you manage all your personal finances in one place, so you can see where you're spending and where you can save. Quicken automatically categorizes your financial transactions,... Read more
iMazing 1.0 - Complete iOS device manage...
iMazing (formerly DiskAid) is the ultimate iOS device manager with capabilities far beyond what iTunes offers. With iMazing and your iOS device (iPhone, iPad, or iPod), you can: Copy music to and... Read more
Xcode 6.0.1 - Integrated development env...
Apple Xcode is Apple Computer's integrated development environment (IDE) for OS X. The full Xcode package is free to ADC members and includes all the tools you need to create, debug, and optimize... Read more

Latest Forum Discussions

See All

View Source – HTML, JavaScript and CSS...
View Source – HTML, JavaScript and CSS 1.0 Device: iOS Universal Category: Utilities Price: $.99, Version: 1.0 (iTunes) Description: View Source is an app plus an iOS 8 Safari extension that makes it easy to do one key web developer... | Read more »
Avenged Sevenfold’s Hail To The King: De...
Avenged Sevenfold’s Hail To The King: Deathbat is Coming to iOS on October 16th Posted by Jessica Fisher on September 19th, 2014 [ permalink ] Just in time for Halloween, on October 16 Avenged Sevenfold will be launching | Read more »
Talisman Has Gone Universal – Can Now be...
Talisman Has Gone Universal – Can Now be Played on the iPhone Posted by Jessica Fisher on September 19th, 2014 [ permalink ] | Read more »
Tap Army Review
Tap Army Review By Jennifer Allen on September 19th, 2014 Our Rating: :: SHOOT EM ALLUniversal App - Designed for iPhone and iPad Mindless but fun, Tap Army is a lane-based shooter that should help you relieve some stress.   | Read more »
Monsters! Volcanoes! Loot! Epic Island f...
Monsters! Volcanoes! Loot! | Read more »
Plunder Pirates: Tips, Tricks, Strategie...
Ahoy There, Seadogs: Interested in knowing our thoughts on all this plundering and pirating? Check out our Plunder Pirates Review! Have you just downloaded the rather enjoyable pirate-em-up Plunder Pirates and are in need of some assistance? Never... | Read more »
Goat Simulator Review
Goat Simulator Review By Lee Hamlet on September 19th, 2014 Our Rating: :: THE GRUFFEST OF BILLY GOATSUniversal App - Designed for iPhone and iPad Unleash chaos as a grumpy goat in this humorous but short-lived casual game.   | Read more »
A New and Improved Wunderlist is Here fo...
A New and Improved Wunderlist is Here for iOS 8 Posted by Jessica Fisher on September 19th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
Evernote Update for iOS 8 Adds Web Clipp...
Evernote Update for iOS 8 Adds Web Clipping, Quick Notes, and More Posted by Ellis Spice on September 19th, 2014 [ permalink ] | Read more »
Apple Names Ultimate Productivity Bundl...
Apple Names Ultimate Productivity Bundle by Readdle as the Essential Bundle on the App Store Posted by Jessica Fisher on September 19th, 2014 [ permalink | Read more »

Price Scanner via MacPrices.net

Updated Price Trackers
We’ve updated our Mac Price Trackers with the latest information on prices, bundles, and availability on systems from Apple’s authorized internet/catalog resellers: - 15″ MacBook Pros - 13″ MacBook... Read more
Mac Pros available for up to $260 off MSRP
Adorama has Mac Pros on sale for up to $260 off MSRP. Shipping is free, and Adorama charges sales tax in NY & NJ only: - 4-core Mac Pro: $2839.99, $160 off MSRP - 6-core Mac Pro: $3739.99, $260... Read more
13-inch 2.6GHz/256GB Retina MacBook Pros avai...
B&H Photo has the 13″ 2.6GHz/256GB Retina MacBook Pro on sale for $1379 including free shipping plus NY sales tax only. Their price is $120 off MSRP. Read more
Previous-generation 15-inch 2.0GHz Retina Mac...
B&H Photo has leftover previous-generation 15″ 2.0GHz Retina MacBook Pros now available for $1599 including free shipping plus NY sales tax only. Their price is $400 off original MSRP. B&H... Read more
21″ 2.7GHz iMac available for $1179, save $12...
Adorama has 21″ 2.7GHz Hawell iMacs on sale for $1179.99 including free shipping. Their price is $120 off MSRP. NY and NJ sales tax only. Read more
iOS 8 Adoption Rate Slower than iOS 7, 6, Hit...
Apple began pushing out iOS 8 updates to eligible devices around 1pm ET on September 17, 2014. However, unlike with iOS 7, which boasted a wide variety of differences from its predecessor iOS 6, in... Read more
LIkely Final Definitive OS X 10.9.5 Mavericks...
Apple has released what will almost certainly be the last incremental version number update of OS X 10.9 Mavericks (save for futire security updates) before OS X 10.10 Yosemite is released next month... Read more
Fingerprints, Apple Pay and Identity Theft Wa...
On Sep 9th, CEO Tim Cook unveiled Apple Pay, along with the new iPhone 6 and iWatch. Apple Pay is a newly developed technology that utilizes a near field communication (NFC) to enable customer... Read more
Amazon Introduces Two All-New Kindles
Amazon on Thursday introduced the 7th generation of its Kindle dedicated e-reader device: Kindle Voyage, its top-of-the-line e-reader, and the new $79 Kindle, with a 20% faster processor, twice the... Read more
Save up to $300 on the price of a new Mac wit...
Purchase a new Mac or iPad at The Apple Store for Education and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free,... Read more

Jobs Board

*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
Project Manager, *Apple* Financial Services...
**Job Summary** Apple Financial Services (AFS) offers consumers, businesses and educational institutions ways to finance Apple purchases. We work with national and Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.