Glenn Lloyd looks at the typical design pitfalls that trap Access beginners. A community service organization was unable to solve a problem with their database. The staff that developed the database had no training or understanding of relational design.
Glenn Lloyd looks at the typical design pitfalls that trap Access beginners. A community service organization was unable to solve a problem with their database. The staff that developed the database had no training or understanding of relational design.
Glenn Lloyd looks at the typical design pitfalls that trap Access beginners. A community service organization was unable to solve a problem with their database. The staff that developed the database had no training or understanding of relational design.
TM Developers 1 Data Modeling for the Access Newcomer, Part 1 Glenn Lloyd 5 Simplifying Queries Russell Sinclair 8 Access Answers: All in the Family Doug Steele 12 November 2005 Downloads November 2005 Volume 13, Number 11 Accompanying files available online at www.pinnaclepublishing.com Applies to Access 2000 Applies to Access 95 Applies to Access 97 2000 2000 2002 2002 Applies to Access 2002 Applies to Access 2003 2003 2003 Data Modeling for the Access Newcomer, Part 1 Glenn Lloyd Thorough, thoughtful, and accurate data modeling should be the starting point of detailed database design. But a surprising number of developers have little or no understanding of data modeling and shy away from what sounds like a non- profitable and time-consuming task. Glenn Lloyd looks at the typical design pitfalls that trap Access beginners and shows the basic techniques that ensure success. M Y call came from a community service organization that provides specialized services both locally and remotely across the vast expanse of Northern Ontario. They were unable to solve a problem with their Access databasea problem that was both embarrassing and damaging to their relations with their membership (and to the community at large). Several months earlier, one of their key members had died. Now, despite their best efforts, his widow and the other organizations with which he had been associated were still receiving their periodic mailings addressed to him. As I listened to the story, I realized that the problem indicated a faulty database design. The staff that had developed the database had no training or understanding of relational design principles and rationale. What they had known was how they wanted to see their data laid out in reports and had designed a data structure that matched those report layouts. What made the problem embarrassing was who had died. The organizations membership is derived from local community organizations located in various centers across Northern Ontario. Board members can represent one or more of these community organizations. The member whose death triggered the problem was a prominent member of one of the communities and either officially or unofficially represented a number of the constituent organizations. This was a very visible mistake. The original sin One of the goals of the original database was to have targeted mailing lists so 2000 2000 2002 2002 2003 2003 2 www.pinnaclepublishing.com Smart Access November 2005 that mailings could be restricted to specific individuals or groups according to the purpose of the mailing. The original database designers concluded that they could best accomplish this objective by subdividing membership data into several tables, one for each of the eight or nine categories that best described the community organizations the members served. This meant that some individuals, including the recently deceased member, had multiple entries, one in each of the relevant membership tables. The member whose information had brought the problem to light had at least four or five of these entries. When all is said and done, a database is nothing more or less than a model of the real world. So, before an efficient relational database can be designed and implemented, the developer or developers must have an in-depth understanding of the nature of the real world that the database will model. A membership database, for example, doesnt contain real people. A database contains information that may be about real people. That information describes who the real people are, where they live, the membership category to which they belong, special skill sets they bring to the group, and any other information the owners of the database need to retain about their members. Data modeling refers to the first step of detailed database design. Data modeling is the basis of the ultimate table and relationship design that, when implemented, becomes the databases structure. Data modelings purpose is to translate the real-world requirements (described either formally or informally) into a formal data structure. The process of data modeling is as vital to database design and implementation as the structure thats produced because the process requires you to study the organization and the information you want the database to track. As a result, when you follow the process you come to know your data very well and move from mere assumption and speculation to in-depth knowledge of the organization and its information needs. Along the way, you define your database structure. While you may have already worked out an intuitive solution to the problem, dont pat yourself on the back. Without a process, you cant guarantee that you would have spotted the problem before it became a problem. And you dont know that youll do as well with every other problem that you face. Data modeling can be done a number of different ways, but Ill walk you through a simple three-step data modeling process. By the end of this article youll see how this process would have led, inevitably, to a practical solution to my clients problem. Step 1: Make a list In this first step, its best to step back from any thoughts about how youll eventually organize the data. Your first step is do a simple brain dump of everything the database is required to track. A formal or informal requirements analysis is the best guide for this step. For example, assume that the database in question is a membership database. The company requesting the database (client, boss, or whoever has determined that the database should be developed) has set out several basic pieces of information they want to know about members. Name and address are obvious, of course. In addition, however, they also want to have some indication of special skills or capabilities the member has to offer the organization, whether the member is a director or executive board member, and the organization or organizations that the member represents along with the category to which the external organization belongs. Step 2: Separate subjects and descriptions The database requirements analysis provides only a guide of what the database is required to track. Those items arent all of the same kind: Some of the items in the list are distinct types of subjects for the database. Others describe or classify the subjects. Typically, your initial brain dump will generate a similar mixture of subjects and descriptions. The goal of the second step is to clearly identify and separate subjects from their descriptions. The technical RDBMS name for the subjects is entity. Of course, the database would be a rather limited tool if all it maintained was a list of entities. In fact, the real purpose of the database is to organize and store bits and pieces of descriptive information (called attributes) about the databases entities. Entities correspond to tables in the ultimate database; attributes correspond to the tables fields. Category or classification information presents a special case because categories themselves are entities whose members describe other entities. Two basic questions guide your work in this step: Which of the list items are entities and which are attributes (information about a particular entity)? For example, if members are one of the subjects of a database (an entity), information describing members might include name, birth date, gender, physical stature, and member or account number (attributes). Each of these attributes speaks directly to who the real member is. By taking the analysis to this level, you now have a model of how youll represent a member in the database and can translate the description into the definition of a table of members. The translation is quite straightforward. The entity (or thing) becomes a table; each attribute becomes a field in the table. The resulting table is shown in Figure 1 and incorporates two design standards that I apply to all my databases. First, each table should have a single field, not part of the data, that identifies the record. This becomes Continues on page 4 www.pinnaclepublishing.com 3 Smart Access November 2005 F U L L
P A G E
A D : F M S 4 www.pinnaclepublishing.com Smart Access November 2005 the tables primary key. Second, each attribute should be indivisible: I should never need, in any application, to pull out data inside the attribute. As you can see in Figure 1, I broke the name down into three component attributes: first name, middle name, and last name. Finding entities Does this table satisfy all of the information needs about the people in the database? Most certainly not! What it does satisfy is the need for information that directly describes each person to the degree that the client and developer have agreed that the person needs to be described for this organization and database. In the list of what to track, in my membership database, members and organizations are quite clearly distinct entities. A member isnt an organization and an organization isnt a member. Not all decisions are so straightforward. For instance, is an address an entity or an attribute? From the data modeling perspective, the answer depends on whether organizations and/or members represented in the database happen to share addresses. In my case, organizations and persons could share addresses. If an organization and a person can share an address, then it suggests that the address is a separate entity with an existence of its own thats independent of the organization or person it belongs to. Therefore, addresses are entities and will have a table of their own. After the obvious physical subjects come the more conceptual subjects that you may need to track. Depending on the organizations business rules, you may need additional entities to track relationships between subjects. For instance, because addresses are an entity in my database, I need an entity to track the relationship between addresses and persons or organizations (or both). In my database, Ill have an Organization/Address table to track the relationship between organizations and addresses. Categories form another problem. For instance, a the person table? The simple answer is no. A category doesnt describe the subject to which the category applies. A category describes how a subject relates to or interacts with other subjects and with the overall organization. A category doesnt describe a subject in isolation from other subjects. A category does have a relationship with a subject, however, and the relationship does require an entity. For instance, look at the category organization type. Rather than being an attribute of the organization subject, the organization type describes a relationship among organizations: Types only make sense if several different entities share the same type. Since an organization has a relationship with the organization type (an organization belongs to a type), you need an entity to track relationships between organizations and their types: an Organization/Category table. The same kind of analysis applies to a persons role within an organization: directors, officers, and executives. Each of these terms describes a particular role a member might have in an organization. In other words, theyre a category that describes the relationship between an organization and a member: You cant be a president unless you have an organization to be president of. So I need a Membership/Role table to track the relationship between a member and a role. While I could have had separate tables for MemberCategory and OrganizationCategory, I chose not to. As Figure 2 shows, the categories for organization and members share a common Categories table with tables that describe the MemberCategory/Member and Organization/OrganizationCategory. The rules that drive this decision are worth explaining. However, youll have to come back next month for that. L Glenn Lloyd is a freelance Access database developer and desktop applications trainer. His present work is solidly grounded in extensive experience in administration and accounting for charitable organizations. Recently appointed as a Forum Administrator, Glenn has been an active member of UtterAccess.com since 2002. He lives and works in Sudbury, Ontario, Canada. Data Modeling... Continued from page 2 Figure 1. Table of members. Figure 2. Organizations, members, and categories. person can be a president, board member, or have some other role in the organization. Dont those categories describe the person and, therefore, shouldnt they be represented by additional fields in www.pinnaclepublishing.com 5 Smart Access November 2005 Smart Access Simplifying Queries Russell Sinclair Rather than define every query that your users might require, why not let your users make up their queries as they need themprovided that theyre not going to be overwhelmed by the options available to them. Russell Sinclair discusses how to create a simplified query interface for Access users. I F youre reading this article, chances are that you have a reasonably good idea of how to work with queries in Access databases. You know how to use the query designer to get at the data you want and how to use that data in your applications. However, can you say that your users have those same skills? One of the companies Im working with right nowM7 Database Services at www.m7database.com specializes in developing Access solutions for small to mid-sized companies or branch offices for large companies. These solutions tend to be aimed at a small group of end users who generally have little or no database design experience. Many of these people dont know how to create queries or work with tables and other Access objects (thats why they hire the experts). So when the users wanted to start running their own queries, M7 needed a tool that could simplify the query process. The first design conversations we had about creating a simplified query tool resulted in all sorts of suggestions, including the ability to limit fields, group fields, and calculate totals. We realized, however, that with too many features, the query builder would simply be a copy of the the results and export them to other applications. It also wouldnt hurt if the application worked with both MDBs and ADPs. Reading the queries When I created the query building application, I knew Id want to be able to allow users to use only a select set of queries with the tool. I didnt want them to be able to use just any query in the database because that would probably make the tool much harder to use. Instead, I came up with a naming convention for these queries: Queries that had a prefix of qbf could be used with our tool. In the code module fdlgQueryBuilder in the sample database, the ListQueries function in that form returns a list of all of the qbf queries. The code loops through the objects in the AllViews, AllFunctions, and AllQueries properties for the CurrentData object. When it runs across a query that starts with qbf, it adds the query to a local table tblQuery with the name of the query, the query type, and the name without the prefix as the name displayed for the query to an end user. This data is used to populate the Query dropdown in the builder form shown in Figure 1. Once a user selects a query on this form, the code analyzes it to see what fields it contains and gathers information on the data contained in those fields. The way that this is done is different for SQL Server queries and Access queries. In fact, its probably the first time I Figure 1. Query builder main screen. Access query designer. Apart from the fact that it might get too complicated for users to work with, we didnt want to reinvent the wheel. If the users were sophisticated enough to use these features, they were probably capable of using the Access query designer. We finally settled on the features that we knew would be required: The query builder would have to base the data it worked with on a query that we pre-created for the users. This would insulate the users from having to use the designers to create their queries. The users would have to have the ability to filter columns in the query to specific values they would choose. The users would need to be able to select the fields that get output, or output all fields. The users would need the ability to view 2000 2000 2002 2002 2003 2003 6 www.pinnaclepublishing.com Smart Access November 2005 can think of when I could justify using both DAO and ADO in the same procedure. If you look at the code tied to the AfterUpdate event of the cboQuery combo box, youll see both of these methods. When working with the Access objects, I referenced the QueryDef object that represented that query. This object allowed me to easily loop through the available fields. For the SQL Server objects, I had to the use the OpenSchema function on the ADO Connection object (see the sidebar, Connection.OpenSchema) calling for the adSchemaColumns, or adSchemaProcedureColumns recordset. I then used the data in this recordset to populate the data in tblField. Although I would have liked to standardize the code for Access and SQL databases, not all data providers support all of the schemas this function can return. This is the case with Access and the adSchemaProcedureColumns enumeration values. For each field, I stored the name, position, and type. This information is used in the criteria sub-form to allow users to pick the fields from the dropdowns and to help me format and validate data the user enters. The user interface is reasonably simple. A user can select the query from the list and then select a field to use in the sub-form. In order to maintain the ease of use of the application, the only comparison operators I chose to implement were equals, does not equal, greater than, and less than. The user can select one of these operators and enter up to three criteria that are ORed together. I didnt provide users the ability to define how criteria related to each other. The main reason behind this decision was to avoid confusion around the order of operations when mixing ANDs and ORs. What I did, instead, was to treat each criterion specified as cumulative so that its ANDed with other criteria. All of these criteria are stored in tblCriteria and used when the user clicks the View Results button to generate the SQL statement for the resulting data. Viewing the results Generating SQL is probably something weve all tried at some point or another. The code in basQueryBuilder is the result of many lessons learned through past projects. The code in this module splits the task of building the SQL string into two units: building the SELECT string, and building the WHERE clause. The SELECT string is easily defined by the selected fields in tblCriteria or all fields. A simple comma-delimited list of fields is built or a wildcard is used. The WHERE clause is slightly more complicated. Whenever one of the value fields is filled in, the WhereClause function calls the CriteriaString function to build a criteria string. This function determines the right operators to use, handles text formatting to use for dates, number, and Boolean fields, and performs wildcard conversion. The path that it takes through the function is very much dependent on the data type of the field thats being analyzed. With the SQL statement complete, the application is ready to submit the statement, retrieve the data, and present the data to the user. However, I didnt want to have to go to an external interface for the users to be able to see the data. I wanted them to be able to open a form and preview the data in place, with all of the functionality available that Access can provide. This meant that I had to modify the design of a form on the fly. The click event of cmdViewResults in the query builder dialog takes care of this for me. The SQL statement is used to open an ADO recordset object. The code opens the sub-form fsfrQueryResults (which will display the results) in design view but hidden. The code then removes any existing controls on the form and then adds a checkbox for each Boolean field or a textbox for any other field. This is handled through the Application .CreateControl method. The form is displayed in datasheet view so I dont need to worry about the layout of the controlsthey automatically show up in the dataset in the order in which theyre created on the form. With all the controls created, the form is closed and saved. After that, the parent form for this sub-form is opened and the ADO recordset I got earlier to the Recordset property of the sub-form is assigned to its Recordset property. This allows me to use either SQL Server or Access data without having the change the code. The form is shown in Figure 2. The results form allows the user to preview the Connection.OpenSchema The OpenSchema method of the ADO Connection object returns information about the data store defined in the connection. It allows you, among other things, to list tables, queries, constraints, and indexes. It can also be used to list users in a database and many other things. This function takes three parameters. The first parameter is a value of the SchemaEnum enumeration that defines the information you want to get. The second parameter is an optional array of restrictions you want to place on the data. Each member of the array corresponds to a particular column in the result set. The Access Help has information on what restrictions can be used with each schema. The final parameter, SchemaID, is a GUID value thats only used if the schema requested is adSchemaProviderSpecific. This special schema type allows you to request information thats custom tailored to the provider, such as the value {947bb102-5d43-11d1-bdbf- 00c04fb92675}, which will cause the function to return the list of users connected to an Access database. The result of the OpenSchema call is a read-only ADO Recordset. www.pinnaclepublishing.com 7 Smart Access November 2005 H A L F
P A G E
A D : B L A C K
M O S H A N N O N data or copy it from Access to some other application. The data can also be exported from the form by a button click. The button simply calls DoCmd.OutputTo to export the data. How to use it The resulting application is available in the accompanying download. The application is designed as an add-in, which allows it to work with both SQL Server and Access data without changing your code. However, if you would rather integrate the code right into an application, you can import all of the objects from the sample Figure 2. Query results. database into your own project. All you need to do to start the application is open the query builder dialog. Sometimes simplicity in design can lead to a better user experience. This tool empowers beginner users with their data in much the same way that the Access query designer can empower more advanced users. The feedback weve had on this tool so far has been extremely favorable. Think about including something like this in your next project and see how much you can improve the usability of your application. L 511SINCLAIR.ZIP at www.pinnaclepublishing.com Russell Sinclair is an MSCD and is the owner of Synthesystems (www.synthesystems.com), an independent consulting firm specializing in .NET, SQL Server, and Microsoft Access development. Hes the author of From Access to SQL Server, an Access developers guide to migrating to SQL Server, and a Smart Access Contributing Editor. 8 www.pinnaclepublishing.com Smart Access November 2005 Access Answers Smart Access All in the Family Doug Steele This month, Doug Steele looks at how to handle tables where multiple types of data are in the same table. I LL begin by mentioning that this problem came from a daycare that wanted to be able to produce cards that each parent could carry to prove that they were entitled to pick up the specific children. Its not often that you get to help out with a problem that means this much to so many people. I have a table where each family member is in a separate record (imported from another program that has it that way). Each record has a FamilyId field, as well as a FamilyPosition field (head, spouse, child). I need to make a name tag that gets the name of the family head from one record and the spouses name from another record and puts them together on the same line. Then I need to get the names of all of the children together on a second line. For the purposes of illustration, Ill assume that the table has the fields listed in Table 1. Table 1. Details of the Family table. Field name Data type Id AutoNumber (PK) FirstName Text LastName Text FamilyId Long Integer FamilyPosition Text Since the data is simplified (it only shows a current snapshot of the family, so I dont have to worry about previous spouses), its reasonable to assume that theres at most a one-to-one relationship between family head and family spouse. That means I should be able to use SQL to relate head to spouse. One way of doing this is to save a couple of queries: one that returns only family heads, and one that returns only family spouses. The SQL for these queries would look like this: SELECT ID, FirstName, LastName, FamilyId FROM Family WHERE FamilyPosition="Head" SELECT ID, FirstName, LastName, FamilyId Table 2. Results of running the query on the sample data (given how left joins work, the empty cells are actually Null, not blank). HeadFirstName HeadLastName SpouseFirstName SpouseLastName FamilyId Jennifer Berry 214 David Jones Cheryl Jones 506 Mark Smith Mandy Brown 360 FROM Family WHERE FamilyPosition="Spouse" Ill name these two queries qryFamilyHead and qryFamilySpouse, respectively, and then write a query that joins the two together: SELECT Head.FirstName AS HeadFirstName, Head.LastName AS HeadLastName, Spouse.FirstName AS SpouseFirstName, Spouse.LastName AS SpouseLastName, Head.FamilyId FROM qryFamilyHead AS Head LEFT JOIN qryFamilySpouse AS Spouse ON Head.FamilyId = Spouse.FamilyId; Running this query against the sample data in the download database gives the results shown in Table 2. In Access 2000 and newer, you can actually do this with only a single query: SELECT Head.FirstName AS HeadFirstName, Head.LastName AS HeadLastName, Spouse.FirstName AS SpouseFirstName, Spouse.LastName AS SpouseLastName, Head.FamilyId FROM (SELECT ID, FirstName, LastName, FamilyId FROM Family WHERE FamilyPosition="Head") AS Head LEFT JOIN (SELECT ID, FirstName, LastName, FamilyId FROM Family WHERE FamilyPosition="Spouse") AS Spouse ON Head.FamilyId = Spouse.FamilyId; Regardless of whether you use one query or two, these queries wont necessarily give the data in the most useful format. Usually, given the data shown in Figure 1, people want to see the names like this: Jennifer Berry David & Cheryl Jones Mark Smith & Mandy Brown In other words, if theres no spouse, the desired result should just be HeadFirstName HeadLastName. If there is a spouse, the query should check whether HeadLastName and SpouseLastName are the same. 2000 2000 2002 2002 2003 2003 www.pinnaclepublishing.com 9 Smart Access November 2005 If they are, the user will want to see HeadFirstName & SpouseFirstName HeadLastName. If not, the desired result is HeadFirstName HeadLastName & SpouseFirstName SpouseLastName. This can be handled using a couple of IIf Working with a data domain implies that Im going to need to work with a recordset. Im also going to need to create a SQL string to create the recordset, as well as a variable to use to hold the concatenated values: Dim rstCurr As DAO.Recordset Dim strConcatenate As String Dim strSQL As String The SQL string needed to create the recordset relies on the values passed to the function for Expr, Domain, and Criteria: strSQL = "SELECT " & Expr & " AS TheValue " & _ "FROM " & Domain If Len(Criteria) > 0 Then strSQL = strSQL & " WHERE " & Criteria End If So the code opens the recordset and then loops through the records concatenating the data into a single variable along with the Separator value. This code concatenates the values, adding the separator after each value, and then removes the final separator at the end: Set rstCurr = CurrentDb().OpenRecordset(strSQL) Do While rstCurr.EOF = False strConcatenate = strConcatenate & _ rstCurr!TheValue & Separator rstCurr.MoveNext Loop If Len(strConcatenate) > 0 Then strConcatenate = _ Left$(strConcatenate, _ Len(strConcatenate) - Len(Separator)) End If Once Ive looped through all of the rows in the recordset, alls that left is to clean up: rstCurr.Close Set rstCurr = Nothing DConcatenate = strConcatenation End Function In this situation, the Expr that Im interested in is FirstName. The Domain, of course, is the table Family. The only records of interest are those where FamilyPosition is child and have matching FamilyIds. For instance, if I want the names of all of the children in family 506, the call to DConcatenate would be: DConcatenate("FirstName","Family", _ "FamilyPosition = 'child' And FamilyId = 506") This call would return Jeremy, Julie, Amy. If you look in the accompanying database, youll see Figure 1. Output of query showing Family Head and Spouse information. statements in the SQL statement: IIf(IsNull(Spouse.LastName), _ Head.FirstName & " " & Head.LastName, _ IIf(Spouse.LastName=Head.LastName, Head.FirstName & " & " & Spouse.FirstName & _ " " & Head.LastName,Head.FirstName & " " & _ Head.LastName & " & " & Spouse.FirstName & _ " " & Spouse.LastName)) Figure 1 shows the results of adding those functions to the query shown earlier. Okay, that gave me the name of the family head from one record, and the spouses name from another record, and puts them together on the same line. How do I concatenate the names of all of the children together as a single line? Concatenating multiple related records into a single result is a fairly common request with one-to-many relationships, but, unfortunately, its not easily supported using SQL, so Ill look at creating a function to do it. Even though I dont appear to have a one-to-many situation (since I only have a single table), I recognized that the data could realistically be thought of as comprising two tablesone for the family, and one for the family membersso if we have a concatenation function, it should be of use to us. Whats required is to return all of the records that are related to one another, and concatenate them into a single field. Working with sets of related records is what Domain Aggregate functions (DAvg, DCount, DLookup, and so on) are all about, but unfortunately there isnt a built-in DConcatenate function in Access, so Im going to create one. The general syntax for Domain Aggregate functions is Dfunction(expr, domain[, criteria]), where Expr is a string expression that identifies the field whose value you want to work with, Domain is a string expression identifying the set of records that constitutes the domain (a table name or a query name), and the optional Criteria is a string expression used to restrict the range of data on which the function is performed. To this, Im going to add an additional optional parameter, Separator, which will let me specify what character is supposed to be used to separate the concatenated values. If not supplied, , (a comma followed by a blank field) is used: Function DConcatenate( _ Expr As String, _ Domain As String, _ Optional Criteria As String = vbNullString, _ Optional Separator As String = ", " _ ) As String 10 www.pinnaclepublishing.com Smart Access November 2005 that Ive created query qryFamilyNames, which uses the preceding query and the DConcatenate function to return both the information on the parents and the information about the children: SELECT Head.FirstName AS HeadFirstName, Head.LastName AS HeadLastName, Spouse.FirstName AS SpouseFirstName, Spouse.LastName AS SpouseLastName, Head.FamilyId, IIf(IsNull(Spouse.LastName),Head.FirstName & " " & Head.LastName, IIf(Spouse.LastName=Head.LastName, Head.FirstName & " & " & Spouse.FirstName & " " & Head.LastName,Head.FirstName & " " & Head.LastName & " & " & Spouse.FirstName & " " & Spouse.LastName)) AS DisplayName, DConcatenate("FirstName", "Family", "FamilyPosition = 'child' And FamilyId =" & [Head].[FamilyId]) AS Children FROM qryFamilyHead AS Head LEFT JOIN qryFamilySpouse AS Spouse ON Head.FamilyId = Spouse.FamilyId Figure 2 shows the results of adding that to the query I showed earlier. Okay, thats almost what I wanted. Sometimes the children dont have the same last name as their parents. Can I get the childs surname shown as well? The simple answer is that the DConcatenate function can actually return more than one field. If you change the call to the previous DConcatenate function to this: DConcatenate("FirstName & ' ' & LastName", "Family", "FamilyPosition = 'child' And FamilyId =" & [Head].[FamilyId]) the query will return the result shown in Table 3. Table 3. Children with different surnames, result 1. Jason Berry, Chloe Berry Jeremy Jones, Julie Jones, Amy Jones Brittany Smith, Jessica Brown If what you want, however, is the result in Table 4, its going to be a little more work (and it will no longer be possible to use a generic function such as the DConcatenate function from earlier). Table 4. Children with different surnames, result 2. Chloe & Jason Berry Amy, Jeremy & Julie Jones Jessica Brown and Brittany Smith What has to be done in this case is open a recordset that returns both FirstName and LastName for the children in a given family. Youll then need to order the recordset so that rows with the same LastName are grouped together. For the first row in the recordset, the code concatenates the FirstName to the working concatenation string. For each subsequent row, the code determines whether or not the LastName is the same as the previous LastName. If it is, I concatenate a comma and the current FirstName to the working string. If it isnt, I determine whether the last thing added to the working string was a comma followed by a FirstName, or just a FirstName. If its a comma, then I replace it with an ampersand. In either case, the next step is to add a space and the previous LastName. Once thats done that, I can concatenate the previous word followed by the new FirstName. I suspect that the code is less complicated than those instructions. The opening section declares some variables: Function ConcatChildren( _ FamilyId As Long _ ) As String Dim dbCurr As DAO.Database Dim rsCurr As DAO.Recordset Dim intSameLastName As Integer Dim strChildren As String Dim strPrevFirstName As String Dim strPrevLastName As String Dim strSQL As String strChildren = vbNullString I then create the SQL string to return a recordset for all the children in the specified family, ordered by LastName (adding FirstName in the ORDER BY clause isnt critical to the solution): strSQL = "SELECT FirstName, LastName " & _ "FROM Family " & _ "WHERE FamilyId = " & FamilyId & _ " AND FamilyPosition = 'child' " & _ "ORDER BY LastName, FirstName" Set dbCurr = CurrentDb Set rsCurr = dbCurr.OpenRecordset(strSQL) Now I look at each record in the recordset that was returned: With rsCurr If .RecordCount <> 0 Then Do While Not .EOF If strPrevLastName <> !LastName Then If strPrevLastName doesnt contain anything, then this is the first record. I use only the first name until I Figure 2. Query output showing Family Head and Spouse information, and Children names. www.pinnaclepublishing.com 11 Smart Access November 2005 Subscribe to Smart Access today and receive a special one-year introductory rate: Just $129* for 12 issues (thats $20 off the regular rate) Pinnacle, A Division of Lawrence Ragan Communications, Inc. L 800-493-4867 x.4209 or 312-960-4100 L Fax 312-960-4106 NAME COMPANY ADDRESS CITY STATE/PROVINCE ZIP/POSTAL CODE COUNTRY IF OTHER THAN U.S. E-MAIL PHONE (IN CASE WE HAVE A QUESTION ABOUT YOUR ORDER) Dont miss another issue! Subscribe now and save! K Check enclosed (payable to Pinnacle Publishing) K Purchase order (in U.S. and Canada only); mail or fax copy K Bill me later K Credit card: __ VISA __MasterCard __American Express CARD NUMBER EXP. DATE SIGNATURE (REQUIRED FOR CARD ORDERS) * Outside the U.S. add $30. Orders payable in U.S. funds drawn on a U.S. or Canadian bank. Detach and return to: Pinnacle Publishing L 316 N. Michigan Ave. L Chicago, IL 60601 Or fax to 312-960-4106 INS5 find out the last name of the next child. I used a counter to check whether this is the first name with the given last name: If Len(strPrevLastName) = 0 Then strChildren = strChildren & _ !FirstName intSameLastName = 1 If strPrevLastName does contain a value, then I know that Im not on the first record, and that the previous record has a different last name than the current record. I want to add the previous last name to the string that holds my concatenated list. However, I need to check whether or not theres only one child with the previous last name (in which case I simply concatenate the previous last name), or if theres more than one (in which case I know that I used a comma when concatenating the previous first name to the list, so I want to change the comma to an ampersand before we continue). I can use the variable intSameLastName to tell me how many children had the same last name: Else If intSameLastName = 1 Then strChildren = strChildren & _ " " & strPrevLastName & _ " and " & !FirstName Else strChildren = Left$(strChildren, _ Len(strChildren) - _ Len(strPrevFirstName) - 2) strChildren = strChildren & _ " & " & strPrevFirstName & _ " " & strPrevLastName & _ " and " & !FirstName End If intSameLastName = 1 End If If the current record has the same last name as the previous record, all I do is concatenate the current FirstName (prefixed with a comma) to my concatenation string. I also have to make sure to increment intSameLastName so that I can have a count of how many children have the same last name: Else strChildren = strChildren & _ ", " & !FirstName intSameLastName = intSameLastName + 1 End If Finally, I save the current names, and move onto the next record: strPrevFirstName = !FirstName strPrevLastName = !LastName .MoveNext Loop After the loop is finished, I still have a last name that hasnt been added to my concatenation string. I use the same logic as before to determine what to add if theres only one child with the previous last name, or if there are many: If intSameLastName = 1 Then strChildren = strChildren & _ " " & strPrevLastName Else strChildren = Left$(strChildren, _ Len(strChildren) - _ Len(strPrevFirstName) - 2) strChildren = strChildren & " & " & _ strPrevFirstName & " " & strPrevLastName End If End If End With 12 www.pinnaclepublishing.com Smart Access November 2005 November 2005 Downloads For access to current and archive content and source code, log in at www.pinnaclepublishing.com. Smart Access (ISSN 1066-7911) is published monthly (12 times per year) by: Pinnacle Publishing A Division of Lawrence Ragan Communications, Inc. 316 N. Michigan Ave., Suite 300 Chicago, IL 60601 POSTMASTER: Send address changes to Lawrence Ragan Communications, Inc., 316 N. Michigan Ave., Suite 300, Chicago, IL 60601. Copyright 2005 by Lawrence Ragan Communications, Inc. All rights reserved. No part of this periodical may be used or reproduced in any fashion whatsoever (except in the case of brief quotations embodied in critical articles and reviews) without the prior written consent of Lawrence Ragan Communications, Inc. Printed in the United States of America. Brand and product names are trademarks or registered trademarks of their respective holders. Microsoft is a registered trademark of Microsoft Corporation. Access is a trademark or registered trademark of Microsoft Corporation in the United States and/or other countries and is used by Ragan Communications, Inc. under license from owner. Smart Access is an independent publication not affiliated with Microsoft Corporation. Microsoft Corporation is not responsible in any way for the editorial policy or other contents of the publication. This publication is intended as a general guide. It covers a highly technical and complex subject and should not be used for making decisions concerning specific products or applications. This publication is sold as is, without warranty of any kind, either express or implied, respecting the contents of this publication, including but not limited to implied warranties for the publication, quality, performance, merchantability, or fitness for any particular purpose. Lawrence Ragan Communications, Inc, shall not be liable to the purchaser or any other person or entity with respect to any liability, loss, or damage caused or alleged to be caused directly or indirectly by this publication. Articles published in Smart Access do not necessarily reflect the viewpoint of Lawrence Ragan Communications, Inc. Inclusion of advertising inserts does not constitute an endorsement by Lawrence Ragan Communications, Inc., or Smart Access. Questions? Customer Service: Phone: 800-920-4804 or 312-960-4100 Fax: 312-960-4106 Email: PinPub@Ragan.com Advertising: RogerS@Ragan.com Editorial: FarionG@Ragan.com Pinnacle Web Site: www.pinnaclepublishing.com Subscription rates United States: One year (12 issues): $149; two years (24 issues): $258 Other:* One year: $179; two years: $318 Single issue rate: $20 ($25 outside United States)* * Funds must be in U.S. currency. Editor: Peter Vogel (peter.vogel@phvis.com) Contributing Editors: Mike Gunderloy, Danny J. Lesandrini, Garry Robinson, Russell Sinclair CEO & Publisher: Mark Ragan Group Publisher: Michael King Executive Editor: Farion Grove Finally, I clean up and return the working concatenation string: rsCurr.Close Set rsCurr = Nothing Set dbCurr = Nothing ConcatChildren = strChildren End Function Yeah, its a lot of work, but it seems to do the trick! While the original questioner didnt request this additional functionality, its fairly straightforward to extend the model to support allowing additional people to be associated with each family, so that its possible to pre- approve a neighbor or relative picking up the children. You could do this by including a new FamilyPosition value of, say, friend. You would then use a query along the lines of: SELECT FirstName, LastName, Null, Null, FamilyId, FirstName & : " & LastName AS DisplayName, DConcatenate("FirstName", "Family", "FamilyPosition = 'child' And FamilyId =" & [FamilyId]) AS Children FROM Family The only reason I included the two Null fields in this query was to ensure that it included the same number of fields as the original query. In this way, its possible to UNION together the two queries when trying to produce the report. L 511STEELE.ZIP at www.pinnaclepublishing.com Doug Steele has worked with databases, both mainframe and PC, for many years. Microsoft has recognized him as an Access MVP for his contributions to the Microsoft-sponsored newsgroups. Check http://I.Am/DougSteele for some Access information, as well as Access-related links. He enjoys hearing from readers who have ideas for future columns, though personal replies arent guaranteed. AccessHelp@rogers.com. 511SINCLAIR.ZIPRussell Sinclairs sample database is actually an Access add-in that you can incorporate into your applications. This add-in provides a simple but effective interface that empowers users to create their own SQL queries. 511STEELE.ZIPDoug Steele has provided the sample code for the complex processing of the wide variety of family structures and naming conventions that are common in todays world. And all Doug wanted to do was produce a list with everyones name presented correctly.